diff options
Diffstat (limited to 'library/Director')
219 files changed, 3558 insertions, 2559 deletions
diff --git a/library/Director/Acl.php b/library/Director/Acl.php index 4aa2bd2..44c23f5 100644 --- a/library/Director/Acl.php +++ b/library/Director/Acl.php @@ -56,7 +56,7 @@ class Acl public function listRoleNames() { return array_map( - array($this, 'getNameForRole'), + [$this, 'getNameForRole'], $this->getUser()->getRoles() ); } diff --git a/library/Director/Auth/MonitoringRestriction.php b/library/Director/Auth/MonitoringRestriction.php new file mode 100644 index 0000000..1fb6013 --- /dev/null +++ b/library/Director/Auth/MonitoringRestriction.php @@ -0,0 +1,36 @@ +<?php + +namespace Icinga\Module\Director\Auth; + +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; + +class MonitoringRestriction +{ + public static function getObjectsFilter(Auth $auth): Filter + { + $restriction = Filter::matchAny(); + $restriction->setAllowedFilterColumns([ + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + function ($c) { + return preg_match('/^_(?:host|service)_/i', $c); + } + ]); + foreach ($auth->getRestrictions(Restriction::MONITORING_RW_OBJECT_FILTER) as $filter) { + if ($filter === '*') { + return Filter::matchAll(); + } + $restriction->addFilter(Filter::fromQueryString($filter)); + } + + if ($restriction->isEmpty()) { + return Filter::matchAll(); + } + + return $restriction; + } +} diff --git a/library/Director/Auth/Permission.php b/library/Director/Auth/Permission.php new file mode 100644 index 0000000..c29d789 --- /dev/null +++ b/library/Director/Auth/Permission.php @@ -0,0 +1,31 @@ +<?php + +namespace Icinga\Module\Director\Auth; + +class Permission +{ + public const ALL_PERMISSIONS = 'director/*'; + public const ADMIN = 'director/admin'; // internal, assign ALL_PERMISSONS + public const API = 'director/api'; + public const AUDIT = 'director/audit'; + public const DEPLOY = 'director/deploy'; + public const DEPLOYMENTS = 'director/deployments'; // internal, assign ALL_PERMISSONS + public const GROUPS_FOR_RESTRICTED_HOSTS = 'director/groups-for-restricted-hosts'; + public const HOSTS = 'director/hosts'; + public const HOST_GROUPS = 'director/hostgroups'; // internal, assign ALL_PERMISSIONS + public const INSPECT = 'director/inspect'; + public const MONITORING_SERVICES_RO = 'director/monitoring/services-ro'; + public const MONITORING_SERVICES = 'director/monitoring/services'; + public const MONITORING_HOSTS = 'director/monitoring/hosts'; + public const ICINGADB_SERVICES_RO = 'director/icingadb/services-ro'; + public const ICINGADB_SERVICES = 'director/icingadb/services'; + public const ICINGADB_HOSTS = 'director/icingadb/hosts'; + public const NOTIFICATIONS = 'director/notifications'; + public const SCHEDULED_DOWNTIMES = 'director/scheduled-downtimes'; + public const SERVICES = 'director/services'; + public const SERVICE_SETS = 'director/servicesets'; + public const SERVICE_SET_APPLY = 'director/service_set/apply'; + public const SHOW_CONFIG = 'director/showconfig'; + public const SHOW_SQL = 'director/showsql'; + public const USERS = 'director/users'; +} diff --git a/library/Director/Auth/Restriction.php b/library/Director/Auth/Restriction.php new file mode 100644 index 0000000..3394dcc --- /dev/null +++ b/library/Director/Auth/Restriction.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Auth; + +class Restriction +{ + public const MONITORING_RW_OBJECT_FILTER = 'director/monitoring/rw-object-filter'; + public const ICINGADB_RW_OBJECT_FILTER = 'director/icingadb/rw-object-filter'; + public const FILTER_HOSTGROUPS = 'director/filter/hostgroups'; + + // Hint: by-name-Filters are being fetched with variable names, like "director/$type/apply/filter-by-name" + public const NOTIFICATION_APPLY_FILTER_BY_NAME = 'director/notification/apply/filter-by-name'; + public const SCHEDULED_DOWNTIME_APPLY_FILTER_BY_NAME = 'director/scheduled-downtime/apply/filter-by-name'; + public const SERVICE_APPLY_FILTER_BY_NAME = 'director/service/apply/filter-by-name'; + public const SERVICE_SET_FILTER_BY_NAME = 'director/service_set/filter-by-name'; + const DB_RESOURCE = 'director/db_resource'; +} diff --git a/library/Director/Cli/Command.php b/library/Director/Cli/Command.php index 69d61b1..7268913 100644 --- a/library/Director/Cli/Command.php +++ b/library/Director/Cli/Command.php @@ -82,9 +82,9 @@ class Command extends CliCommand { MemoryLimit::raiseTo('1024M'); - ini_set('max_execution_time', 0); + ini_set('max_execution_time', '0'); if (version_compare(PHP_VERSION, '7.0.0') < 0) { - ini_set('zend.enable_gc', 0); + ini_set('zend.enable_gc', '0'); } return $this; diff --git a/library/Director/Cli/ObjectCommand.php b/library/Director/Cli/ObjectCommand.php index ca68213..ed99c14 100644 --- a/library/Director/Cli/ObjectCommand.php +++ b/library/Director/Cli/ObjectCommand.php @@ -428,7 +428,7 @@ class ObjectCommand extends Command } $stdin = file_get_contents('php://stdin'); - if (strlen($stdin) === 0) { + if (! $stdin) { return null; } diff --git a/library/Director/Core/CoreApi.php b/library/Director/Core/CoreApi.php index ea10916..73588c2 100644 --- a/library/Director/Core/CoreApi.php +++ b/library/Director/Core/CoreApi.php @@ -569,7 +569,7 @@ constants 'icon_image_alt' => 'icon_image_alt', ]; - if (version_compare($this->getVersion(), '2.8.0', '>=')) { + if (version_compare($this->getVersion() ?? '', '2.8.0', '>=')) { $params['flapping_threshold_high'] = 'flapping_threshold_high'; $params['flapping_threshold_low'] = 'flapping_threshold_low'; } @@ -622,7 +622,7 @@ constants { IcingaCommand::setPluginDir($this->getConstant('PluginDir')); - $objects = $this->getDirectorObjects('Command', "${type}Commands", [ + $objects = $this->getDirectorObjects('Command', "{$type}Commands", [ 'arguments' => 'arguments', // 'env' => 'env', 'timeout' => 'timeout', diff --git a/library/Director/Core/LegacyDeploymentApi.php b/library/Director/Core/LegacyDeploymentApi.php index 7287c4a..0ab77e0 100644 --- a/library/Director/Core/LegacyDeploymentApi.php +++ b/library/Director/Core/LegacyDeploymentApi.php @@ -128,6 +128,10 @@ class LegacyDeploymentApi implements DeploymentApiInterface if (file_exists($path)) { if (is_link($path)) { $linkTarget = readlink($path); + if (! $linkTarget) { + throw new IcingaException('Failed to read symlink'); + } + $linkTargetDir = dirname($linkTarget); $linkTargetName = basename($linkTarget); @@ -165,7 +169,7 @@ class LegacyDeploymentApi implements DeploymentApiInterface $this->assertDeploymentPath(); $dh = @opendir($this->deploymentPath); - if ($dh === null) { + if ($dh === false) { throw new IcingaException('Can not list contents of %s', $this->deploymentPath); } @@ -279,7 +283,7 @@ class LegacyDeploymentApi implements DeploymentApiInterface $this->mkdir(dirname($fullPath), true); $fh = @fopen($fullPath, 'w'); - if ($fh === null) { + if ($fh === false) { throw new IcingaException('Could not open file "%s" for writing.', $fullPath); } chmod($fullPath, $this->file_mode); @@ -334,7 +338,7 @@ class LegacyDeploymentApi implements DeploymentApiInterface protected function listDirectoryContents($path, $depth = 0) { $dh = @opendir($path); - if ($dh === null) { + if ($dh === false) { throw new IcingaException('Can not list contents of %s', $path); } diff --git a/library/Director/Core/RestApiClient.php b/library/Director/Core/RestApiClient.php index b0854ff..19f6b68 100644 --- a/library/Director/Core/RestApiClient.php +++ b/library/Director/Core/RestApiClient.php @@ -206,14 +206,14 @@ class RestApiClient } /** - * @return resource + * @throws RuntimeException */ protected function curl() { if ($this->curl === null) { $this->curl = curl_init(sprintf('https://%s:%d', $this->peer, $this->port)); if (! $this->curl) { - throw new RuntimeException('CURL INIT ERROR: ' . curl_error($this->curl)); + throw new RuntimeException('CURL INIT FAILED'); } } @@ -260,13 +260,11 @@ class RestApiClient public function disconnect() { - if ($this->curl !== null) { - if (is_resource($this->curl)) { - @curl_close($this->curl); - } - - $this->curl = null; + if ($this->curl) { + @curl_close($this->curl); } + + $this->curl = null; } public function __destruct() diff --git a/library/Director/Core/RestApiResponse.php b/library/Director/Core/RestApiResponse.php index 523ed35..43516f7 100644 --- a/library/Director/Core/RestApiResponse.php +++ b/library/Director/Core/RestApiResponse.php @@ -113,7 +113,7 @@ class RestApiResponse throw new IcingaException('API request failed: ' . $result->status); } } else { - throw new IcingaException('API request failed: ' . var_export($result, 1)); + throw new IcingaException('API request failed: ' . var_export($result, true)); } } diff --git a/library/Director/CustomVariable/CustomVariable.php b/library/Director/CustomVariable/CustomVariable.php index 98eda84..4b5dd3e 100644 --- a/library/Director/CustomVariable/CustomVariable.php +++ b/library/Director/CustomVariable/CustomVariable.php @@ -236,7 +236,7 @@ abstract class CustomVariable implements IcingaConfigRenderer // TODO: check for specific class/stdClass/interface? return new CustomVariableDictionary($key, $value); } else { - throw new LogicException(sprintf('WTF (%s): %s', $key, var_export($value, 1))); + throw new LogicException(sprintf('WTF (%s): %s', $key, var_export($value, true))); } } diff --git a/library/Director/CustomVariable/CustomVariableBoolean.php b/library/Director/CustomVariable/CustomVariableBoolean.php index 9953fae..750f1d6 100644 --- a/library/Director/CustomVariable/CustomVariableBoolean.php +++ b/library/Director/CustomVariable/CustomVariableBoolean.php @@ -31,7 +31,7 @@ class CustomVariableBoolean extends CustomVariable if (! is_bool($value)) { throw new ProgrammingError( 'Expected a boolean, got %s', - var_export($value, 1) + var_export($value, true) ); } diff --git a/library/Director/CustomVariable/CustomVariableNull.php b/library/Director/CustomVariable/CustomVariableNull.php index f87ccfa..83e07f0 100644 --- a/library/Director/CustomVariable/CustomVariableNull.php +++ b/library/Director/CustomVariable/CustomVariableNull.php @@ -31,7 +31,7 @@ class CustomVariableNull extends CustomVariable if (! is_null($value)) { throw new ProgrammingError( 'Null can only be null, got %s', - var_export($value, 1) + var_export($value, true) ); } diff --git a/library/Director/CustomVariable/CustomVariableNumber.php b/library/Director/CustomVariable/CustomVariableNumber.php index 62838a9..7b0c3e9 100644 --- a/library/Director/CustomVariable/CustomVariableNumber.php +++ b/library/Director/CustomVariable/CustomVariableNumber.php @@ -47,7 +47,7 @@ class CustomVariableNumber extends CustomVariable if (! is_int($value) && ! is_float($value)) { throw new ProgrammingError( 'Expected a number, got %s', - var_export($value, 1) + var_export($value, true) ); } diff --git a/library/Director/CustomVariable/CustomVariables.php b/library/Director/CustomVariable/CustomVariables.php index cdcc4bd..01227c5 100644 --- a/library/Director/CustomVariable/CustomVariables.php +++ b/library/Director/CustomVariable/CustomVariables.php @@ -414,10 +414,15 @@ class CustomVariables implements Iterator, Countable, IcingaConfigRenderer protected function renderKeyName($key) { + return 'vars' . self::renderKeySuffix($key); + } + + public static function renderKeySuffix($key) + { if (preg_match('/^[a-z][a-z0-9_]*$/i', $key)) { - return 'vars.' . c::escapeIfReserved($key); + return '.' . c::escapeIfReserved($key); } else { - return 'vars[' . c::renderString($key) . ']'; + return '[' . c::renderString($key) . ']'; } } diff --git a/library/Director/Daemon/BackgroundDaemon.php b/library/Director/Daemon/BackgroundDaemon.php index 34cc28b..2d8a29c 100644 --- a/library/Director/Daemon/BackgroundDaemon.php +++ b/library/Director/Daemon/BackgroundDaemon.php @@ -104,7 +104,7 @@ class BackgroundDaemon try { $uuid = \bin2hex(Uuid::uuid4()->getBytes()); } catch (Exception $e) { - $uuid = 'deadc0de' . \substr(\md5(\getmypid()), 0, 24); + $uuid = 'deadc0de' . substr(md5((string) getmypid()), 0, 24); } } $processDetails = new DaemonProcessDetails($uuid); diff --git a/library/Director/Dashboard/BranchesDashboard.php b/library/Director/Dashboard/BranchesDashboard.php index fe8b385..faeb8bf 100644 --- a/library/Director/Dashboard/BranchesDashboard.php +++ b/library/Director/Dashboard/BranchesDashboard.php @@ -4,8 +4,10 @@ namespace Icinga\Module\Director\Dashboard; use gipfl\Web\Widget\Hint; use Icinga\Application\Hook; +use Icinga\Authentication\Auth; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\Db\Branch\PreferredBranchSupport; use Icinga\Module\Director\Hook\BranchSupportHook; use ipl\Html\Html; @@ -19,6 +21,14 @@ class BranchesDashboard extends Dashboard $this->translate('You\'re currently working in a Configuration Branch: %s'), Branch::requireHook()->linkToBranch($branch, $this->getAuth(), $branch->getName()) ))); + } else { + if (($implementation = Branch::optionalHook()) && $implementation instanceof PreferredBranchSupport) { + if ($implementation->hasPreferredBranch(Auth::getInstance())) { + $this->prepend(Hint::warning( + $this->translate('You\'re currently working in the main Configuration Branch') + )); + } + } } return $this->translate('Prepare your configuration in a safe Environment'); diff --git a/library/Director/Dashboard/Dashboard.php b/library/Director/Dashboard/Dashboard.php index de8970c..3f1fc38 100644 --- a/library/Director/Dashboard/Dashboard.php +++ b/library/Director/Dashboard/Dashboard.php @@ -149,6 +149,7 @@ abstract class Dashboard extends HtmlDocument ]); } + #[\ReturnTypeWillChange] public function count() { return count($this->dashlets()); diff --git a/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php b/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php index 9794986..7a9745b 100644 --- a/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php +++ b/library/Director/Dashboard/Dashlet/ActivityLogDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ActivityLogDashlet extends Dashlet { protected $icon = 'book'; @@ -30,6 +32,6 @@ class ActivityLogDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/audit'); + return [Permission::AUDIT]; } } diff --git a/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php b/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php index 419859d..238059a 100644 --- a/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/ApiUserObjectDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ApiUserObjectDashlet extends Dashlet { protected $icon = 'lock-open-alt'; - protected $requiredStats = array('apiuser'); + protected $requiredStats = ['apiuser']; public function getTitle() { @@ -20,6 +22,6 @@ class ApiUserObjectDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/BasketDashlet.php b/library/Director/Dashboard/Dashlet/BasketDashlet.php index 10f2b81..8ac26ed 100644 --- a/library/Director/Dashboard/Dashlet/BasketDashlet.php +++ b/library/Director/Dashboard/Dashlet/BasketDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class BasketDashlet extends Dashlet { protected $icon = 'tag'; @@ -25,6 +27,6 @@ class BasketDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php b/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php index 65d8c8c..458c700 100644 --- a/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php +++ b/library/Director/Dashboard/Dashlet/CheckCommandsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class CheckCommandsDashlet extends Dashlet { protected $icon = 'wrench'; @@ -21,7 +23,7 @@ class CheckCommandsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/ChoicesDashlet.php b/library/Director/Dashboard/Dashlet/ChoicesDashlet.php index efdbba5..105ebb9 100644 --- a/library/Director/Dashboard/Dashlet/ChoicesDashlet.php +++ b/library/Director/Dashboard/Dashlet/ChoicesDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + abstract class ChoicesDashlet extends Dashlet { protected $icon = 'flapping'; @@ -36,6 +38,6 @@ abstract class ChoicesDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php b/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php index 083172e..18a4731 100644 --- a/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/CommandObjectDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class CommandObjectDashlet extends Dashlet { protected $icon = 'wrench'; - protected $requiredStats = array('command'); + protected $requiredStats = ['command']; public function getTitle() { @@ -20,6 +22,6 @@ class CommandObjectDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/CustomvarDashlet.php b/library/Director/Dashboard/Dashlet/CustomvarDashlet.php index 919c06b..a2acd54 100644 --- a/library/Director/Dashboard/Dashlet/CustomvarDashlet.php +++ b/library/Director/Dashboard/Dashlet/CustomvarDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class CustomvarDashlet extends Dashlet { protected $icon = 'keyboard'; @@ -25,6 +27,6 @@ class CustomvarDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php b/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php index 6efb4ca..eb1cefc 100644 --- a/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php +++ b/library/Director/Dashboard/Dashlet/DatafieldCategoryDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class DatafieldCategoryDashlet extends Dashlet { protected $icon = 'th-list'; @@ -25,6 +27,6 @@ class DatafieldCategoryDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/DatafieldDashlet.php b/library/Director/Dashboard/Dashlet/DatafieldDashlet.php index 03f2d8d..a381a3f 100644 --- a/library/Director/Dashboard/Dashlet/DatafieldDashlet.php +++ b/library/Director/Dashboard/Dashlet/DatafieldDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class DatafieldDashlet extends Dashlet { protected $icon = 'edit'; @@ -25,6 +27,6 @@ class DatafieldDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/DatalistDashlet.php b/library/Director/Dashboard/Dashlet/DatalistDashlet.php index bdf179f..fe82e4b 100644 --- a/library/Director/Dashboard/Dashlet/DatalistDashlet.php +++ b/library/Director/Dashboard/Dashlet/DatalistDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class DatalistDashlet extends Dashlet { protected $icon = 'sort-name-up'; @@ -25,6 +27,6 @@ class DatalistDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php b/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php index 47a18aa..b6455fe 100644 --- a/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/DependencyObjectDashlet.php @@ -6,7 +6,7 @@ class DependencyObjectDashlet extends Dashlet { protected $icon = 'sitemap'; - protected $requiredStats = array('dependency'); + protected $requiredStats = ['dependency']; public function getTitle() { diff --git a/library/Director/Dashboard/Dashlet/DeploymentDashlet.php b/library/Director/Dashboard/Dashlet/DeploymentDashlet.php index 7a52793..83b4cea 100644 --- a/library/Director/Dashboard/Dashlet/DeploymentDashlet.php +++ b/library/Director/Dashboard/Dashlet/DeploymentDashlet.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use Exception; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Objects\DirectorDeploymentLog; class DeploymentDashlet extends Dashlet @@ -109,6 +110,6 @@ class DeploymentDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/deploy'); + return [Permission::DEPLOY]; } } diff --git a/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php b/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php index 9dd9467..97ae746 100644 --- a/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/EndpointObjectDashlet.php @@ -3,12 +3,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use Exception; +use Icinga\Module\Director\Auth\Permission; class EndpointObjectDashlet extends Dashlet { protected $icon = 'cloud'; - protected $requiredStats = array('endpoint'); + protected $requiredStats = ['endpoint']; protected $hasDeploymentEndpoint; @@ -24,7 +25,7 @@ class EndpointObjectDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } protected function hasDeploymentEndpoint() diff --git a/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php b/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php index 5d3b25f..249d4d6 100644 --- a/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php +++ b/library/Director/Dashboard/Dashlet/HostGroupsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class HostGroupsDashlet extends Dashlet { protected $icon = 'tags'; @@ -26,6 +28,6 @@ class HostGroupsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/hostgroups'); + return [Permission::HOST_GROUPS]; } } diff --git a/library/Director/Dashboard/Dashlet/HostObjectDashlet.php b/library/Director/Dashboard/Dashlet/HostObjectDashlet.php index 10cff94..e77f75b 100644 --- a/library/Director/Dashboard/Dashlet/HostObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/HostObjectDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class HostObjectDashlet extends Dashlet { protected $icon = 'host'; - protected $requiredStats = array('host', 'hostgroup'); + protected $requiredStats = ['host', 'hostgroup']; public function getTitle() { @@ -15,7 +17,7 @@ class HostObjectDashlet extends Dashlet public function listRequiredPermissions() { - return ['director/hosts']; + return [Permission::HOSTS]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php b/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php index 09bed17..eb4092a 100644 --- a/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php +++ b/library/Director/Dashboard/Dashlet/HostTemplatesDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class HostTemplatesDashlet extends Dashlet { protected $icon = 'cubes'; @@ -26,6 +28,6 @@ class HostTemplatesDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/HostsDashlet.php b/library/Director/Dashboard/Dashlet/HostsDashlet.php index 39c1421..55bebbd 100644 --- a/library/Director/Dashboard/Dashlet/HostsDashlet.php +++ b/library/Director/Dashboard/Dashlet/HostsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class HostsDashlet extends Dashlet { protected $icon = 'host'; @@ -27,6 +29,6 @@ class HostsDashlet extends Dashlet public function listRequiredPermissions() { - return ['director/hosts']; + return [Permission::HOSTS]; } } diff --git a/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php b/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php index 302c1ed..aa34613 100644 --- a/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php +++ b/library/Director/Dashboard/Dashlet/ImportSourceDashlet.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use Exception; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Objects\ImportSource; class ImportSourceDashlet extends Dashlet @@ -60,6 +61,6 @@ class ImportSourceDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php b/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php index 328df72..e1d5908 100644 --- a/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php +++ b/library/Director/Dashboard/Dashlet/InfrastructureDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class InfrastructureDashlet extends Dashlet { protected $icon = 'cloud'; @@ -25,6 +27,6 @@ class InfrastructureDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/JobDashlet.php b/library/Director/Dashboard/Dashlet/JobDashlet.php index d7452e0..af5429d 100644 --- a/library/Director/Dashboard/Dashlet/JobDashlet.php +++ b/library/Director/Dashboard/Dashlet/JobDashlet.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use Exception; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Objects\DirectorJob; class JobDashlet extends Dashlet @@ -60,6 +61,6 @@ class JobDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/KickstartDashlet.php b/library/Director/Dashboard/Dashlet/KickstartDashlet.php index 09801f5..eb74371 100644 --- a/library/Director/Dashboard/Dashlet/KickstartDashlet.php +++ b/library/Director/Dashboard/Dashlet/KickstartDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class KickstartDashlet extends Dashlet { protected $icon = 'gauge'; @@ -26,6 +28,6 @@ class KickstartDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php b/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php index e0b0443..d0dbb01 100644 --- a/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php +++ b/library/Director/Dashboard/Dashlet/NotificationApplyDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class NotificationApplyDashlet extends Dashlet { protected $icon = 'bell'; @@ -27,7 +29,7 @@ class NotificationApplyDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/notifications'); + return [Permission::NOTIFICATIONS]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php b/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php index a58b5d0..6f1fe64 100644 --- a/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php +++ b/library/Director/Dashboard/Dashlet/NotificationTemplateDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class NotificationTemplateDashlet extends Dashlet { protected $icon = 'cubes'; - protected $requiredStats = array('notification'); + protected $requiredStats = ['notification']; public function getTitle() { @@ -21,7 +23,7 @@ class NotificationTemplateDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/NotificationsDashlet.php b/library/Director/Dashboard/Dashlet/NotificationsDashlet.php index 85610f0..a0b1e43 100644 --- a/library/Director/Dashboard/Dashlet/NotificationsDashlet.php +++ b/library/Director/Dashboard/Dashlet/NotificationsDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class NotificationsDashlet extends Dashlet { protected $icon = 'bell'; - protected $requiredStats = array('notification'); + protected $requiredStats = ['notification']; public function getTitle() { @@ -23,7 +25,7 @@ class NotificationsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/notifications'); + return [Permission::NOTIFICATIONS]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php b/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php index 45bcfa2..c9fbb68 100644 --- a/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php +++ b/library/Director/Dashboard/Dashlet/ScheduledDowntimeApplyDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ScheduledDowntimeApplyDashlet extends Dashlet { protected $icon = 'plug'; @@ -15,7 +17,7 @@ class ScheduledDowntimeApplyDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/scheduled-downtimes'); + return [Permission::SCHEDULED_DOWNTIMES]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php b/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php index 32b1cfa..b3d15fc 100644 --- a/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php +++ b/library/Director/Dashboard/Dashlet/SelfServiceDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class SelfServiceDashlet extends Dashlet { protected $icon = 'chat'; @@ -26,6 +28,6 @@ class SelfServiceDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php b/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php index b4bee04..487be02 100644 --- a/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php +++ b/library/Director/Dashboard/Dashlet/ServiceApplyRulesDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ServiceApplyRulesDashlet extends Dashlet { protected $icon = 'resize-full-alt'; @@ -26,6 +28,6 @@ class ServiceApplyRulesDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php b/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php index ad47768..44162a9 100644 --- a/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php +++ b/library/Director/Dashboard/Dashlet/ServiceGroupsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ServiceGroupsDashlet extends Dashlet { protected $icon = 'tags'; @@ -26,6 +28,6 @@ class ServiceGroupsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php b/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php index 01fb800..087590b 100644 --- a/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/ServiceObjectDashlet.php @@ -3,6 +3,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use Icinga\Module\Director\Acl; +use Icinga\Module\Director\Auth\Permission; +use RuntimeException; class ServiceObjectDashlet extends Dashlet { @@ -22,13 +24,13 @@ class ServiceObjectDashlet extends Dashlet public function listRequiredPermissions() { - return ['director/services']; + throw new RuntimeException('This method should not be accessed, isAllowed() has been implemented'); } public function isAllowed() { $acl = Acl::instance(); - return $acl->hasPermission('director/services') - || $acl->hasPermission('director/service_sets'); + return $acl->hasPermission(Permission::SERVICES) + || $acl->hasPermission(Permission::SERVICE_SETS); } } diff --git a/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php b/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php index f971d42..c8db0e9 100644 --- a/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php +++ b/library/Director/Dashboard/Dashlet/ServiceSetsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ServiceSetsDashlet extends Dashlet { protected $icon = 'services'; @@ -26,6 +28,6 @@ class ServiceSetsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/servicesets'); + return [Permission::SERVICE_SETS]; } } diff --git a/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php b/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php index 62d1b41..c2131d6 100644 --- a/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php +++ b/library/Director/Dashboard/Dashlet/ServiceTemplatesDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ServiceTemplatesDashlet extends Dashlet { protected $icon = 'cubes'; @@ -26,6 +28,6 @@ class ServiceTemplatesDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/SettingsDashlet.php b/library/Director/Dashboard/Dashlet/SettingsDashlet.php index 716e565..0a3d680 100644 --- a/library/Director/Dashboard/Dashlet/SettingsDashlet.php +++ b/library/Director/Dashboard/Dashlet/SettingsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class SettingsDashlet extends Dashlet { protected $icon = 'edit'; @@ -25,6 +27,6 @@ class SettingsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php b/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php index 297b3f8..a7d648a 100644 --- a/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php +++ b/library/Director/Dashboard/Dashlet/SingleServicesDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class SingleServicesDashlet extends Dashlet { protected $icon = 'service'; @@ -26,6 +28,6 @@ class SingleServicesDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/services'); + return [Permission::SERVICES]; } } diff --git a/library/Director/Dashboard/Dashlet/SyncDashlet.php b/library/Director/Dashboard/Dashlet/SyncDashlet.php index 4ac689a..d212bc2 100644 --- a/library/Director/Dashboard/Dashlet/SyncDashlet.php +++ b/library/Director/Dashboard/Dashlet/SyncDashlet.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use Exception; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Objects\SyncRule; class SyncDashlet extends Dashlet @@ -60,6 +61,6 @@ class SyncDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php b/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php index ba4c1db..2aa4c9b 100644 --- a/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/TimeperiodObjectDashlet.php @@ -4,12 +4,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; use DirectoryIterator; use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Auth\Permission; class TimeperiodObjectDashlet extends Dashlet { protected $icon = 'calendar'; - protected $requiredStats = array('timeperiod'); + protected $requiredStats = ['timeperiod']; public function getTitle() { @@ -23,6 +24,6 @@ class TimeperiodObjectDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php b/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php index 26339e4..7aa3201 100644 --- a/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php +++ b/library/Director/Dashboard/Dashlet/TimeperiodTemplateDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class TimeperiodTemplateDashlet extends Dashlet { protected $icon = 'cubes'; - protected $requiredStats = array('timeperiod'); + protected $requiredStats = ['timeperiod']; public function getTitle() { @@ -21,7 +23,7 @@ class TimeperiodTemplateDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php b/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php index 5a54bec..827cc12 100644 --- a/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php +++ b/library/Director/Dashboard/Dashlet/TimeperiodsDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class TimeperiodsDashlet extends Dashlet { protected $icon = 'calendar'; - protected $requiredStats = array('timeperiod'); + protected $requiredStats = ['timeperiod']; public function getTitle() { @@ -20,6 +22,6 @@ class TimeperiodsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php b/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php index 3fba4ba..792e140 100644 --- a/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php +++ b/library/Director/Dashboard/Dashlet/UserGroupsDashlet.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class UserGroupsDashlet extends Dashlet { protected $icon = 'tags'; @@ -26,6 +28,6 @@ class UserGroupsDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Dashboard/Dashlet/UserObjectDashlet.php b/library/Director/Dashboard/Dashlet/UserObjectDashlet.php index 463b84c..7e4f511 100644 --- a/library/Director/Dashboard/Dashlet/UserObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/UserObjectDashlet.php @@ -2,14 +2,11 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; -use DirectoryIterator; -use Icinga\Exception\ProgrammingError; - class UserObjectDashlet extends Dashlet { protected $icon = 'users'; - protected $requiredStats = array('user', 'usergroup'); + protected $requiredStats = ['user', 'usergroup']; public function getTitle() { diff --git a/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php b/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php index 291ab05..c00215d 100644 --- a/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php +++ b/library/Director/Dashboard/Dashlet/UserTemplateDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class UserTemplateDashlet extends Dashlet { protected $icon = 'cubes'; - protected $requiredStats = array('user'); + protected $requiredStats = ['user']; public function getTitle() { @@ -21,7 +23,7 @@ class UserTemplateDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/UsersDashlet.php b/library/Director/Dashboard/Dashlet/UsersDashlet.php index 43ddc26..4f0c7d7 100644 --- a/library/Director/Dashboard/Dashlet/UsersDashlet.php +++ b/library/Director/Dashboard/Dashlet/UsersDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class UsersDashlet extends Dashlet { protected $icon = 'users'; - protected $requiredStats = array('user', 'usergroup'); + protected $requiredStats = ['user', 'usergroup']; public function getTitle() { @@ -15,7 +17,7 @@ class UsersDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/users'); + return [Permission::USERS]; } public function getUrl() diff --git a/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php b/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php index ee789f2..f2ff8c8 100644 --- a/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php +++ b/library/Director/Dashboard/Dashlet/ZoneObjectDashlet.php @@ -2,11 +2,13 @@ namespace Icinga\Module\Director\Dashboard\Dashlet; +use Icinga\Module\Director\Auth\Permission; + class ZoneObjectDashlet extends Dashlet { protected $icon = 'globe'; - protected $requiredStats = array('zone'); + protected $requiredStats = ['zone']; public function getTitle() { @@ -20,6 +22,6 @@ class ZoneObjectDashlet extends Dashlet public function listRequiredPermissions() { - return array('director/admin'); + return [Permission::ADMIN]; } } diff --git a/library/Director/Data/AssignFilterHelper.php b/library/Director/Data/AssignFilterHelper.php index b0253cf..7448c51 100644 --- a/library/Director/Data/AssignFilterHelper.php +++ b/library/Director/Data/AssignFilterHelper.php @@ -135,7 +135,7 @@ class AssignFilterHelper $parts = array(); foreach (preg_split('~\*~', $expression) as $part) { - $parts[] = preg_quote($part); + $parts[] = preg_quote($part, '/'); } // match() is case insensitive $pattern = '/^' . implode('.*', $parts) . '$/i'; diff --git a/library/Director/Data/Db/DbDataFormatter.php b/library/Director/Data/Db/DbDataFormatter.php index d6e4eeb..91fc776 100644 --- a/library/Director/Data/Db/DbDataFormatter.php +++ b/library/Director/Data/Db/DbDataFormatter.php @@ -6,7 +6,7 @@ use InvalidArgumentException; class DbDataFormatter { - public static function normalizeBoolean($value) + public static function normalizeBoolean($value): ?string { if ($value === 'y' || $value === '1' || $value === true || $value === 1) { return 'y'; @@ -20,7 +20,19 @@ class DbDataFormatter throw new InvalidArgumentException(sprintf( 'Got invalid boolean: %s', - var_export($value, 1) + var_export($value, true) )); } + + public static function booleanForDbValue($value): ?bool + { + if ($value === 'y') { + return true; + } + if ($value === 'n') { + return false; + } + + return $value; // let this fail elsewhere, if not null + } } diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php index 6ecae8b..114b61b 100644 --- a/library/Director/Data/Db/DbObject.php +++ b/library/Director/Data/Db/DbObject.php @@ -80,6 +80,9 @@ abstract class DbObject protected $binaryProperties = []; + /* key/value!! */ + protected $booleans = []; + /** * Filled with object instances when prefetchAll is used */ @@ -346,6 +349,16 @@ abstract class DbObject return $this->$func($value); } + if ($this->getUuidColumn() === $key) { + if (strlen($value) > 16) { + $value = Uuid::fromString($value)->getBytes(); + } + } + + if ($this->propertyIsBoolean($key)) { + $value = DbDataFormatter::normalizeBoolean($value); + } + if (! $this->hasProperty($key)) { throw new InvalidArgumentException(sprintf( 'Trying to set invalid key "%s"', @@ -372,7 +385,10 @@ abstract class DbObject return $this; } if ($key === 'id' || substr($key, -3) === '_id') { - if ((int) $value === (int) $this->properties[$key]) { + if ($value !== null + && $this->properties[$key] !== null + && (int) $value === (int) $this->properties[$key] + ) { return $this; } } @@ -553,7 +569,7 @@ abstract class DbObject /** * Unique key name * - * @return string + * @return string|array */ public function getKeyName() { @@ -706,8 +722,7 @@ abstract class DbObject */ protected function loadFromDb() { - $select = $this->db->select()->from($this->table)->where($this->createWhere()); - $properties = $this->db->fetchRow($select); + $properties = $this->db->fetchRow($this->prepareObjectQuery()); if (empty($properties)) { if (is_array($this->getKeyName())) { @@ -728,6 +743,11 @@ abstract class DbObject return $this->setDbProperties($properties); } + public function prepareObjectQuery() + { + return $this->db->select()->from($this->table)->where($this->createWhere()); + } + /** * @param object|array $row * @param Db $db @@ -878,6 +898,11 @@ abstract class DbObject return in_array($column, $this->binaryProperties) || $this->getUuidColumn() === $column; } + public function propertyIsBoolean($property) + { + return array_key_exists($property, $this->booleans); + } + /** * Store object to database * @@ -959,7 +984,7 @@ abstract class DbObject $this->table, $this->getLogId(), $e->getMessage(), - var_export($this->getProperties(), 1) // TODO: Remove properties + var_export($this->getProperties(), true) // TODO: Remove properties )); } @@ -1027,7 +1052,7 @@ abstract class DbObject if ($this->hasUuidColumn() && $this->properties[$this->uuidColumn] !== null) { return $this->db->quoteInto( sprintf('%s = ?', $this->getUuidColumn()), - $this->connection->quoteBinary($this->getUniqueId()->getBytes()) + $this->connection->quoteBinary($this->getOriginalProperty($this->uuidColumn)) ); } if ($id = $this->getAutoincId()) { @@ -1302,6 +1327,40 @@ abstract class DbObject } /** + * @param $id + * @param DbConnection $connection + * @return static + */ + public static function loadOptional($id, DbConnection $connection): ?DbObject + { + if ($prefetched = static::getPrefetched($id)) { + return $prefetched; + } + /** @var DbObject $obj */ + $obj = new static(); + + if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { + $table = $obj->getTableName(); + assert($connection instanceof Db); + $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); + if ($uuid) { + return self::$dbObjectStore->load($table, $uuid); + } + + return null; + } + + $obj->setConnection($connection)->setKey($id); + $properties = $connection->getDbAdapter()->fetchRow($obj->prepareObjectQuery()); + if (empty($properties)) { + return null; + } + + $obj->setDbProperties($properties); + return $obj; + } + + /** * @param DbConnection $connection * @param \Zend_Db_Select $query * @param string|null $keyColumn @@ -1436,7 +1495,7 @@ abstract class DbObject )); } - public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection) + public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection): ?DbObject { $db = $connection->getDbAdapter(); $obj = new static; diff --git a/library/Director/Data/Db/ServiceSetQueryBuilder.php b/library/Director/Data/Db/ServiceSetQueryBuilder.php index 7841d1e..597fe0e 100644 --- a/library/Director/Data/Db/ServiceSetQueryBuilder.php +++ b/library/Director/Data/Db/ServiceSetQueryBuilder.php @@ -27,6 +27,8 @@ class ServiceSetQueryBuilder /** @var \Zend_Db_Adapter_Abstract */ protected $db; + protected $searchColumns = []; + /** * @param ?UuidInterface $uuid */ diff --git a/library/Director/Data/Exporter.php b/library/Director/Data/Exporter.php index a2e3191..1a3cfcb 100644 --- a/library/Director/Data/Exporter.php +++ b/library/Director/Data/Exporter.php @@ -2,10 +2,14 @@ namespace Icinga\Module\Director\Data; +use gipfl\Json\JsonString; use gipfl\ZfDb\Adapter\Adapter; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Data\Db\DbDataFormatter; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Data\Db\DbObjectWithSettings; use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\Basket; use Icinga\Module\Director\Objects\DirectorDatafield; use Icinga\Module\Director\Objects\DirectorDatalist; use Icinga\Module\Director\Objects\DirectorDatalistEntry; @@ -18,6 +22,7 @@ use Icinga\Module\Director\Objects\IcingaTemplateChoice; use Icinga\Module\Director\Objects\ImportSource; use Icinga\Module\Director\Objects\InstantiatedViaHook; use Icinga\Module\Director\Objects\SyncRule; +use Ramsey\Uuid\Uuid; use RuntimeException; class Exporter @@ -68,6 +73,11 @@ class Exporter $props = $chosen; } + if ($column = $object->getUuidColumn()) { + if ($uuid = $object->get($column)) { + $props[$column] = Uuid::fromBytes($uuid)->toString(); + } + } ksort($props); return (object) $props; @@ -152,10 +162,10 @@ class Exporter throw new RuntimeException('Not yet'); } $props['services'] = []; - foreach ($object->getServiceObjects() as $serviceObject) { - $props['services'][$serviceObject->getObjectName()] = $this->export($serviceObject); + foreach ($object->getServices() as $serviceObject) { + $props['services'][] = $this->export($serviceObject); } - ksort($props['services']); + usort($props['services'], [$this, 'sortByName']); } elseif ($object instanceof IcingaHost) { if ($this->exportHostServices) { $services = []; @@ -168,10 +178,15 @@ class Exporter } } + protected function sortByName($left, $right) + { + return $left->object_name < $right->object_name ? '-1' : '1'; + } + public function serviceLoader() { if ($this->serviceLoader === null) { - $this->serviceLoader = new HostServiceLoader($this->connection); + $this->serviceLoader = new HostServiceLoader($this->connection, Auth::getInstance()); $this->serviceLoader->resolveObjects($this->resolveObjects); } @@ -241,6 +256,12 @@ class Exporter protected function exportDbObject(DbObject $object) { $props = $object->getProperties(); + foreach ($props as $key => &$value) { + if ($object->propertyIsBoolean($key)) { + $value = DbDataFormatter::booleanForDbValue($value); + } + } + unset($value); if ($object instanceof DbObjectWithSettings) { if ($object instanceof InstantiatedViaHook) { $props['settings'] = (object) $object->getInstance()->exportSettings(); @@ -248,6 +269,11 @@ class Exporter $props['settings'] = (object) $object->getSettings(); // Already sorted } } + if ($object instanceof Basket) { + if (isset($props['objects']) && is_string($props['objects'])) { + $props['objects'] = JsonString::decode($props['objects']); + } + } unset($props['uuid']); // Not yet if (! $this->showDefaults) { foreach ($props as $key => $value) { @@ -279,16 +305,7 @@ class Exporter protected function exportDatalistEntries(DirectorDatalist $list) { $entries = []; - $id = $list->get('id'); - if ($id === null) { - return $entries; - } - - $dbEntries = DirectorDatalistEntry::loadAllForList($list); - // Hint: they are loaded with entry_name key - ksort($dbEntries); - - foreach ($dbEntries as $entry) { + foreach ($list->getEntries() as $name => $entry) { if ($entry->shouldBeRemoved()) { continue; } diff --git a/library/Director/Data/FieldReferenceLoader.php b/library/Director/Data/FieldReferenceLoader.php index 1e3d92e..99a9925 100644 --- a/library/Director/Data/FieldReferenceLoader.php +++ b/library/Director/Data/FieldReferenceLoader.php @@ -29,12 +29,12 @@ class FieldReferenceLoader } $type = $object->getShortTableName(); $res = $db->fetchAll( - $db->select()->from(['f' => "icinga_${type}_field"], [ + $db->select()->from(['f' => "icinga_{$type}_field"], [ 'f.datafield_id', 'f.is_required', 'f.var_filter', ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', []) - ->where("${type}_id = ?", (int) $id) + ->where("{$type}_id = ?", (int) $id) ->order('varname ASC') ); diff --git a/library/Director/Data/HostServiceLoader.php b/library/Director/Data/HostServiceLoader.php index 4cc4b96..c8bd8b9 100644 --- a/library/Director/Data/HostServiceLoader.php +++ b/library/Director/Data/HostServiceLoader.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Director\Data; use gipfl\IcingaWeb2\Table\QueryBasedTable; use gipfl\ZfDb\Select; +use Icinga\Authentication\Auth; use Icinga\Data\SimpleQuery; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\AppliedServiceSetLoader; @@ -26,21 +27,26 @@ class HostServiceLoader /** @var \Zend_Db_Adapter_Abstract */ protected $db; + /** @var Auth */ + protected $auth; + /** @var bool */ protected $resolveHostServices = false; /** @var bool */ protected $resolveObjects = false; - public function __construct(Db $connection) + public function __construct(Db $connection, Auth $auth) { $this->connection = $connection; $this->db = $connection->getDbAdapter(); + $this->auth = $auth; } public function fetchServicesForHost(IcingaHost $host) { - $table = (new ObjectsTableService($this->connection))->setHost($host); + $table = (new ObjectsTableService($this->connection, $this->auth)) + ->setHost($host); $services = $this->fetchServicesForTable($table); if ($this->resolveHostServices) { foreach ($this->fetchAllServicesForHost($host) as $service) { @@ -69,7 +75,7 @@ class HostServiceLoader /** @var IcingaHost[] $parents */ $parents = IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true); foreach ($parents as $parent) { - $table = (new ObjectsTableService($this->connection)) + $table = (new ObjectsTableService($this->connection, $this->auth)) ->setHost($parent) ->setInheritedBy($host); foreach ($this->fetchServicesForTable($table) as $service) { diff --git a/library/Director/Data/ObjectImporter.php b/library/Director/Data/ObjectImporter.php new file mode 100644 index 0000000..231ad1c --- /dev/null +++ b/library/Director/Data/ObjectImporter.php @@ -0,0 +1,168 @@ +<?php + +namespace Icinga\Module\Director\Data; + +use gipfl\Json\JsonDecodeException; +use gipfl\Json\JsonString; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\Basket; +use Icinga\Module\Director\Objects\DirectorJob; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\Objects\SyncRule; +use InvalidArgumentException; +use Ramsey\Uuid\Uuid; +use stdClass; + +class ObjectImporter +{ + protected static $templatesOnly = [ + IcingaHost::class, + IcingaService::class, + IcingaServiceSet::class, + ]; + + /** @var Db */ + protected $db; + + public function __construct(Db $db) + { + $this->db = $db; + } + + /** + * @param class-string|DbObject $implementation + * @param stdClass $plain + * @return DbObject + * @throws JsonDecodeException + */ + public function import(string $implementation, stdClass $plain): DbObject + { + $this->assertTemplate($implementation, $plain); + $this->fixRelations($implementation, $plain); + $this->applyOtherWorkarounds($implementation, $plain); + $this->fixLegacyBaskets($implementation, $plain); + $this->fixSubObjects($implementation, $plain); + + $object = $this->loadExistingObject($implementation, $plain); + if ($object === null) { + $object = $implementation::create([], $this->db); + } + + $properties = (array) $plain; + unset($properties['fields']); + unset($properties['originalId']); + if ($implementation === Basket::class) { + if (isset($properties['objects']) && is_string($properties['objects'])) { + $properties['objects'] = JsonString::decode($properties['objects']); + } + } + $object->setProperties($properties); + + return $object; + } + + protected function fixLegacyBaskets(string $implementation, stdClass $plain) + { + // TODO: Check, whether current export sets modifiers = [] in case there is none + if ($implementation == ImportSource::class) { + if (!isset($plain->modifiers)) { + $plain->modifiers = []; + } + } + } + + protected function applyOtherWorkarounds(string $implementation, stdClass $plain) + { + if ($implementation === SyncRule::class) { + if (isset($plain->properties)) { + $plain->syncProperties = $plain->properties; + unset($plain->properties); + } + } + } + + protected function fixSubObjects(string $implementation, stdClass $plain) + { + if ($implementation === IcingaServiceSet::class) { + foreach ($plain->services as $service) { + unset($service->fields); + } + // Hint: legacy baskets are carrying service names as object keys, new baskets have arrays + $plain->services = array_values((array) $plain->services); + } + } + + protected function fixRelations(string $implementation, stdClass $plain) + { + if ($implementation === DirectorJob::class) { + $settings = $plain->settings; + $source = $settings->source ?? null; + if ($source && !isset($settings->source_id)) { + $settings->source_id = ImportSource::load($source, $this->db)->get('id'); + unset($settings->source); + } + $rule = $settings->rule ?? null; + if ($rule && !isset($settings->rule_id)) { + $settings->rule_id = SyncRule::load($rule, $this->db)->get('id'); + unset($settings->rule); + } + } + } + + /** + * @param class-string<DbObject> $implementation + * @param stdClass $plain + * @return DbObject|null + */ + protected function loadExistingObject(string $implementation, stdClass $plain): ?DbObject + { + if (isset($plain->uuid) + && $instance = $implementation::loadWithUniqueId(Uuid::fromString($plain->uuid), $this->db) + ) { + return $instance; + } + + if ($implementation === IcingaService::class) { + $key = [ + 'object_type' => 'template', + 'object_name' => $plain->object_name + ]; + } else { + $dummy = $implementation::create(); + $keyColumn = $dummy->getKeyName(); + if (is_array($keyColumn)) { + if (empty($keyColumn)) { + throw new \RuntimeException("$implementation has an empty keyColumn array"); + } + $key = []; + foreach ($keyColumn as $column) { + if (isset($plain->$column)) { + $key[$column] = $plain->$column; + } + } + } else { + $key = $plain->$keyColumn; + } + } + + return $implementation::loadOptional($key, $this->db); + } + + protected function assertTemplate(string $implementation, stdClass $plain) + { + if (! in_array($implementation, self::$templatesOnly)) { + return; + } + if ($plain->object_type !== 'template') { + throw new InvalidArgumentException(sprintf( + 'Can import only Templates, got "%s" for "%s"', + $plain->object_type, + $plain->name + )); + } + } +} diff --git a/library/Director/Data/PropertyMangler.php b/library/Director/Data/PropertyMangler.php index a457f1d..40b2570 100644 --- a/library/Director/Data/PropertyMangler.php +++ b/library/Director/Data/PropertyMangler.php @@ -19,7 +19,7 @@ class PropertyMangler throw new InvalidArgumentException(sprintf( 'I can only append to arrays, %s is %s', $key, - var_export($current, 1) + var_export($current, true) )); } @@ -52,7 +52,7 @@ class PropertyMangler throw new InvalidArgumentException(sprintf( 'I can only remove strings or from arrays, %s is %s', $key, - var_export($current, 1) + var_export($current, true) )); } } diff --git a/library/Director/Db/Branch/Branch.php b/library/Director/Db/Branch/Branch.php index cd68ff0..c99b1bd 100644 --- a/library/Director/Db/Branch/Branch.php +++ b/library/Director/Db/Branch/Branch.php @@ -2,11 +2,11 @@ namespace Icinga\Module\Director\Db\Branch; +use Icinga\Application\Hook; use Icinga\Application\Icinga; use Icinga\Authentication\Auth; use Icinga\Module\Director\Db; use Icinga\Module\Director\Hook\BranchSupportHook; -use Icinga\Web\Hook; use Icinga\Web\Request; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; @@ -45,7 +45,7 @@ class Branch $row->uuid = stream_get_contents($row->uuid); } if (strlen($row->uuid) !== 16) { - throw new RuntimeException('Valid UUID expected, got ' . var_export($row->uuid, 1)); + throw new RuntimeException('Valid UUID expected, got ' . var_export($row->uuid, true)); } $self->branchUuid = Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid)); $self->name = $row->branch_name; diff --git a/library/Director/Db/Branch/BranchActivity.php b/library/Director/Db/Branch/BranchActivity.php index 3812e75..e95ac7d 100644 --- a/library/Director/Db/Branch/BranchActivity.php +++ b/library/Director/Db/Branch/BranchActivity.php @@ -294,7 +294,13 @@ class BranchActivity */ public function getObjectName() { - return $this->getProperty('object_name', 'unknown object name'); + if ($this->objectTable === BranchSupport::TABLE_ICINGA_SERVICE && $host = $this->getProperty('host')) { + $suffix = " ($host)"; + } else { + $suffix = ''; + } + + return $this->getProperty('object_name', 'unknown object name') . $suffix; } /** diff --git a/library/Director/Db/Branch/BranchStore.php b/library/Director/Db/Branch/BranchStore.php index 196d079..13971b9 100644 --- a/library/Director/Db/Branch/BranchStore.php +++ b/library/Director/Db/Branch/BranchStore.php @@ -56,6 +56,7 @@ class BranchStore $rows = $db->fetchAll($db->select()->from($table)->where('branch_uuid = ?', $oldQuotedUuid)); foreach ($rows as $row) { $modified = (array)$row; + $this->quoteBinaryProperties($modified); $modified['branch_uuid'] = $quotedUuid; if ($table === self::TABLE_ACTIVITY) { $modified['timestamp_ns'] = round($modified['timestamp_ns'] / 1000000); @@ -68,6 +69,21 @@ class BranchStore return $this->fetchBranchByName($newName); } + protected function quoteBinaryProperties(&$properties) + { + foreach ($properties as $key => $value) { + if ($this->isBinaryColumn($key)) { + $properties[$key] = $this->connection->quoteBinary($value); + } + } + } + + protected function isBinaryColumn($key) + { + return (strpos($key, 'uuid') !== false || strpos($key, 'checksum') !== false) + && strpos($key, 'hex') === false; + } + protected function runTransaction($callback) { $db = $this->db; @@ -100,7 +116,6 @@ class BranchStore } } }); - } protected function newFromDbResult($query) diff --git a/library/Director/Db/Branch/PreferredBranchSupport.php b/library/Director/Db/Branch/PreferredBranchSupport.php new file mode 100644 index 0000000..3463bfe --- /dev/null +++ b/library/Director/Db/Branch/PreferredBranchSupport.php @@ -0,0 +1,10 @@ +<?php + +namespace Icinga\Module\Director\Db\Branch; + +use Icinga\Authentication\Auth; + +interface PreferredBranchSupport +{ + public function hasPreferredBranch(Auth $auth): bool; +} diff --git a/library/Director/Db/Branch/UuidLookup.php b/library/Director/Db/Branch/UuidLookup.php index b340e07..4db7866 100644 --- a/library/Director/Db/Branch/UuidLookup.php +++ b/library/Director/Db/Branch/UuidLookup.php @@ -15,18 +15,13 @@ use function is_string; class UuidLookup { /** - * @param Db $connection - * @param Branch $branch - * @param string $objectType * @param int|string $key - * @param IcingaHost|null $host - * @param IcingaServiceSet $set * @return ?UuidInterface */ public static function findServiceUuid( Db $connection, Branch $branch, - $objectType = null, + ?string $objectType = null, $key = null, IcingaHost $host = null, IcingaServiceSet $set = null @@ -37,11 +32,20 @@ class UuidLookup $query->where('object_type = ?', $objectType); } $query = self::addKeyToQuery($connection, $query, $key); - if ($host) { - $query->where('host_id = ?', $host->get('id')); - } if ($set) { - $query->where('service_set_id = ?', $set->get('id')); + $setId = $set->get('id'); + if ($setId === null) { + $query->where('1 = 0'); + } else { + $query->where('service_set_id = ?', $setId); + } + } elseif ($host) { + $hostId = $host->get('id'); + if ($hostId === null) { + $query->where('1 = 0'); + } else { + $query->where('host_id = ?', $hostId); + } } $uuid = self::fetchOptionalUuid($connection, $query); @@ -100,7 +104,7 @@ class UuidLookup $uuid = self::fetchOptionalUuid($connection, $query); if ($uuid === null && $branch->isBranch()) { if (is_array($key) && isset($key['host_id'])) { - $key['host'] = IcingaHost::load($key['host_id'], $connection)->getObjectName(); + $key['host'] = IcingaHost::loadWithAutoIncId((int) $key['host_id'], $connection)->getObjectName(); unset($key['host_id']); } $query = self::addKeyToQuery($connection, $db->select()->from("branched_$table", 'uuid'), $key); diff --git a/library/Director/Db/Cache/CustomVariableCache.php b/library/Director/Db/Cache/CustomVariableCache.php index 243ecae..ee2b9ef 100644 --- a/library/Director/Db/Cache/CustomVariableCache.php +++ b/library/Director/Db/Cache/CustomVariableCache.php @@ -76,9 +76,4 @@ class CustomVariableCache return new CustomVariables(); } } - - public function __destruct() - { - unset($this->db); - } } diff --git a/library/Director/Db/IcingaObjectFilterHelper.php b/library/Director/Db/IcingaObjectFilterHelper.php index 2eef406..2d9f8f3 100644 --- a/library/Director/Db/IcingaObjectFilterHelper.php +++ b/library/Director/Db/IcingaObjectFilterHelper.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Resolver\TemplateTree; use InvalidArgumentException; +use Ramsey\Uuid\UuidInterface; use RuntimeException; use Zend_Db_Select as ZfSelect; @@ -30,7 +31,7 @@ class IcingaObjectFilterHelper throw new InvalidArgumentException(sprintf( 'Numeric ID or IcingaObject expected, got %s', // TODO: just type/class info? - var_export($id, 1) + var_export($id, true) )); } } @@ -46,20 +47,49 @@ class IcingaObjectFilterHelper ZfSelect $query, $template, $tableAlias = 'o', - $inheritanceType = self::INHERIT_DIRECT + $inheritanceType = self::INHERIT_DIRECT, + UuidInterface $branchuuid = null ) { $i = $tableAlias . 'i'; $o = $tableAlias; $type = $template->getShortTableName(); $db = $template->getDb(); $id = static::wantId($template); + + if ($branchuuid) { + if ($inheritanceType === self::INHERIT_DIRECT) { + return $query->where('imports LIKE \'%"' . $template->getObjectName() . '"%\''); + } elseif ($inheritanceType === self::INHERIT_INDIRECT + || $inheritanceType === self::INHERIT_DIRECT_OR_INDIRECT + ) { + $tree = new TemplateTree($type, $template->getConnection()); + $templateNames = $tree->getDescendantsFor($template); + + if ($inheritanceType === self::INHERIT_DIRECT_OR_INDIRECT) { + $templateNames[] = $template->getObjectName(); + } + + if (empty($templateNames)) { + $condition = '(1 = 0)'; + } else { + $condition = 'imports LIKE \'%"' . array_pop($templateNames) . '"%\''; + + foreach ($templateNames as $templateName) { + $condition .= " OR imports LIKE '%\"$templateName\"%'"; + } + } + + return $query->where($condition); + } + } + $sub = $db->select()->from( - array($i => "icinga_${type}_inheritance"), + array($i => "icinga_{$type}_inheritance"), array('e' => '(1)') - )->where("$i.${type}_id = $o.id"); + )->where("$i.{$type}_id = $o.id"); if ($inheritanceType === self::INHERIT_DIRECT) { - $sub->where("$i.parent_${type}_id = ?", $id); + $sub->where("$i.parent_{$type}_id = ?", $id); } elseif ($inheritanceType === self::INHERIT_INDIRECT || $inheritanceType === self::INHERIT_DIRECT_OR_INDIRECT ) { @@ -72,7 +102,7 @@ class IcingaObjectFilterHelper if (empty($ids)) { $sub->where('(1 = 0)'); } else { - $sub->where("$i.parent_${type}_id IN (?)", $ids); + $sub->where("$i.parent_{$type}_id IN (?)", $ids); } } else { throw new RuntimeException(sprintf( @@ -95,12 +125,12 @@ class IcingaObjectFilterHelper $query->where('(1 = 0)'); } else { $sub = $query->getAdapter()->select()->from( - array('go' => "icinga_${type}group_${type}"), + array('go' => "icinga_{$type}group_{$type}"), array('e' => '(1)') )->join( - array('g' => "icinga_${type}group"), - "go.${type}group_id = g.id" - )->where("go.${type}_id = ${tableAlias}.id") + array('g' => "icinga_{$type}group"), + "go.{$type}group_id = g.id" + )->where("go.{$type}_id = {$tableAlias}.id") ->where('g.object_name IN (?)', $groups); $query->where('EXISTS ?', $sub); @@ -118,13 +148,13 @@ class IcingaObjectFilterHelper $query->where('(1 = 0)'); } else { $sub = $query->getAdapter()->select()->from( - array('go' => "icinga_${type}group_${type}_resolved"), + array('go' => "icinga_{$type}group_{$type}_resolved"), array('e' => '(1)') )->join( - array('g' => "icinga_${type}group"), - "go.${type}group_id = g.id", + array('g' => "icinga_{$type}group"), + "go.{$type}group_id = g.id", [] - )->where("go.${type}_id = ${tableAlias}.id") + )->where("go.{$type}_id = {$tableAlias}.id") ->where('g.object_name IN (?)', $groups); $query->where('EXISTS ?', $sub); diff --git a/library/Director/Db/Migrations.php b/library/Director/Db/Migrations.php index 2310408..ad59329 100644 --- a/library/Director/Db/Migrations.php +++ b/library/Director/Db/Migrations.php @@ -90,7 +90,7 @@ class Migrations public function applyPendingMigrations() { // Ensure we have enough time to migrate - ini_set('max_execution_time', 0); + ini_set('max_execution_time', '0'); foreach ($this->getPendingMigrations() as $migration) { $migration->apply($this->connection); diff --git a/library/Director/DirectorObject/Automation/Basket.php b/library/Director/DirectorObject/Automation/Basket.php index f7eb8e5..81ae107 100644 --- a/library/Director/DirectorObject/Automation/Basket.php +++ b/library/Director/DirectorObject/Automation/Basket.php @@ -4,8 +4,6 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Data\Db\DbObject; -use Icinga\Module\Director\Db; -use Icinga\Module\Director\Exception\DuplicateKeyException; /** * Class Basket @@ -15,16 +13,17 @@ use Icinga\Module\Director\Exception\DuplicateKeyException; */ class Basket extends DbObject implements ExportInterface { - const SELECTION_ALL = true; - const SELECTION_NONE = false; + const SELECTION_ALL = 'ALL'; + const SELECTION_NONE = 'IGNORE'; + const SELECTION_CUSTOM = '[]'; protected $table = 'director_basket'; protected $keyName = 'basket_name'; - protected $chosenObjects = []; + protected $uuidColumn = 'uuid'; - protected $protectedFormerChosenObjects; + protected $chosenObjects = []; protected $defaultProperties = [ 'uuid' => null, @@ -69,44 +68,6 @@ class Basket extends DbObject implements ExportInterface return $this->get('basket_name'); } - public function export() - { - $result = $this->getProperties(); - unset($result['uuid']); - $result['objects'] = Json::decode($result['objects']); - ksort($result); - - return (object) $result; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['basket_name']; - - if ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::exists($name, $db)) { - throw new DuplicateKeyException( - 'Basket "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - $object->setProperties($properties); - - return $object; - } - public function supportsCustomSelectionFor($type) { if (! array_key_exists($type, $this->chosenObjects)) { @@ -121,7 +82,6 @@ class Basket extends DbObject implements ExportInterface if (empty($objects)) { $this->chosenObjects = []; } else { - $this->protectedFormerChosenObjects = $this->chosenObjects; $this->chosenObjects = []; foreach ((array) $objects as $type => $object) { $this->addObjects($type, $object); @@ -141,32 +101,22 @@ class Basket extends DbObject implements ExportInterface { BasketSnapshot::assertValidType($type); // '1' -> from Form! - if ($objects === 'ALL') { + if ($objects === self::SELECTION_ALL) { $objects = true; - } elseif ($objects === null || $objects === 'IGNORE') { + } elseif ($objects === null || $objects === self::SELECTION_NONE) { return; - } elseif ($objects === '[]' || is_array($objects)) { + } elseif ($objects === self::SELECTION_CUSTOM || is_array($objects)) { if (! isset($this->chosenObjects[$type]) || ! is_array($this->chosenObjects[$type])) { $this->chosenObjects[$type] = []; } - if (isset($this->protectedFormerChosenObjects[$type])) { - if (is_array($this->protectedFormerChosenObjects[$type])) { - $this->chosenObjects[$type] = $this->protectedFormerChosenObjects[$type]; - } else { - $this->chosenObjects[$type] = []; - } - } - - if ($objects === '[]') { + if ($objects === self::SELECTION_CUSTOM) { $objects = []; } } if ($objects === true) { $this->chosenObjects[$type] = true; - } elseif ($objects === '0') { - // nothing - } else { + } elseif ($objects !== '0') { // TODO: what would generate '0'? foreach ($objects as $object) { $this->addObject($type, $object); } diff --git a/library/Director/DirectorObject/Automation/BasketDiff.php b/library/Director/DirectorObject/Automation/BasketDiff.php new file mode 100644 index 0000000..8dbb423 --- /dev/null +++ b/library/Director/DirectorObject/Automation/BasketDiff.php @@ -0,0 +1,121 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Automation; + +use gipfl\Json\JsonString; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\ObjectImporter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\DirectorDatalist; +use Ramsey\Uuid\UuidInterface; +use stdClass; + +class BasketDiff +{ + /** @var Db */ + protected $db; + /** @var ObjectImporter */ + protected $importer; + /** @var Exporter */ + protected $exporter; + /** @var BasketSnapshot */ + protected $snapshot; + /** @var ?stdClass */ + protected $objects = null; + /** @var BasketSnapshotFieldResolver */ + protected $fieldResolver; + + public function __construct(BasketSnapshot $snapshot, Db $db) + { + $this->db = $db; + $this->importer = new ObjectImporter($db); + $this->exporter = new Exporter($db); + $this->snapshot = $snapshot; + } + + public function hasChangedFor(string $type, string $key, ?UuidInterface $uuid = null): bool + { + return $this->getCurrentString($type, $key, $uuid) !== $this->getBasketString($type, $key); + } + + public function getCurrentString(string $type, string $key, ?UuidInterface $uuid = null): string + { + $current = $this->getCurrent($type, $key, $uuid); + return $current ? JsonString::encode($current, JSON_PRETTY_PRINT) : ''; + } + + public function getBasketString(string $type, string $key): string + { + return JsonString::encode($this->getBasket($type, $key), JSON_PRETTY_PRINT); + } + + protected function getFieldResolver(): BasketSnapshotFieldResolver + { + if ($this->fieldResolver === null) { + $this->fieldResolver = new BasketSnapshotFieldResolver($this->getBasketObjects(), $this->db); + } + + return $this->fieldResolver; + } + + protected function getCurrent(string $type, string $key, ?UuidInterface $uuid = null): ?stdClass + { + if ($uuid && $current = BasketSnapshot::instanceByUuid($type, $uuid, $this->db)) { + $exported = $this->exporter->export($current); + $this->getFieldResolver()->tweakTargetIds($exported); + } elseif ($current = BasketSnapshot::instanceByIdentifier($type, $key, $this->db)) { + $exported = $this->exporter->export($current); + $this->getFieldResolver()->tweakTargetIds($exported); + } else { + $exported = null; + } + CompareBasketObject::normalize($exported); + + return $exported; + } + + protected function getBasket($type, $key): stdClass + { + $object = $this->getBasketObject($type, $key); + $fields = $object->fields ?? null; + $reExport = $this->exporter->export( + $this->importer->import(BasketSnapshot::getClassForType($type), $object) + ); + if ($fields === null) { + unset($reExport->fields); + } else { + $reExport->fields = $fields; + } + CompareBasketObject::normalize($reExport); + + return $reExport; + } + + public function hasCurrentInstance(string $type, string $key, ?UuidInterface $uuid = null): bool + { + return $this->getCurrentInstance($type, $key, $uuid) !== null; + } + + public function getCurrentInstance(string $type, string $key, ?UuidInterface $uuid = null) + { + if ($uuid && $instance = BasketSnapshot::instanceByUuid($type, $uuid, $this->db)) { + return $instance; + } else { + return BasketSnapshot::instanceByIdentifier($type, $key, $this->db); + } + } + + public function getBasketObjects(): stdClass + { + if ($this->objects === null) { + $this->objects = JsonString::decode($this->snapshot->getJsonDump()); + } + + return $this->objects; + } + + public function getBasketObject(string $type, string $key): stdClass + { + return $this->getBasketObjects()->$type->$key; + } +} diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php index 4ddf2ce..9638e49 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshot.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php @@ -2,10 +2,11 @@ namespace Icinga\Module\Director\DirectorObject\Automation; +use gipfl\Json\JsonDecodeException; use gipfl\Json\JsonEncodeException; use gipfl\Json\JsonString; -use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\ObjectImporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Objects\DirectorDatafield; @@ -29,7 +30,9 @@ use Icinga\Module\Director\Objects\IcingaUserGroup; use Icinga\Module\Director\Objects\ImportSource; use Icinga\Module\Director\Objects\SyncRule; use InvalidArgumentException; +use Ramsey\Uuid\UuidInterface; use RuntimeException; +use stdClass; class BasketSnapshot extends DbObject { @@ -217,16 +220,11 @@ class BasketSnapshot extends DbObject /** * @param Db $connection - * @param bool $replace * @throws \Icinga\Exception\NotFoundError */ - public function restoreTo(Db $connection, $replace = true) + public function restoreTo(Db $connection) { - static::restoreJson( - $this->getJsonDump(), - $connection, - $replace - ); + static::restoreJson($this->getJsonDump(), $connection); } /** @@ -240,61 +238,49 @@ class BasketSnapshot extends DbObject 'basket_uuid' => $basket->get('uuid') ]); $snapshot->objects = []; - foreach ((array) Json::decode($string) as $type => $objects) { + foreach ((array) JsonString::decode($string) as $type => $objects) { $snapshot->objects[$type] = (array) $objects; } return $snapshot; } - public static function restoreJson($string, Db $connection, $replace = true) + public static function restoreJson($string, Db $connection) { - $snapshot = new static(); - $snapshot->restoreObjects( - Json::decode($string), - $connection, - $replace - ); + (new static())->restoreObjects(JsonString::decode($string), $connection); } /** - * @param $all - * @param Db $connection - * @param bool $replace * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Zend_Db_Adapter_Exception * @throws \Icinga\Exception\NotFoundError + * @throws JsonDecodeException */ - protected function restoreObjects($all, Db $connection, $replace = true) + protected function restoreObjects(stdClass $all, Db $connection) { $db = $connection->getDbAdapter(); $db->beginTransaction(); $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); - $this->restoreType($all, 'DataList', $fieldResolver, $connection, $replace); - $this->restoreType($all, 'DatafieldCategory', $fieldResolver, $connection, $replace); + $this->restoreType($all, 'DataList', $fieldResolver, $connection); + $this->restoreType($all, 'DatafieldCategory', $fieldResolver, $connection); $fieldResolver->storeNewFields(); foreach ($this->restoreOrder as $typeName) { - $this->restoreType($all, $typeName, $fieldResolver, $connection, $replace); + $this->restoreType($all, $typeName, $fieldResolver, $connection); } $db->commit(); } /** - * @param $all - * @param $typeName - * @param BasketSnapshotFieldResolver $fieldResolver - * @param Db $connection - * @param $replace * @throws \Icinga\Exception\NotFoundError * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Zend_Db_Adapter_Exception + * @throws JsonDecodeException */ public function restoreType( - &$all, - $typeName, + stdClass $all, + string $typeName, BasketSnapshotFieldResolver $fieldResolver, - Db $connection, - $replace + Db $connection ) { if (isset($all->$typeName)) { $objects = (array) $all->$typeName; @@ -302,11 +288,10 @@ class BasketSnapshot extends DbObject return; } $class = static::getClassForType($typeName); - + $importer = new ObjectImporter($connection); $changed = []; - foreach ($objects as $key => $object) { - /** @var DbObject $new */ - $new = $class::import($object, $connection, $replace); + foreach ($objects as $object) { + $new = $importer->import($class, $object); if ($new->hasBeenModified()) { if ($new instanceof IcingaObject && $new->supportsImports()) { /** @var ExportInterface $new */ @@ -325,7 +310,6 @@ class BasketSnapshot extends DbObject $fieldResolver->relinkObjectFields($new, $object); } } - $allObjects[spl_object_hash($new)] = $object; } /** @var IcingaObject $object */ @@ -334,7 +318,7 @@ class BasketSnapshot extends DbObject } foreach ($changed as $key => $new) { // Store related fields. As objects might have formerly been - // un-stored, let's to it right here + // un-stored, let's do it right here if ($new instanceof IcingaObject) { $fieldResolver->relinkObjectFields($new, $objects[$key]); } @@ -358,10 +342,9 @@ class BasketSnapshot extends DbObject } /** - * @return BasketContent * @throws \Icinga\Exception\NotFoundError */ - protected function getContent() + protected function getContent(): BasketContent { if ($this->content === null) { $this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection()); @@ -380,26 +363,25 @@ class BasketSnapshot extends DbObject } /** - * @return string - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonEncodeException */ - public function getJsonSummary() + public function getJsonSummary(): string { if ($this->hasBeenLoadedFromDb()) { return $this->getContent()->get('summary'); } - return Json::encode($this->getSummary(), JSON_PRETTY_PRINT); + return JsonString::encode($this->getSummary(), JSON_PRETTY_PRINT); } /** * @return array|mixed - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonDecodeException */ public function getSummary() { if ($this->hasBeenLoadedFromDb()) { - return Json::decode($this->getContent()->get('summary')); + return JsonString::decode($this->getContent()->get('summary')); } $summary = []; @@ -412,7 +394,7 @@ class BasketSnapshot extends DbObject /** * @return string - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonEncodeException */ public function getJsonDump() { @@ -428,7 +410,7 @@ class BasketSnapshot extends DbObject try { JsonString::encode($object); } catch (JsonEncodeException $singleError) { - $dump = var_export($object, 1); + $dump = var_export($object, true); if (function_exists('iconv')) { $dump = iconv('UTF-8', 'UTF-8//IGNORE', $dump); } @@ -486,6 +468,17 @@ class BasketSnapshot extends DbObject } /** + * @return ExportInterface|DbObject|null + */ + public static function instanceByUuid(string $typeName, UuidInterface $uuid, Db $connection) + { + /** @var class-string<DbObject> $class */ + $class = static::getClassForType($typeName); + /** @var ExportInterface $object */ + return $class::loadWithUniqueId($uuid, $connection); + } + + /** * @param $typeName * @param $identifier * @param Db $connection @@ -493,21 +486,17 @@ class BasketSnapshot extends DbObject */ public static function instanceByIdentifier($typeName, $identifier, Db $connection) { + /** @var class-string<DbObject> $class */ $class = static::getClassForType($typeName); - if (substr($class, -13) === 'IcingaService') { + if ($class === IcingaService::class) { $identifier = [ 'object_type' => 'template', 'object_name' => $identifier, ]; } - /** @var ExportInterface $object */ - if ($class::exists($identifier, $connection)) { - $object = $class::load($identifier, $connection); - } else { - $object = null; - } - return $object; + /** @var ExportInterface $object */ + return $class::loadOptional($identifier, $connection); } /** diff --git a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php index 4653255..e565f77 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php @@ -4,7 +4,10 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\DirectorDatafield; +use Icinga\Module\Director\Objects\DirectorDatalist; use Icinga\Module\Director\Objects\IcingaObject; +use InvalidArgumentException; +use stdClass; class BasketSnapshotFieldResolver { @@ -39,7 +42,7 @@ class BasketSnapshotFieldResolver * @return DirectorDatafield[] * @throws \Icinga\Exception\NotFoundError */ - public function loadCurrentFields(Db $db) + public function loadCurrentFields(Db $db): array { $fields = []; foreach ($this->getRequiredIds() as $id) { @@ -90,6 +93,11 @@ class BasketSnapshotFieldResolver $existingFields[(int) $mapping->datafield_id] = $mapping; } foreach ($object->fields as $field) { + if (! isset($fieldMap[(int) $field->datafield_id])) { + throw new InvalidArgumentException( + 'Basket Snapshot contains invalid field reference: ' . $field->datafield_id + ); + } $id = $fieldMap[(int) $field->datafield_id]; if (isset($existingFields[$id])) { unset($existingFields[$id]); @@ -114,6 +122,8 @@ class BasketSnapshotFieldResolver } /** + * For diff purposes only, gives '(UNKNOWN)' for fields missing in our DB + * * @param object $object * @throws \Icinga\Exception\NotFoundError */ @@ -127,21 +137,34 @@ class BasketSnapshotFieldResolver if (isset($map[$id])) { $field->datafield_id = $map[$id]; } else { - $field->datafield_id = "(NEW)"; + $field->datafield_id = "(UNKNOWN)"; } } } } - /** - * @return int - */ - protected function getNextNewId() + public static function fixOptionalDatalistReference(stdClass $plain, Db $db) + { + if (isset($plain->settings->datalist_uuid)) { + unset($plain->settings->datalist); + return; + } + if (isset($plain->settings->datalist)) { + // Just try to load the list, final import will fail if missing + // No modification in case we do not find the list, + if ($list = DirectorDatalist::loadOptional($plain->settings->datalist, $db)) { + unset($plain->settings->datalist); + $plain->settings->datalist_id = $list->get('id'); + } + } + } + + protected function getNextNewId(): int { return $this->nextNewId++; } - protected function getRequiredIds() + protected function getRequiredIds(): array { if ($this->requiredIds === null) { if (isset($this->objects['Datafield'])) { @@ -169,7 +192,7 @@ class BasketSnapshotFieldResolver * @param $type * @return object[] */ - protected function getObjectsByType($type) + protected function getObjectsByType($type): array { if (isset($this->objects->$type)) { return (array) $this->objects->$type; @@ -182,7 +205,7 @@ class BasketSnapshotFieldResolver * @return DirectorDatafield[] * @throws \Icinga\Exception\NotFoundError */ - protected function getTargetFields() + protected function getTargetFields(): array { if ($this->targetFields === null) { $this->calculateIdMap(); @@ -194,7 +217,7 @@ class BasketSnapshotFieldResolver /** * @throws \Icinga\Exception\NotFoundError */ - protected function getIdMap() + protected function getIdMap(): array { if ($this->idMap === null) { $this->calculateIdMap(); diff --git a/library/Director/DirectorObject/Automation/CompareBasketObject.php b/library/Director/DirectorObject/Automation/CompareBasketObject.php index ef2e9e2..f1ab6a9 100644 --- a/library/Director/DirectorObject/Automation/CompareBasketObject.php +++ b/library/Director/DirectorObject/Automation/CompareBasketObject.php @@ -31,7 +31,7 @@ class CompareBasketObject static::normalize($v); } unset($v); - $value = $sorted; + $value = (object) $sorted; // foreign baskets might not sort those lists correctly: if (isset($value->list_name) && isset($value->entries)) { @@ -46,7 +46,11 @@ class CompareBasketObject protected static function sortListBy($key, &$list) { usort($list, function ($a, $b) use ($key) { - return $a->$key > $b->$key ? -1 : 1; + if (is_array($a)) { + return $a[$key] > $b[$key] ? -1 : 1; + } else { + return $a->$key > $b->$key ? -1 : 1; + } }); } diff --git a/library/Director/DirectorObject/Automation/ExportInterface.php b/library/Director/DirectorObject/Automation/ExportInterface.php index 275dfed..271824f 100644 --- a/library/Director/DirectorObject/Automation/ExportInterface.php +++ b/library/Director/DirectorObject/Automation/ExportInterface.php @@ -2,18 +2,8 @@ namespace Icinga\Module\Director\DirectorObject\Automation; -use Icinga\Module\Director\Db; - interface ExportInterface { - /** - * @deprecated - * @return \stdClass - */ - public function export(); - - public static function import($plain, Db $db, $replace = false); - // TODO: // public function getXyzChecksum(); public function getUniqueIdentifier(); diff --git a/library/Director/DirectorObject/Automation/ImportExport.php b/library/Director/DirectorObject/Automation/ImportExport.php index a5e72fa..1664f5d 100644 --- a/library/Director/DirectorObject/Automation/ImportExport.php +++ b/library/Director/DirectorObject/Automation/ImportExport.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\ObjectImporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\DirectorDatafield; use Icinga\Module\Director\Objects\DirectorDatalist; @@ -125,8 +126,9 @@ class ImportExport { $count = 0; $this->connection->runFailSafeTransaction(function () use ($objects, &$count) { + $importer = new ObjectImporter($this->connection); foreach ($objects as $object) { - ImportSource::import($object, $this->connection)->store(); + $importer->import(ImportSource::class, $object)->store(); $count++; } }); @@ -138,8 +140,9 @@ class ImportExport { $count = 0; $this->connection->runFailSafeTransaction(function () use ($objects, &$count) { + $importer = new ObjectImporter($this->connection); foreach ($objects as $object) { - SyncRule::import($object, $this->connection)->store(); + $importer->import(SyncRule::class, $object)->store(); } $count++; }); diff --git a/library/Director/DirectorObject/Lookup/ServiceFinder.php b/library/Director/DirectorObject/Lookup/ServiceFinder.php index fb8d74c..a14d853 100644 --- a/library/Director/DirectorObject/Lookup/ServiceFinder.php +++ b/library/Director/DirectorObject/Lookup/ServiceFinder.php @@ -4,6 +4,8 @@ namespace Icinga\Module\Director\DirectorObject\Lookup; use gipfl\IcingaWeb2\Url; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; +use Icinga\Module\Director\Integration\MonitoringModule\Monitoring; use Icinga\Module\Director\Objects\HostApplyMatches; use Icinga\Module\Director\Objects\IcingaHost; use RuntimeException; @@ -49,31 +51,4 @@ class ServiceFinder return false; } - - /** - * @param $serviceName - * @return Url - */ - public function getRedirectionUrl($serviceName) - { - if ($this->auth === null) { - throw new RuntimeException('Auth is required for ServiceFinder when dealing when asking for URLs'); - } - if ($this->auth->hasPermission('director/host')) { - if ($info = $this::find($this->host, $serviceName)) { - return $info->getUrl(); - } - } - if ($this->auth->hasPermission('director/monitoring/services-ro')) { - return Url::fromPath('director/host/servicesro', [ - 'name' => $this->host->getObjectName(), - 'service' => $serviceName - ]); - } - - return Url::fromPath('director/host/invalidservice', [ - 'name' => $this->host->getObjectName(), - 'service' => $serviceName, - ]); - } } diff --git a/library/Director/DirectorObject/ObjectPurgeHelper.php b/library/Director/DirectorObject/ObjectPurgeHelper.php index a043965..5e50727 100644 --- a/library/Director/DirectorObject/ObjectPurgeHelper.php +++ b/library/Director/DirectorObject/ObjectPurgeHelper.php @@ -44,11 +44,11 @@ class ObjectPurgeHelper // TODO: this is object-specific and to be found in the ::import() function! unset($properties['fields']); $object = $class::fromPlainObject($properties); - } elseif (\get_class($object) !== $class) { + } elseif (get_class($object) !== $class) { throw new InvalidArgumentException( 'Can keep only matching objects, expected "%s", got "%s', $class, - \get_class($keep) + get_class($object) ); } $key = []; diff --git a/library/Director/Field/FormFieldSuggestion.php b/library/Director/Field/FormFieldSuggestion.php new file mode 100644 index 0000000..2f7f875 --- /dev/null +++ b/library/Director/Field/FormFieldSuggestion.php @@ -0,0 +1,167 @@ +<?php + +namespace Icinga\Module\Director\Field; + +use gipfl\Translation\TranslationHelper; +use Icinga\Module\Director\Objects\IcingaCommand; + +class FormFieldSuggestion +{ + use TranslationHelper; + + /** + * Macro/Argument names used in command argument values + * + * @var array + */ + protected $argumentVars = []; + protected $suggestedFields = []; + protected $blacklistedVars = []; + protected $descriptions = []; + protected $booleans = []; + + /** @var ?IcingaCommand */ + protected $command; + + /** @var array */ + protected $existingFields; + + protected $fields = null; + + public function __construct( + ?IcingaCommand $command, + array $existingFields + ) { + $this->command = $command; + $this->existingFields = $existingFields; + } + + public function getCommandFields(): array + { + if ($this->fields === null) { + $this->fields = $this->prepareFields(); + } + + return $this->fields; + } + + protected function prepareFields(): array + { + // TODO: remove assigned ones! + + foreach ($this->existingFields as $id => $field) { + if (preg_match('/ \(([^)]+)\)$/', $field, $m)) { + $this->blacklistedVars['$' . $m[1] . '$'] = $id; + } + } + + if ($this->command) { + foreach ($this->command->arguments() as $arg) { + if ($arg->argument_format === 'string') { + foreach (self::extractMacroNamesFromString($arg->argument_value) as $val) { + $this->addSuggestion($val, $arg->description, $this->argumentVars); + } + } + + if (($arg->set_if_format === 'string' || $arg->set_if_format === null) + && $val = self::getMacroIfStringIsSingleMacro($arg->set_if) + ) { + $this->addSuggestion($val, $arg->description, $this->booleans); + } + } + } + + asort($this->suggestedFields, SORT_NATURAL | SORT_FLAG_CASE); + ksort($this->argumentVars); + ksort($this->booleans); + asort($this->existingFields, SORT_NATURAL | SORT_FLAG_CASE); + + // Prepare combined fields array + $fields = []; + if (! empty($this->suggestedFields)) { + $fields[$this->translate('Suggested fields')] = $this->suggestedFields; + } + + if (! empty($this->argumentVars)) { + $fields[$this->translate('Argument macros')] = $this->argumentVars; + } + + if (! empty($this->booleans)) { + $fields[$this->translate('Toggles (boolean arguments)')] = $this->booleans; + } + + if (! empty($this->existingFields)) { + $fields[$this->translate('Other available fields')] = $this->existingFields; + } + + return $fields; + } + + public function getDescription($id) + { + if (array_key_exists($id, $this->descriptions)) { + return $this->descriptions[$id]; + } + + return null; + } + + public function isBoolean(string $macro): bool + { + return isset($this->booleans[$macro]); + } + + protected function addSuggestion( + string $val, + ?string $description, + array &$targetList + ) { + if (array_key_exists($val, $this->blacklistedVars)) { + $id = $this->blacklistedVars[$val]; + + // Hint: if not set it might already have been + // removed in this loop + if (array_key_exists($id, $this->existingFields)) { + $this->suggestedFields[$id] = $this->existingFields[$id]; + unset($this->existingFields[$id]); + } + } else { + $targetList[$val] = $val; + $this->descriptions[$val] = $description; + } + } + + /** + * Returns a macro name string ($macro_name$), if the given string is such, null otherwise + * + * @param ?string $string + * @return ?string + */ + protected static function getMacroIfStringIsSingleMacro(?string $string): ?string + { + if ($string === null) { + return null; + } + + if (preg_match('/^(\$[a-z0-9_]+\$)$/i', $string, $matches)) { + return $matches[1]; + } + + return null; + } + + /** + * Extracts all macro names ($macro_name$) from a given string + * + * @param ?string $string + * @return array + */ + protected static function extractMacroNamesFromString(?string $string): array + { + if ($string !== null && preg_match_all('/(\$[a-z0-9_]+\$)/i', $string, $matches, PREG_PATTERN_ORDER)) { + return $matches[1]; + } + + return []; + } +} diff --git a/library/Director/Filter/CidrExpression.php b/library/Director/Filter/CidrExpression.php new file mode 100644 index 0000000..169ddce --- /dev/null +++ b/library/Director/Filter/CidrExpression.php @@ -0,0 +1,89 @@ +<?php + +namespace Icinga\Module\Director\Filter; + +use Icinga\Data\Filter\FilterExpression; +use InvalidArgumentException; + +use function array_map; +use function filter_var; +use function inet_pton; +use function pack; +use function preg_match; +use function str_pad; +use function str_split; + +class CidrExpression extends FilterExpression +{ + protected $networkAddress; + protected $broadcastAddress; + + public function __construct($column, $sign, $expression) + { + if ($parts = static::splitOptionalCidrString($expression)) { + list($this->networkAddress, $this->broadcastAddress) = $parts; + } else { + throw new InvalidArgumentException("'$expression' isn't valid CIDR notation"); + } + + parent::__construct($column, $sign, $expression); + } + + public static function isCidrFormat(string $string): bool + { + return static::splitOptionalCidrString($string) !== null; + } + + protected static function splitOptionalCidrString(string $string): ?array + { + if (preg_match('#^(.+?)/(\d{1,3})$#', $string, $match)) { + $address = $match[1]; + $mask = (int) $match[2]; + + if (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $mask <= 32) { + $bits = 32; + } elseif (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && $mask <= 128) { + $bits = 128; + } else { + return null; + } + + $binaryAddress = inet_pton($address); + $broadcast = $binaryAddress | static::bitmaskToInverseBinaryMask($mask, $bits); + + return [$binaryAddress, $broadcast]; + } + + return null; + } + + public function matches($row): bool + { + if (! isset($row->{$this->column})) { + return false; + } + $value = inet_pton((string) $row->{$this->column}); + + return $value >= $this->networkAddress && $value <= $this->broadcastAddress; + } + + public static function fromExpression(FilterExpression $filter): CidrExpression + { + $sign = $filter->getSign(); + if ($sign !== '=') { + throw new InvalidArgumentException("'$sign' cannot be applied to CIDR notation"); + } + return new CidrExpression($filter->getColumn(), $sign, $filter->getExpression()); + } + + protected static function bitmaskToInverseBinaryMask($mask, $maxLen): string + { + $binary = str_pad(str_pad('', $mask, '0'), $maxLen, '1'); + $address = ''; + foreach (array_map('bindec', str_split($binary, 8)) as $char) { + $address .= pack('C*', $char); + } + + return $address; + } +} diff --git a/library/Director/Filter/FilterEnrichment.php b/library/Director/Filter/FilterEnrichment.php new file mode 100644 index 0000000..c726f76 --- /dev/null +++ b/library/Director/Filter/FilterEnrichment.php @@ -0,0 +1,29 @@ +<?php + +namespace Icinga\Module\Director\Filter; + +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; + +class FilterEnrichment +{ + public static function enrichFilter(Filter $filter): Filter + { + if ($filter instanceof FilterExpression) { + if (CidrExpression::isCidrFormat($filter->getExpression())) { + return CidrExpression::fromExpression($filter); + } + } elseif ($filter instanceof FilterChain) { + foreach ($filter->filters() as $subFilter) { + if ($subFilter instanceof FilterExpression + && CidrExpression::isCidrFormat($subFilter->getExpression()) + ) { + $filter->replaceById($subFilter->getId(), CidrExpression::fromExpression($subFilter)); + } + } + } + + return $filter; + } +} diff --git a/library/Director/Hook/BranchSupportHook.php b/library/Director/Hook/BranchSupportHook.php index 6615cbe..72ce096 100644 --- a/library/Director/Hook/BranchSupportHook.php +++ b/library/Director/Hook/BranchSupportHook.php @@ -14,7 +14,7 @@ abstract class BranchSupportHook { /** * @param Request $request - * @param BranchSTore $store + * @param BranchStore $store * @param Auth $auth * @return Branch */ diff --git a/library/Director/IcingaConfig/AgentWizard.php b/library/Director/IcingaConfig/AgentWizard.php index aceddb1..93527f5 100644 --- a/library/Director/IcingaConfig/AgentWizard.php +++ b/library/Director/IcingaConfig/AgentWizard.php @@ -148,15 +148,34 @@ class AgentWizard public function renderIcinga4WindowsWizardCommand($token) { - $script = "Use-Icinga;\n" - . 'Start-IcingaAgentInstallWizard `' . "\n " + $ifwParams = [ + "IfW-DirectorSelfServiceKey" => [ + "Values" => [$token], + ], + "IfW-DirectorUrl" => [ + "Values" => [$this->getDirectorUrl()], + ], + "IfW-StableRepository" => [ + "Values" => ["https://packages.icinga.com/IcingaForWindows/stable"], + ] + ]; + + $script = "[Net.ServicePointManager]::SecurityProtocol = 'tls12, tls11';\n" + . "\$ProgressPreference = 'SilentlyContinue';" . "\n" + . "[string]\$ScriptFile = 'C:\Users\Public\IcingaForWindows.ps1';\n" + . "\n" + . "Invoke-WebRequest `\n " . $this->renderPowershellParameters([ - 'DirectorUrl' => $this->getDirectorUrl(), - 'SelfServiceAPIKey' => $token, - 'UseDirectorSelfService' => 1, - 'OverrideDirectorVars' => 0, - 'Reconfigure', - 'RunInstaller' + 'UseBasicParsing', + 'Uri' => "https://packages.icinga.com/IcingaForWindows/IcingaForWindows.ps1", + 'OutFile' => '$ScriptFile;', + ]) . "\n" + . "\n" + . "& \$ScriptFile `\n " + . $this->renderPowershellParameters([ + 'ModuleDirectory' => "C:\Program Files\WindowsPowerShell\Modules\\", + 'InstallCommand' => json_encode($ifwParams, JSON_UNESCAPED_SLASHES), + 'IcingaRepository' => "https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json" ]); return $script; @@ -235,6 +254,8 @@ class AgentWizard $ret .= implode(', ', $vals); } elseif (is_int($value)) { $ret .= $value; + } elseif (is_string($value) && $value[0] === '$') { + $ret .= $value; } else { $ret .= $this->renderPowershellString($value); } @@ -297,7 +318,7 @@ class AgentWizard } else { $value = escapeshellarg($value); } - $script = preg_replace("~^#?$quotedKey='@$quotedKey@'$~m", "${key}=${value}", $script); + $script = preg_replace("~^#?$quotedKey='@$quotedKey@'$~m", "{$key}={$value}", $script); } return $script; diff --git a/library/Director/IcingaConfig/AssignRenderer.php b/library/Director/IcingaConfig/AssignRenderer.php index 6acbfee..495ad1e 100644 --- a/library/Director/IcingaConfig/AssignRenderer.php +++ b/library/Director/IcingaConfig/AssignRenderer.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\IcingaConfig; +use gipfl\Json\JsonDecodeException; use gipfl\Json\JsonString; use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\FilterAnd; @@ -128,8 +129,14 @@ class AssignRenderer } $column = $filter->getColumn(); - $rawExpression = Json::decode($filter->getExpression()); - $expression = $this->renderExpressionValue($rawExpression); + try { + $rawExpression = JsonString::decode($filter->getExpression()); + $expression = $this->renderExpressionValue($rawExpression); + } catch (JsonDecodeException $e) { + throw new InvalidArgumentException( + "Got invalid JSON in filter string: $column" . $filter->getSign() . $filter->getExpression() + ); + } if (is_array($rawExpression) && $filter instanceof FilterMatch) { return $this->renderInArray($column, $expression); diff --git a/library/Director/IcingaConfig/ExtensibleSet.php b/library/Director/IcingaConfig/ExtensibleSet.php index 9120816..d52a30e 100644 --- a/library/Director/IcingaConfig/ExtensibleSet.php +++ b/library/Director/IcingaConfig/ExtensibleSet.php @@ -50,7 +50,7 @@ class ExtensibleSet $set->object = $object; $set->propertyName = $propertyName; - if ($object->hasBeenLoadedFromDb()) { + if ($object->hasBeenLoadedFromDb() && $id = $object->get('id')) { $set->loadFromDb(); } diff --git a/library/Director/IcingaConfig/IcingaConfig.php b/library/Director/IcingaConfig/IcingaConfig.php index 72edd7e..a79bf3c 100644 --- a/library/Director/IcingaConfig/IcingaConfig.php +++ b/library/Director/IcingaConfig/IcingaConfig.php @@ -25,6 +25,9 @@ class IcingaConfig protected $zoneMap = array(); + /** @var ?array Exists for caching reasons at rendering time */ + protected $nonGlobalZones = null; + protected $lastActivityChecksum; /** @var \Zend_Db_Adapter_Abstract */ @@ -349,6 +352,15 @@ class IcingaConfig return $this->zoneMap[$id]; } + public function listNonGlobalZones(): array + { + if ($this->nonGlobalZones === null) { + $this->nonGlobalZones = array_values($this->connection->enumNonglobalZones()); + } + + return $this->nonGlobalZones; + } + /** * @return self */ @@ -436,9 +448,9 @@ class IcingaConfig $start = microtime(true); MemoryLimit::raiseTo('1024M'); - ini_set('max_execution_time', 0); + ini_set('max_execution_time', '0'); // Workaround for https://bugs.php.net/bug.php?id=68606 or similar - ini_set('zend.enable_gc', 0); + ini_set('zend.enable_gc', '0'); if (! $this->connection->isPgsql() && $this->db->quote("1\0") !== '\'1\\0\'') { throw new RuntimeException( @@ -501,6 +513,7 @@ class IcingaConfig "\nconst DirectorStageDir = dirname(dirname(current_filename))\n" . $this->renderFlappingLogHelper() . $this->renderHostOverridableVars() + . $this->renderIfwFallbackTemplate() ); return $this; @@ -566,6 +579,20 @@ if (! globals.contains(DirectorOverrideTemplate)) { ); } + + protected function renderIfwFallbackTemplate(): string + { + return ' +// Make sure config validates for Icinga < 2.14 with IfW 1.11 configuration. This might look weird, +// but is intentional. get_object() does\'t work as expected at parse time. +if (! globals.System || ! System.get_template || ! get_template(CheckCommand, "ifw-api-check-command")) { + object CheckCommand "ifw-api" { + import "plugin-check-command" + } +} +'; + } + /** * @param string $checksum * diff --git a/library/Director/IcingaConfig/IcingaConfigHelper.php b/library/Director/IcingaConfig/IcingaConfigHelper.php index 03c017e..634337f 100644 --- a/library/Director/IcingaConfig/IcingaConfigHelper.php +++ b/library/Director/IcingaConfig/IcingaConfigHelper.php @@ -177,7 +177,7 @@ class IcingaConfigHelper throw new InvalidArgumentException(sprintf( 'Unexpected type %s', - var_export($value, 1) + var_export($value, true) )); } diff --git a/library/Director/Import/ImportSourceCoreApi.php b/library/Director/Import/ImportSourceCoreApi.php index 6d590ec..3c54d48 100644 --- a/library/Director/Import/ImportSourceCoreApi.php +++ b/library/Director/Import/ImportSourceCoreApi.php @@ -30,7 +30,7 @@ class ImportSourceCoreApi extends ImportSourceHook public function listColumns() { $res = $this->fetchData(); - if (empty($data)) { + if (empty($res)) { return array('object_name'); } diff --git a/library/Director/Import/ImportSourceLdap.php b/library/Director/Import/ImportSourceLdap.php index 4518565..559669d 100644 --- a/library/Director/Import/ImportSourceLdap.php +++ b/library/Director/Import/ImportSourceLdap.php @@ -45,7 +45,7 @@ class ImportSourceLdap extends ImportSourceHook public static function addSettingsFormFields(QuickForm $form) { - Util::addLDAPResourceFormElement($form, 'resource'); + Util::addLdapResourceFormElement($form, 'resource'); $form->addElement('text', 'base', array( 'label' => $form->translate('LDAP Search Base'), 'description' => $form->translate( diff --git a/library/Director/Import/ImportSourceRestApi.php b/library/Director/Import/ImportSourceRestApi.php index dc772e1..45f7351 100644 --- a/library/Director/Import/ImportSourceRestApi.php +++ b/library/Director/Import/ImportSourceRestApi.php @@ -69,6 +69,7 @@ class ImportSourceRestApi extends ImportSourceHook $data = $result; foreach ($parts as $part) { // un-escape any dots + /** @var string $part */ $part = preg_replace('~\\\\.~', '.', $part); if (property_exists($data, $part)) { diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php index 8fea46c..2957433 100644 --- a/library/Director/Import/Sync.php +++ b/library/Director/Import/Sync.php @@ -16,6 +16,7 @@ use Icinga\Module\Director\Objects\HostGroupMembershipResolver; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaHostGroup; use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaObjectGroup; use Icinga\Module\Director\Objects\ImportSource; use Icinga\Module\Director\Objects\IcingaService; use Icinga\Module\Director\Objects\SyncProperty; @@ -49,6 +50,9 @@ class Sync /** @var array<mixed, array<int, string>> key => [property, property]*/ protected $setNull = []; + /** @var array<mixed, array<string, mixed>> key => [propertyName, newValue]*/ + protected $newProperties = []; + /** @var bool Whether we already prepared your sync */ protected $isPrepared = false; @@ -88,6 +92,12 @@ class Sync /** @var ?DbObjectStore */ protected $store; + /** @var IcingaObjectGroup[] */ + protected $modifiedGroups = []; + + /** @var IcingaObject[] */ + protected $modifiedGroupObjects = []; + /** * @param SyncRule $rule * @param ?DbObjectStore $store @@ -137,24 +147,6 @@ class Sync } /** - * Transform the given value to an array - * - * @param array|string|null $value - * - * @return array - */ - protected function wantArray($value) - { - if (is_array($value)) { - return $value; - } elseif ($value === null) { - return []; - } else { - return [$value]; - } - } - - /** * Raise PHP resource limits * * @return self; @@ -162,7 +154,7 @@ class Sync protected function raiseLimits() { MemoryLimit::raiseTo('1024M'); - ini_set('max_execution_time', 0); + ini_set('max_execution_time', '0'); return $this; } @@ -452,7 +444,28 @@ class Sync if ($this->store) { $objects = $this->store->loadAll(DbObjectTypeRegistry::tableNameByType($ruleObjectType), 'object_name'); } else { - $objects = IcingaObject::loadAllByType($ruleObjectType, $this->db); + $keyColumn = null; + $query = null; + // We enforce named index for combined-key templates (Services and Sets) and applied Sets + if ($ruleObjectType === 'service' || $ruleObjectType === 'serviceSet') { + foreach ($this->syncProperties as $prop) { + $configuredObjectType = $prop->get('source_expression'); + if ($prop->get('destination_field') === 'object_type' + && ( + $configuredObjectType === 'template' + || ($configuredObjectType === 'apply' && $ruleObjectType === 'serviceSet') + ) + ) { + $keyColumn = 'object_name'; + $table = $ruleObjectType === 'service' + ? BranchSupport::TABLE_ICINGA_SERVICE + : BranchSupport::TABLE_ICINGA_SERVICE_SET; + $query = $this->db->getDbAdapter()->select() + ->from($table)->where('object_type = ?', $configuredObjectType); + } + } + } + $objects = IcingaObject::loadAllByType($ruleObjectType, $this->db, $query, $keyColumn); } if ($useLowerCaseKeys) { @@ -532,6 +545,15 @@ class Sync */ protected function prepareNewObject($row, DbObject $object, $objectKey, $sourceId) { + if (!isset($this->newProperties[$objectKey])) { + $this->newProperties[$objectKey] = []; + } + // TODO: some more improvements are possible here. First, no need to instantiate + // all new objects, we could stick with the newProperties array. Next, we + // should be more correct when respecting sync property order. Right now, + // a property from another Import Source might win, even if property order + // tells something different. This is a very rare case, but still incorrect. + $properties = &$this->newProperties[$objectKey]; foreach ($this->syncProperties as $propertyKey => $p) { if ($p->get('source_id') !== $sourceId) { continue; @@ -557,38 +579,35 @@ class Sync $varName = substr($prop, 5); if (substr($varName, -2) === '[]') { $varName = substr($varName, 0, -2); - $current = $this->wantArray($object->vars()->$varName); $object->vars()->$varName = array_merge( - $current, - $this->wantArray($val) + (array) ($object->vars()->$varName), + (array) $val ); } else { - if ($val === null) { - $this->setNull[$objectKey][$prop] = $prop; - } else { - unset($this->setNull[$objectKey][$prop]); - $object->vars()->$varName = $val; - } + $this->setPropertyWithNullLogic($object, $objectKey, $prop, $val, $properties); } } else { - if ($val === null) { - $this->setNull[$objectKey][$prop] = $prop; - } else { - unset($this->setNull[$objectKey][$prop]); - $object->set($prop, $val); - } + $this->setPropertyWithNullLogic($object, $objectKey, $prop, $val, $properties); } } else { - if ($val === null) { - $this->setNull[$objectKey][$prop] = $prop; - } else { - unset($this->setNull[$objectKey][$prop]); - $object->set($prop, $val); - } + $this->setPropertyWithNullLogic($object, $objectKey, $prop, $val, $properties); } } } + protected function setPropertyWithNullLogic(DbObject $object, $objectKey, $property, $value, &$allProps) + { + if ($value === null) { + if (! array_key_exists($property, $allProps) || $allProps[$property] === null) { + $this->setNull[$objectKey][$property] = $property; + } + } else { + unset($this->setNull[$objectKey][$property]); + $object->set($property, $value); + } + $allProps[$property] = $value; + } + /** * @return $this */ @@ -625,6 +644,9 @@ class Sync protected function notifyResolvers() { if ($resolver = $this->getHostGroupMembershipResolver()) { + if ($this->rule->get('object_type') === 'hostgroup') { + $resolver->setGroups($this->modifiedGroups); + } $resolver->refreshDb(true); } @@ -748,6 +770,13 @@ class Sync } } + protected function optionallyTellResolverAboutModifiedGroup(IcingaObjectGroup $group) + { + if (in_array('assign_filter', $group->getModifiedProperties())) { + $this->modifiedGroups[] = $group; + } + } + /** * @param $key * @param DbObject|IcingaObject $object @@ -791,7 +820,11 @@ class Sync } } - if (isset($this->setNull[$key])) { + // Hint: in theory, NULL should be set on new objects, but this has no effect + // anyway, and we also do not store vars.something = null, this would + // instead delete the variable. So here we do not need to check for new + // objects, and skip all null values with update policy = 'ignore' + if ($policy !== 'ignore' && isset($this->setNull[$key])) { foreach ($this->setNull[$key] as $property) { $this->objects[$key]->set($property, null); } @@ -850,6 +883,9 @@ class Sync } if ($object->hasBeenModified()) { + if ($object instanceof IcingaObjectGroup) { + $this->optionallyTellResolverAboutModifiedGroup($object); + } $existing = $object->hasBeenLoadedFromDb(); if ($existing) { if ($this->store) { diff --git a/library/Director/Import/SyncUtils.php b/library/Director/Import/SyncUtils.php index 5528b2d..c106c20 100644 --- a/library/Director/Import/SyncUtils.php +++ b/library/Director/Import/SyncUtils.php @@ -99,7 +99,7 @@ class SyncUtils throw new InvalidArgumentException(sprintf( 'Data is not nested, cannot access %s: %s', $var, - var_export($row, 1) + var_export($row, true) )); } diff --git a/library/Director/Integration/BackendInterface.php b/library/Director/Integration/BackendInterface.php new file mode 100644 index 0000000..7b2b88c --- /dev/null +++ b/library/Director/Integration/BackendInterface.php @@ -0,0 +1,55 @@ +<?php + +namespace Icinga\Module\Director\Integration; + +use Icinga\Web\Url; + +interface BackendInterface +{ + /** + * Whether the backend has the given host + * + * @param ?string $hostName + * + * @return bool + */ + public function hasHost(?string $hostName): bool; + + /** + * Whether the backend has the given service of the specified host + * + * @param ?string $hostName + * @param ?string $serviceName + * + * @return bool + */ + public function hasService(?string $hostName, ?string $serviceName): bool; + + /** + * Whether an authenticated user has the permission (is not restricted) to modify given host + * + * @param ?string $hostName + * + * @return bool + */ + public function canModifyHost(?string $hostName): bool; + + /** + * Whether an authenticated user has the permission (is not restricted) to modify given service of specified host + * + * @param ?string $hostName + * @param ?string $serviceName + * + * @return bool + */ + public function canModifyService(?string $hostName, ?string $serviceName): bool; + + /** + * Get the url of given host + * + * @param ?string $hostName + * + * @return Url + */ + public function getHostUrl(?string $hostName): ?Url; +} diff --git a/library/Director/Integration/Icingadb/IcingadbBackend.php b/library/Director/Integration/Icingadb/IcingadbBackend.php new file mode 100644 index 0000000..874cddd --- /dev/null +++ b/library/Director/Integration/Icingadb/IcingadbBackend.php @@ -0,0 +1,127 @@ +<?php + +namespace Icinga\Module\Director\Integration\Icingadb; + +use Icinga\Module\Director\Auth\Permission; +use Icinga\Module\Director\Auth\Restriction; +use Icinga\Module\Director\Integration\BackendInterface; +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use Icinga\Web\Url; +use ipl\Orm\Query; +use ipl\Stdlib\Filter; + +class IcingadbBackend implements BackendInterface +{ + use Database; + use Auth; + + public function hasHost(?string $hostName): bool + { + if ($hostName === null) { + return false; + } + + return $this->getHostQuery($hostName)->first() !== null; + } + + public function hasService(?string $hostName, ?string $serviceName): bool + { + if ($hostName === null || $serviceName === null) { + return false; + } + + return $this->getServiceQuery($hostName, $serviceName)->first() !== null; + } + + public function getHostUrl(?string $hostName): ?Url + { + if ($hostName === null) { + return null; + } + + return Url::fromPath('icingadb/host', ['name' => $hostName]); + } + + public function canModifyHost(?string $hostName): bool + { + if ($hostName === null + || ! $this->getAuth()->hasPermission(Permission::ICINGADB_HOSTS) + ) { + return false; + } + + $query = $this->getHostQuery($hostName); + + return $query->first() !== null; + } + + public function canModifyService(?string $hostName, ?string $serviceName): bool + { + if ($hostName === null + || $serviceName === null + || ! $this->getAuth()->hasPermission(Permission::ICINGADB_SERVICES) + ) { + return false; + } + + $query = $this->getServiceQuery($hostName, $serviceName); + + return $query->first() !== null; + } + + /** + * Get the query for given host + * + * @param string $hostName + * + * @return Query + */ + protected function getHostQuery(string $hostName): Query + { + $query = Host::on($this->getDb()) + ->filter(Filter::equal('host.name', $hostName)); + + $this->applyDirectorRestrictions($query); + + return $query; + } + + /** + * Get the query for given host and service + * + * @param string $hostName + * @param string $serviceName + * + * @return Query + */ + protected function getServiceQuery(string $hostName, string $serviceName): Query + { + $query = Service::on($this->getDb()) + ->filter(Filter::all( + Filter::equal('service.name', $serviceName), + Filter::equal('host.name', $hostName) + )); + + $this->applyDirectorRestrictions($query); + + return $query; + } + + /** + * Apply director restrictions on the given query + * + * @param Query $query + */ + protected function applyDirectorRestrictions(Query $query): void + { + $queryFilter = Filter::any(); + foreach ($this->getAuth()->getRestrictions(Restriction::ICINGADB_RW_OBJECT_FILTER) as $restriction) { + $queryFilter->add($this->parseRestriction($restriction, Restriction::ICINGADB_RW_OBJECT_FILTER)); + } + + $query->filter($queryFilter); + } +} diff --git a/library/Director/Integration/MonitoringModule/Monitoring.php b/library/Director/Integration/MonitoringModule/Monitoring.php new file mode 100644 index 0000000..5a2dfde --- /dev/null +++ b/library/Director/Integration/MonitoringModule/Monitoring.php @@ -0,0 +1,200 @@ +<?php + +namespace Icinga\Module\Director\Integration\MonitoringModule; + +use Exception; +use Icinga\Application\Icinga; +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Director\Auth\MonitoringRestriction; +use Icinga\Module\Director\Auth\Permission; +use Icinga\Module\Director\Auth\Restriction; +use Icinga\Module\Director\Integration\BackendInterface; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use Icinga\Web\Url; + +class Monitoring implements BackendInterface +{ + /** @var ?MonitoringBackend */ + protected $backend; + + /** @var Auth */ + protected $auth; + + public function __construct(Auth $auth) + { + $this->auth = $auth; + $this->initializeMonitoringBackend(); + } + + public function getHostUrl(?string $hostName): ?Url + { + if ($hostName === null) { + return null; + } + + return Url::fromPath('monitoring/host/show', ['host' => $hostName]); + } + + public function hasHost(?string $hostName): bool + { + if ($hostName === null || ! $this->isAvailable()) { + return false; + } + + try { + return $this->selectHost($hostName)->fetchOne() === $hostName; + } catch (Exception $_) { + return false; + } + } + + public function hasService(?string $hostName, ?string $serviceName): bool + { + if ($hostName === null || $serviceName === null || ! $this->isAvailable()) { + return false; + } + + try { + return $this->rowIsService( + $this->selectService($hostName, $serviceName)->fetchRow(), + $hostName, + $serviceName + ); + } catch (Exception $_) { + return false; + } + } + + public function canModifyService(?string $hostName, ?string $serviceName): bool + { + if (! $this->isAvailable() || $hostName === null || $serviceName === null) { + return false; + } + if ($this->auth->hasPermission(Permission::MONITORING_SERVICES)) { + $restriction = null; + foreach ($this->auth->getRestrictions(Restriction::MONITORING_RW_OBJECT_FILTER) as $restriction) { + if ($this->hasServiceWithFilter($hostName, $serviceName, Filter::fromQueryString($restriction))) { + return true; + } + } + if ($restriction === null) { + return $this->hasService($hostName, $serviceName); + } + } + + return false; + } + + public function canModifyHost(?string $hostName): bool + { + if ($hostName !== null && $this->isAvailable() && $this->auth->hasPermission(Permission::MONITORING_HOSTS)) { + $restriction = null; + foreach ($this->auth->getRestrictions(Restriction::MONITORING_RW_OBJECT_FILTER) as $restriction) { + if ($this->hasHostWithFilter($hostName, Filter::fromQueryString($restriction))) { + return true; + } + } + if ($restriction === null) { + return $this->hasHost($hostName); + } + } + + return false; + } + + protected function hasHostWithFilter($hostname, Filter $filter): bool + { + try { + return $this->selectHost($hostname)->applyFilter($filter)->fetchOne() === $hostname; + } catch (Exception $e) { + return false; + } + } + + protected function hasServiceWithFilter($hostname, $service, Filter $filter): bool + { + try { + return $this->rowIsService( + $this->selectService($hostname, $service)->applyFilter($filter)->fetchRow(), + $hostname, + $service + ); + } catch (Exception $e) { + return false; + } + } + + protected function selectHost($hostname) + { + return $this->selectHostStatus($hostname, [ + 'hostname' => 'host_name', + ]); + } + + protected function selectHostStatus($hostname, $columns) + { + return $this->restrictQuery( + $this->backend + ->select() + ->from('hostStatus', $columns) + ->where('host_name', $hostname) + ); + } + + protected function selectService($hostname, $service) + { + return $this->selectServiceStatus($hostname, $service, [ + 'hostname' => 'host_name', + 'service' => 'service_description', + ]); + } + + protected function selectServiceStatus($hostname, $service, $columns) + { + return $this->restrictQuery( + $this->backend + ->select() + ->from('serviceStatus', $columns) + ->where('host_name', $hostname) + ->where('service_description', $service) + ); + } + + protected function restrictQuery($query) + { + $query->applyFilter(MonitoringRestriction::getObjectsFilter($this->auth)); + return $query; + } + + protected function rowIsService($row, $hostname, $service): bool + { + return (array) $row === [ + 'hostname' => $hostname, + 'service' => $service, + ]; + } + + protected function initializeMonitoringBackend() + { + $app = Icinga::app(); + $modules = $app->getModuleManager(); + if (!$modules->hasLoaded('monitoring') && $app->isCli()) { + $modules->loadEnabledModules(); + } + + if ($modules->hasLoaded('monitoring')) { + try { + $this->backend = MonitoringBackend::instance(); + } catch (ConfigurationError $e) { + $this->backend = null; + } + } + } + + protected function isAvailable(): bool + { + return $this->backend !== null; + } +} diff --git a/library/Director/Monitoring.php b/library/Director/Monitoring.php deleted file mode 100644 index f5d4108..0000000 --- a/library/Director/Monitoring.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php - -namespace Icinga\Module\Director; - -use Icinga\Application\Icinga; -use Icinga\Authentication\Auth; -use Icinga\Data\Filter\Filter; -use Icinga\Module\Monitoring\Backend\MonitoringBackend; - -class Monitoring -{ - protected $backend; - - public function __construct() - { - $app = Icinga::app(); - $modules = $app->getModuleManager(); - if (!$modules->hasLoaded('monitoring') && $app->isCli()) { - $app->getModuleManager()->loadEnabledModules(); - } - - if ($modules->hasLoaded('monitoring')) { - $this->backend = MonitoringBackend::instance(); - } - } - - public function isAvailable() - { - return $this->backend !== null; - } - - public function hasHost($hostname) - { - return $this->backend->select()->from('hostStatus', [ - 'hostname' => 'host_name', - ])->where('host_name', $hostname)->fetchOne() === $hostname; - } - - public function hasService($hostname, $service) - { - return (array) $this->prepareServiceKeyColumnQuery($hostname, $service)->fetchRow() === [ - 'hostname' => $hostname, - 'service' => $service, - ]; - } - - public function authCanEditHost(Auth $auth, $hostname) - { - if ($auth->hasPermission('director/monitoring/hosts')) { - $restriction = null; - foreach ($auth->getRestrictions('director/monitoring/rw-object-filter') as $restriction) { - if ($this->hasHostWithExtraFilter($hostname, Filter::fromQueryString($restriction))) { - return true; - } - } - if ($restriction === null) { - return $this->hasHost($hostname); - } - } - - return false; - } - - public function authCanEditService(Auth $auth, $hostname, $service) - { - if ($hostname === null || $service === null) { - // TODO: UUID support! - return false; - } - if ($auth->hasPermission('director/monitoring/services')) { - $restriction = null; - foreach ($auth->getRestrictions('director/monitoring/rw-object-filter') as $restriction) { - if ($this->hasServiceWithExtraFilter($hostname, $service, Filter::fromQueryString($restriction))) { - return true; - } - } - if ($restriction === null) { - return $this->hasService($hostname, $service); - } - } - - return false; - } - - public function hasHostWithExtraFilter($hostname, Filter $filter) - { - return $this->backend->select()->from('hostStatus', [ - 'hostname' => 'host_name', - ])->where('host_name', $hostname)->applyFilter($filter)->fetchOne() === $hostname; - } - - public function hasServiceWithExtraFilter($hostname, $service, Filter $filter) - { - return (array) $this - ->prepareServiceKeyColumnQuery($hostname, $service) - ->applyFilter($filter) - ->fetchRow() === [ - 'hostname' => $hostname, - 'service' => $service, - ]; - } - - public function getHostState($hostname) - { - $hostStates = [ - '0' => 'up', - '1' => 'down', - '2' => 'unreachable', - '99' => 'pending', - ]; - - $query = $this->backend->select()->from('hostStatus', [ - 'hostname' => 'host_name', - 'state' => 'host_state', - 'problem' => 'host_problem', - 'acknowledged' => 'host_acknowledged', - 'in_downtime' => 'host_in_downtime', - 'output' => 'host_output', - ])->where('host_name', $hostname); - - $res = $query->fetchRow(); - if ($res === false) { - $res = (object) [ - 'hostname' => $hostname, - 'state' => '99', - 'problem' => '0', - 'acknowledged' => '0', - 'in_downtime' => '0', - 'output' => null, - ]; - } - - $res->state = $hostStates[$res->state]; - - return $res; - } - - protected function prepareServiceKeyColumnQuery($hostname, $service) - { - return $this->backend - ->select() - ->from('serviceStatus', [ - 'hostname' => 'host_name', - 'service' => 'service_description', - ]) - ->where('host_name', $hostname) - ->where('service_description', $service); - } -} diff --git a/library/Director/Objects/DirectorActivityLog.php b/library/Director/Objects/DirectorActivityLog.php index cb041b6..2cecc2e 100644 --- a/library/Director/Objects/DirectorActivityLog.php +++ b/library/Director/Objects/DirectorActivityLog.php @@ -7,6 +7,7 @@ use Icinga\Module\Director\Db; use Icinga\Authentication\Auth; use Icinga\Application\Icinga; use Icinga\Application\Logger; +use stdClass; class DirectorActivityLog extends DbObject { @@ -176,7 +177,19 @@ class DirectorActivityLog extends DbObject { $name = $object->getObjectName(); $type = $object->getTableName(); - $oldProps = json_encode($object->getPlainUnmodifiedObject()); + /** @var stdClass $plainUnmodifiedObject */ + $plainUnmodifiedObject = $object->getPlainUnmodifiedObject(); + + if ($object instanceof IcingaServiceSet) { + $services = []; + foreach ($object->getCachedServices() as $service) { + $services[$service->getObjectName()] = $service->toPlainObject(); + } + + $plainUnmodifiedObject->services = $services; + } + + $oldProps = json_encode($plainUnmodifiedObject); $data = [ 'object_name' => $name, diff --git a/library/Director/Objects/DirectorDatafield.php b/library/Director/Objects/DirectorDatafield.php index 84db068..ced6218 100644 --- a/library/Director/Objects/DirectorDatafield.php +++ b/library/Director/Objects/DirectorDatafield.php @@ -2,28 +2,30 @@ namespace Icinga\Module\Director\Objects; -use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Data\Db\DbObjectWithSettings; use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver; use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\Forms\IcingaServiceForm; use Icinga\Module\Director\Hook\DataTypeHook; use Icinga\Module\Director\Resolver\OverriddenVarsResolver; use Icinga\Module\Director\Web\Form\DirectorObjectForm; -use InvalidArgumentException; +use Ramsey\Uuid\Uuid; +use stdClass; use Zend_Form_Element as ZfElement; class DirectorDatafield extends DbObjectWithSettings { protected $table = 'director_datafield'; - protected $keyName = 'id'; - protected $autoincKeyName = 'id'; + protected $uuidColumn = 'uuid'; + protected $settingsTable = 'director_datafield_setting'; + protected $settingsRemoteId = 'datafield_id'; protected $defaultProperties = [ 'id' => null, + 'uuid' => null, 'category_id' => null, 'varname' => null, 'caption' => null, @@ -33,16 +35,11 @@ class DirectorDatafield extends DbObjectWithSettings ]; protected $relations = [ - 'category' => 'DirectorDatafieldCategory' + 'category' => 'DirectorDatafieldCategory' ]; - protected $settingsTable = 'director_datafield_setting'; - - protected $settingsRemoteId = 'datafield_id'; - - /** @var DirectorDatafieldCategory|null */ + /** @var ?DirectorDatafieldCategory */ private $category; - private $object; public static function fromDbRow($row, Db $connection) @@ -70,10 +67,9 @@ class DirectorDatafield extends DbObjectWithSettings } /** - * @return DirectorDatafieldCategory|null * @throws \Icinga\Exception\NotFoundError */ - public function getCategory() + public function getCategory(): ?DirectorDatafieldCategory { if ($this->category) { return $this->category; @@ -84,7 +80,7 @@ class DirectorDatafield extends DbObjectWithSettings } } - public function getCategoryName() + public function getCategoryName(): ?string { $category = $this->getCategory(); if ($category === null) { @@ -105,27 +101,26 @@ class DirectorDatafield extends DbObjectWithSettings } $this->category = $category; } else { - if (DirectorDatafieldCategory::exists($category, $this->getConnection())) { - $this->setCategory(DirectorDatafieldCategory::load($category, $this->getConnection())); + if ($category = DirectorDatafieldCategory::loadOptional($category, $this->getConnection())) { + $this->setCategory($category); } else { $this->setCategory(DirectorDatafieldCategory::create([ 'category_name' => $category ], $this->getConnection())); } } - - return $this; } /** - * @return object * @throws \Icinga\Exception\NotFoundError */ - public function export() + public function export(): stdClass { $plain = (object) $this->getProperties(); - $plain->originalId = $plain->id; unset($plain->id); + if ($uuid = $this->get('uuid')) { + $plain->uuid = Uuid::fromBytes($uuid)->toString(); + } $plain->settings = (object) $this->getSettings(); if (property_exists($plain->settings, 'datalist_id')) { @@ -144,63 +139,35 @@ class DirectorDatafield extends DbObjectWithSettings } /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return DirectorDatafield * @throws \Icinga\Exception\NotFoundError */ - public static function import($plain, Db $db, $replace = false) + public static function import(stdClass $plain, Db $db): DirectorDatafield { - $properties = (array) $plain; - if (isset($properties['originalId'])) { - $id = $properties['originalId']; - unset($properties['originalId']); - } else { - $id = null; - } - - if (isset($properties['settings']->datalist)) { - // Just try to load the list, import should fail if missing - $list = DirectorDatalist::load( - $properties['settings']->datalist, - $db - ); - } else { - $list = null; - } - - $compare = Json::decode(Json::encode($properties)); - if ($id && static::exists($id, $db)) { - $existing = static::loadWithAutoIncId($id, $db); - $existingProperties = (array) $existing->export(); - unset($existingProperties['originalId']); - if (CompareBasketObject::equals((object) $compare, (object) $existingProperties)) { - return $existing; + $dba = $db->getDbAdapter(); + if ($uuid = $plain->uuid ?? null) { + $uuid = Uuid::fromString($uuid); + if ($candidate = DirectorDatafield::loadWithUniqueId($uuid, $db)) { + BasketSnapshotFieldResolver::fixOptionalDatalistReference($plain, $db); + assert($candidate instanceof DirectorDatafield); + $candidate->setProperties((array) $plain); + return $candidate; } } - - if ($list) { - unset($properties['settings']->datalist); - $properties['settings']->datalist_id = $list->get('id'); - } - - $dba = $db->getDbAdapter(); - $query = $dba->select() - ->from('director_datafield') - ->where('varname = ?', $plain->varname); + $query = $dba->select()->from('director_datafield')->where('varname = ?', $plain->varname); $candidates = DirectorDatafield::loadAll($db, $query); foreach ($candidates as $candidate) { $export = $candidate->export(); - unset($export->originalId); CompareBasketObject::normalize($export); - if (CompareBasketObject::equals($export, $compare)) { + unset($export->uuid); + unset($plain->originalId); + if (CompareBasketObject::equals($export, $plain)) { return $candidate; } } + BasketSnapshotFieldResolver::fixOptionalDatalistReference($plain, $db); - return static::create($properties, $db); + return static::create((array) $plain, $db); } protected function beforeStore() @@ -223,7 +190,7 @@ class DirectorDatafield extends DbObjectWithSettings return $this->object; } - public function getFormElement(DirectorObjectForm $form, $name = null) + public function getFormElement(DirectorObjectForm $form, $name = null): ?ZfElement { $className = $this->get('datatype'); @@ -305,7 +272,7 @@ class DirectorDatafield extends DbObjectWithSettings } } - protected function eventuallyGetResolvedCommandVar(IcingaObject $object, $varName) + protected function eventuallyGetResolvedCommandVar(IcingaObject $object, $varName): ?array { if (! $object->hasRelation('check_command')) { return null; diff --git a/library/Director/Objects/DirectorDatalist.php b/library/Director/Objects/DirectorDatalist.php index ae5c983..1bb821b 100644 --- a/library/Director/Objects/DirectorDatalist.php +++ b/library/Director/Objects/DirectorDatalist.php @@ -4,68 +4,35 @@ namespace Icinga\Module\Director\Objects; use Exception; use Icinga\Module\Director\Data\Db\DbObject; -use Icinga\Module\Director\Db; +use Icinga\Module\Director\DataType\DataTypeDatalist; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; use Icinga\Module\Director\Exception\DuplicateKeyException; class DirectorDatalist extends DbObject implements ExportInterface { protected $table = 'director_datalist'; - protected $keyName = 'list_name'; - protected $autoincKeyName = 'id'; + protected $uuidColumn = 'uuid'; - protected $defaultProperties = array( + protected $defaultProperties = [ 'id' => null, + 'uuid' => null, 'list_name' => null, 'owner' => null - ); + ]; /** @var DirectorDatalistEntry[] */ - protected $storedEntries; + protected $entries; public function getUniqueIdentifier() { return $this->get('list_name'); } - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws \Icinga\Exception\NotFoundError - * @throws DuplicateKeyException - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - if (isset($properties['originalId'])) { - unset($properties['originalId']); - } else { - $id = null; - } - $name = $properties['list_name']; - - if ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::exists($name, $db)) { - throw new DuplicateKeyException( - 'Data List %s already exists', - $name - ); - } else { - $object = static::create([], $db); - } - $object->setProperties($properties); - - return $object; - } - - public function setEntries($entries) + public function setEntries($entries): void { - $existing = $this->getStoredEntries(); + $existing = $this->getEntries(); $new = []; $seen = []; @@ -101,15 +68,13 @@ class DirectorDatalist extends DbObject implements ExportInterface $this->hasBeenModified = true; } - $this->storedEntries = $existing; - ksort($this->storedEntries); - - return $this; + $this->entries = $existing; + ksort($this->entries); } protected function beforeDelete() { - if ($this->hasBeenUsed()) { + if ($this->isInUse()) { throw new Exception( sprintf( "Cannot delete '%s', as the datalist '%s' is currently being used.", @@ -120,9 +85,13 @@ class DirectorDatalist extends DbObject implements ExportInterface } } - protected function hasBeenUsed() + protected function isInUse(): bool { - $datalistType = 'Icinga\\Module\\Director\\DataType\\DataTypeDatalist'; + $id = $this->get('id'); + if ($id === null) { + return false; + } + $db = $this->getDb(); $dataFieldsCheck = $db->select() @@ -137,8 +106,8 @@ class DirectorDatalist extends DbObject implements ExportInterface 'l.id = dfs.setting_value', [] ) - ->where('datatype = ?', $datalistType) - ->where('setting_value = ?', $this->get('id')); + ->where('datatype = ?', DataTypeDatalist::class) + ->where('setting_value = ?', $id); if ($db->fetchOne($dataFieldsCheck)) { return true; @@ -147,7 +116,7 @@ class DirectorDatalist extends DbObject implements ExportInterface $syncCheck = $db->select() ->from(['sp' =>'sync_property'], ['source_expression']) ->where('sp.destination_field = ?', 'list_id') - ->where('sp.source_expression = ?', $this->get('id')); + ->where('sp.source_expression = ?', $id); if ($db->fetchOne($syncCheck)) { return true; @@ -161,65 +130,38 @@ class DirectorDatalist extends DbObject implements ExportInterface */ public function onStore() { - if ($this->storedEntries) { + if ($this->entries) { $db = $this->getConnection(); $removedKeys = []; $myId = $this->get('id'); - foreach ($this->storedEntries as $key => $entry) { + foreach ($this->entries as $key => $entry) { if ($entry->shouldBeRemoved()) { $entry->delete(); $removedKeys[] = $key; } else { - if (! $entry->hasBeenLoadedFromDb()) { - $entry->set('list_id', $myId); - } $entry->set('list_id', $myId); $entry->store($db); } } foreach ($removedKeys as $key) { - unset($this->storedEntries[$key]); - } - } - } - - /** - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @return object - */ - public function export() - { - $plain = (object) $this->getProperties(); - $plain->originalId = $plain->id; - unset($plain->id); - - $plain->entries = []; - foreach ($this->getStoredEntries() as $key => $entry) { - if ($entry->shouldBeRemoved()) { - continue; + unset($this->entries[$key]); } - $plainEntry = (object) $entry->getProperties(); - unset($plainEntry->list_id); - - $plain->entries[] = $plainEntry; } - - return $plain; } - protected function getStoredEntries() + public function getEntries(): array { - if ($this->storedEntries === null) { - if ($id = $this->get('id')) { - $this->storedEntries = DirectorDatalistEntry::loadAllForList($this); - ksort($this->storedEntries); + if ($this->entries === null) { + if ($this->get('id')) { + $this->entries = DirectorDatalistEntry::loadAllForList($this); + ksort($this->entries); } else { - $this->storedEntries = []; + $this->entries = []; } } - return $this->storedEntries; + return $this->entries; } } diff --git a/library/Director/Objects/DirectorDatalistEntry.php b/library/Director/Objects/DirectorDatalistEntry.php index 086686a..278de97 100644 --- a/library/Director/Objects/DirectorDatalistEntry.php +++ b/library/Director/Objects/DirectorDatalistEntry.php @@ -51,7 +51,7 @@ class DirectorDatalistEntry extends DbObject } else { throw new RuntimeException( 'Expected array or null for allowed_roles, got %s', - var_export($roles, 1) + var_export($roles, true) ); } } diff --git a/library/Director/Objects/DirectorDeploymentLog.php b/library/Director/Objects/DirectorDeploymentLog.php index 0794a3c..e9b07ce 100644 --- a/library/Director/Objects/DirectorDeploymentLog.php +++ b/library/Director/Objects/DirectorDeploymentLog.php @@ -46,7 +46,11 @@ class DirectorDeploymentLog extends DbObject public function getConfigHexChecksum() { - return bin2hex($this->config_checksum); + $checksum = $this->get('config_checksum'); + if ($checksum === null) { + return null; + } + return bin2hex($checksum); } public function getConfig() diff --git a/library/Director/Objects/DirectorJob.php b/library/Director/Objects/DirectorJob.php index 361f764..2a18d52 100644 --- a/library/Director/Objects/DirectorJob.php +++ b/library/Director/Objects/DirectorJob.php @@ -122,8 +122,12 @@ class DirectorJob extends DbObjectWithSettings implements ExportInterface, Insta return false; } + if ($this->get('ts_last_attempt') === null) { + return true; + } + return ( - strtotime((int) $this->get('ts_last_attempt')) + $this->get('run_interval') * 2 + strtotime($this->get('ts_last_attempt')) + $this->get('run_interval') * 2 ) < time(); } @@ -194,87 +198,6 @@ class DirectorJob extends DbObjectWithSettings implements ExportInterface, Insta } /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() - { - $plain = (object) $this->getProperties(); - $plain->originalId = $plain->id; - unset($plain->id); - unset($plain->timeperiod_id); - if ($this->hasTimeperiod()) { - $plain->timeperiod = $this->timeperiod()->getObjectName(); - } - - foreach ($this->stateProperties as $key) { - unset($plain->$key); - } - $plain->settings = $this->getInstance()->exportSettings(); - - return $plain; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return DirectorJob - * @throws DuplicateKeyException - * @throws NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $dummy = new static; - $idCol = $dummy->autoincKeyName; - $keyCol = $dummy->keyName; - $properties = (array) $plain; - if (isset($properties['originalId'])) { - $id = $properties['originalId']; - unset($properties['originalId']); - } else { - $id = null; - } - $name = $properties[$keyCol]; - - if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) { - $object = static::loadWithAutoIncId($id, $db); - } elseif ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::exists($name, $db)) { - throw new DuplicateKeyException( - 'Director Job "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - $settings = (array) $properties['settings']; - - if (array_key_exists('source', $settings) && ! (array_key_exists('source_id', $settings))) { - $val = ImportSource::load($settings['source'], $db)->get('id'); - $settings['source_id'] = $val; - unset($settings['source']); - } - - if (array_key_exists('rule', $settings) && ! (array_key_exists('rule_id', $settings))) { - $val = SyncRule::load($settings['rule'], $db)->get('id'); - $settings['rule_id'] = $val; - unset($settings['rule']); - } - - $properties['settings'] = (object) $settings; - $object->setProperties($properties); - if ($id !== null) { - $object->reallySet($idCol, $id); - } - - return $object; - } - - /** * @param string $name * @param int $id * @param Db $connection diff --git a/library/Director/Objects/GroupMembershipResolver.php b/library/Director/Objects/GroupMembershipResolver.php index f5ef418..7266de9 100644 --- a/library/Director/Objects/GroupMembershipResolver.php +++ b/library/Director/Objects/GroupMembershipResolver.php @@ -211,7 +211,7 @@ abstract class GroupMembershipResolver $query, $object, 'o', - Db\IcingaObjectFilterHelper::INHERIT_DIRECT_OR_INDIRECT + IcingaObjectFilterHelper::INHERIT_DIRECT_OR_INDIRECT ); foreach ($object::loadAll($this->connection, $query) as $child) { @@ -352,7 +352,7 @@ abstract class GroupMembershipResolver $type = $this->type; if ($this->groupMap === null) { $this->groupMap = $this->db->fetchPairs( - $this->db->select()->from("icinga_${type}group", ['object_name', 'id']) + $this->db->select()->from("icinga_{$type}group", ['object_name', 'id']) ); } @@ -390,9 +390,9 @@ abstract class GroupMembershipResolver $db->delete( $this->getResolvedTableName(), sprintf( - "(${type}group_id = %d AND ${type}_id = %d)", - $row["${type}group_id"], - $row["${type}_id"] + "({$type}group_id = %d AND {$type}_id = %d)", + $row["{$type}group_id"], + $row["{$type}_id"] ) ); } @@ -416,16 +416,16 @@ abstract class GroupMembershipResolver foreach ($objectIds as $objectId) { if (! array_key_exists($objectId, $right[$groupId])) { $diff[] = array( - "${type}group_id" => $groupId, - "${type}_id" => $objectId, + "{$type}group_id" => $groupId, + "{$type}_id" => $objectId, ); } } } else { foreach ($objectIds as $objectId) { $diff[] = array( - "${type}group_id" => $groupId, - "${type}_id" => $objectId, + "{$type}group_id" => $groupId, + "{$type}_id" => $objectId, ); } } @@ -445,16 +445,16 @@ abstract class GroupMembershipResolver $query = $this->db->select()->from( array('hgh' => $this->getResolvedTableName()), array( - 'group_id' => "${type}group_id", - 'object_id' => "${type}_id", + 'group_id' => "{$type}group_id", + 'object_id' => "{$type}_id", ) ); - $this->addMembershipWhere($query, "${type}_id", $this->objects); - $this->addMembershipWhere($query, "${type}group_id", $this->groups); + $this->addMembershipWhere($query, "{$type}_id", $this->objects); + $this->addMembershipWhere($query, "{$type}group_id", $this->groups); if (! empty($this->groups)) { // load staticGroups (we touched here) additionally, so we can compare changes - $this->addMembershipWhere($query, "${type}group_id", $this->staticGroups); + $this->addMembershipWhere($query, "{$type}group_id", $this->staticGroups); } foreach ($this->db->fetchAll($query) as $row) { @@ -602,7 +602,7 @@ abstract class GroupMembershipResolver { $type = $this->getType(); $query = $this->db->select()->from( - array('hg' => "icinga_${type}group"), + array('hg' => "icinga_{$type}group"), array( 'id', 'assign_filter', @@ -629,7 +629,7 @@ abstract class GroupMembershipResolver protected function getTableName() { $type = $this->getType(); - return "icinga_${type}group_${type}"; + return "icinga_{$type}group_{$type}"; } protected function getResolvedTableName() diff --git a/library/Director/Objects/IcingaArguments.php b/library/Director/Objects/IcingaArguments.php index e788da8..22bf914 100644 --- a/library/Director/Objects/IcingaArguments.php +++ b/library/Director/Objects/IcingaArguments.php @@ -155,7 +155,9 @@ class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer if (property_exists($value, 'type')) { // argument is directly set as function, no further properties if ($value->type === 'Function') { - $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED; + $attrs['argument_value'] = property_exists($value, 'body') + ? $value->body + : self::COMMENT_DSL_UNSUPPORTED; $attrs['argument_format'] = 'expression'; } } elseif (property_exists($value, 'value')) { @@ -296,6 +298,7 @@ class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer $this->arguments = IcingaCommandArgument::loadAll($connection, $query, 'argument_name'); $this->cloneStored(); $this->refreshIndex(); + $this->modified = false; return $this; } @@ -360,6 +363,7 @@ class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer } $this->refreshIndex(); $this->cloneStored(); + $this->modified = false; } /** @@ -393,7 +397,9 @@ class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer unset($this->arguments[$key]); } + $this->refreshIndex(); $this->cloneStored(); + $this->modified = false; return $this; } diff --git a/library/Director/Objects/IcingaCommand.php b/library/Director/Objects/IcingaCommand.php index 35f38a4..8c5aed0 100644 --- a/library/Director/Objects/IcingaCommand.php +++ b/library/Director/Objects/IcingaCommand.php @@ -2,9 +2,7 @@ namespace Icinga\Module\Director\Objects; -use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; use Icinga\Module\Director\Objects\Extension\Arguments; @@ -129,10 +127,8 @@ class IcingaCommand extends IcingaObject implements ObjectWithArguments, ExportI // return $value; } - if (self::$pluginDir !== null) { - if (($pos = strpos($value, self::$pluginDir)) === 0) { - $value = substr($value, strlen(self::$pluginDir) + 1); - } + if (isset($value, self::$pluginDir) && strpos($value, self::$pluginDir) === 0) { + $value = substr($value, strlen(self::$pluginDir) + 1); } return $value; @@ -212,89 +208,6 @@ class IcingaCommand extends IcingaObject implements ObjectWithArguments, ExportI return $this->getObjectName(); } - /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() - { - $props = (array) $this->toPlainObject(); - if (isset($props['arguments'])) { - foreach ($props['arguments'] as $key => $argument) { - if (property_exists($argument, 'command_id')) { - unset($props['arguments'][$key]->command_id); - } - } - } - $props['fields'] = $this->loadFieldReferences(); - ksort($props); - - return (object) $props; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaCommand - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['object_name']; - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Command "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - unset($properties['fields']); - $object->setProperties($properties); - - return $object; - } - - /** - * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader - * @return array - */ - protected function loadFieldReferences() - { - $db = $this->getDb(); - - $res = $db->fetchAll( - $db->select()->from([ - 'cf' => 'icinga_command_field' - ], [ - 'cf.datafield_id', - 'cf.is_required', - 'cf.var_filter', - ])->join(['df' => 'director_datafield'], 'df.id = cf.datafield_id', []) - ->where('command_id = ?', $this->get('id')) - ->order('varname ASC') - ); - - if (empty($res)) { - return []; - } else { - foreach ($res as $field) { - $field->datafield_id = (int) $field->datafield_id; - } - - return $res; - } - } - protected function renderCommand() { $command = $this->get('command'); diff --git a/library/Director/Objects/IcingaDependency.php b/library/Director/Objects/IcingaDependency.php index c9d9b89..abd92e6 100644 --- a/library/Director/Objects/IcingaDependency.php +++ b/library/Director/Objects/IcingaDependency.php @@ -3,9 +3,7 @@ namespace Icinga\Module\Director\Objects; use Icinga\Exception\ConfigurationError; -use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; use Icinga\Exception\NotFoundError; use Icinga\Data\Filter\Filter; @@ -80,52 +78,21 @@ class IcingaDependency extends IcingaObject implements ExportInterface return $this->getObjectName(); } - /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() + public function parentHostIsVar() { - $props = (array) $this->toPlainObject(); - ksort($props); - - return (object) $props; + return $this->get('parent_host_var') !== null; } /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError + * Check if the given string is a custom variable + * + * @param $string string + * + * @return false|int */ - public static function import($plain, Db $db, $replace = false) + protected function isCustomVar(string $string) { - $properties = (array) $plain; - $name = $properties['object_name']; - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Dependency "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - $object->setProperties($properties); - - return $object; - } - - public function parentHostIsVar() - { - return $this->get('parent_host_var') !== null; + return preg_match('/^(?:host|service)\.vars\..+$/', $string); } /** @@ -485,9 +452,16 @@ class IcingaDependency extends IcingaObject implements ExportInterface public function renderParent_service_by_name() { // @codingStandardsIgnoreEnd + $var = $this->get('parent_service_by_name'); + if ($this->isCustomVar($var)) { + return c::renderKeyValue( + 'parent_service_name', + $var + ); + } return c::renderKeyValue( 'parent_service_name', - c::renderString($this->get('parent_service_by_name')) + c::renderString($var) ); } @@ -506,7 +480,7 @@ class IcingaDependency extends IcingaObject implements ExportInterface protected function resolveUnresolvedRelatedProperty($name) { $short = substr($name, 0, -3); - /** @var IcingaObject $class */ + /** @var IcingaObject|string $class */ $class = $this->getRelationClass($short); $objKey = $this->unresolvedRelatedProperties[$name]; @@ -589,7 +563,25 @@ class IcingaDependency extends IcingaObject implements ExportInterface $this->reallySet($name, $object->get('id')); unset($this->unresolvedRelatedProperties[$name]); } else { - throw new NotFoundError('Unable to resolve related property: "%s"', $name); + // Depend on a single service on a single host. Rare case, as usually you want to + // depend on a service on the very same host - and leave the Host field empty. The + // latter is already being handled above. This duplicates some code, but I'll leave + // it this way for now. There might have been a reason for the parent_host_id = null + // check in that code. + if ($name === 'parent_service_id' && $this->get('object_type') === 'apply') { + $this->reallySet( + 'parent_service_by_name', + $this->unresolvedRelatedProperties[$name] + ); + $this->reallySet('parent_service_id', null); + unset($this->unresolvedRelatedProperties[$name]); + return; + } + throw new NotFoundError(sprintf( + 'Unable to resolve related property: %s "%s"', + $name, + $this->unresolvedRelatedProperties[$name] + )); } } @@ -620,8 +612,12 @@ class IcingaDependency extends IcingaObject implements ExportInterface $related = parent::getRelatedProperty($key); // handle special case for plain string parent service on Dependency // Apply rules - if ($related === null && $key === 'parent_service' - && null !== $this->get('parent_service_by_name') + if ($related === null + && $key === 'parent_service' + && ( + $this->get('parent_service_by_name') + && ! $this->isCustomVar($this->get('parent_service_by_name')) + ) ) { return $this->get('parent_service_by_name'); } diff --git a/library/Director/Objects/IcingaHost.php b/library/Director/Objects/IcingaHost.php index 2731f4a..7859324 100644 --- a/library/Director/Objects/IcingaHost.php +++ b/library/Director/Objects/IcingaHost.php @@ -7,7 +7,6 @@ use Icinga\Exception\NotFoundError; use Icinga\Module\Director\Data\PropertiesFilter; use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfig; use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; @@ -310,88 +309,18 @@ class IcingaHost extends IcingaObject implements ExportInterface } } - /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() + protected function rendersConditionalTemplate(): bool { - // TODO: ksort in toPlainObject? - $props = (array) $this->toPlainObject(); - $props['fields'] = $this->loadFieldReferences(); - ksort($props); - - return (object) $props; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaHost - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['object_name']; - if ($properties['object_type'] !== 'template') { - throw new InvalidArgumentException(sprintf( - 'Can import only Templates, got "%s" for "%s"', - $properties['object_type'], - $name - )); - } - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Service Template "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - // $object->newFields = $properties['fields']; - unset($properties['fields']); - $object->setProperties($properties); - - return $object; + return $this->getRenderingZone() === self::ALL_NON_GLOBAL_ZONES; } - /** - * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader - * @return array - */ - protected function loadFieldReferences() + protected function getDefaultZone(IcingaConfig $config = null) { - $db = $this->getDb(); - - $res = $db->fetchAll( - $db->select()->from([ - 'hf' => 'icinga_host_field' - ], [ - 'hf.datafield_id', - 'hf.is_required', - 'hf.var_filter', - ])->join(['df' => 'director_datafield'], 'df.id = hf.datafield_id', []) - ->where('host_id = ?', $this->get('id')) - ->order('varname ASC') - ); - - if (empty($res)) { - return []; - } else { - foreach ($res as $field) { - $field->datafield_id = (int) $field->datafield_id; - } - return $res; + if ($this->isTemplate()) { + return self::ALL_NON_GLOBAL_ZONES; } + + return parent::getDefaultZone(); } public function beforeDelete() diff --git a/library/Director/Objects/IcingaHostGroup.php b/library/Director/Objects/IcingaHostGroup.php index e11f672..e78e931 100644 --- a/library/Director/Objects/IcingaHostGroup.php +++ b/library/Director/Objects/IcingaHostGroup.php @@ -31,12 +31,8 @@ class IcingaHostGroup extends IcingaObjectGroup return $this; } - protected function notifyResolvers() + protected function getMemberShipResolver() { - $resolver = $this->getHostGroupMembershipResolver(); - $resolver->addGroup($this); - $resolver->refreshDb(); - - return $this; + return $this->getHostGroupMembershipResolver(); } } diff --git a/library/Director/Objects/IcingaNotification.php b/library/Director/Objects/IcingaNotification.php index 9c5d08d..4768704 100644 --- a/library/Director/Objects/IcingaNotification.php +++ b/library/Director/Objects/IcingaNotification.php @@ -2,9 +2,8 @@ namespace Icinga\Module\Director\Objects; -use Icinga\Module\Director\Db; +use Icinga\Module\Director\CustomVariable\CustomVariables; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; use RuntimeException; @@ -29,6 +28,8 @@ class IcingaNotification extends IcingaObject implements ExportInterface 'notification_interval' => null, 'period_id' => null, 'zone_id' => null, + 'users_var' => null, + 'user_groups_var' => null, 'assign_filter' => null, ]; @@ -85,92 +86,76 @@ class IcingaNotification extends IcingaObject implements ExportInterface * @codingStandardsIgnoreStart * @return string */ - protected function renderTimes_end() + protected function renderUsers_var() { // @codingStandardsIgnoreEnd - return c::renderKeyValue('times.end', c::renderInterval($this->get('times_end'))); - } - - public function getUniqueIdentifier() - { - return $this->getObjectName(); + return ''; } /** - * @return \stdClass - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError + * @codingStandardsIgnoreStart + * @return string */ - public function export() + protected function renderUser_groups_var() { - // TODO: ksort in toPlainObject? - $props = (array) $this->toPlainObject(); - $props['fields'] = $this->loadFieldReferences(); - ksort($props); - - return (object) $props; + // @codingStandardsIgnoreEnd + return ''; } - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) + protected function renderUserVarsSuffixFor($property) { - $properties = (array) $plain; - $name = $properties['object_name']; - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Notification "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); + $varName = $this->getResolvedProperty("{$property}_var"); + if ($varName === null) { + return ''; + } + + $varSuffix = CustomVariables::renderKeySuffix($varName); + $indent = ' '; + $objectType = $this->get('apply_to'); + if ($objectType === 'service') { + return "{$indent}if (service.vars$varSuffix) {\n" + . c::renderKeyOperatorValue($property, '+=', "service.vars$varSuffix", $indent . ' ') + . "$indent} else {\n" + . $this->getHostSnippet($indent . ' ') + . "$indent if (host.vars$varSuffix) { " + . c::renderKeyOperatorValue($property, '+=', "host.vars$varSuffix }", '') + . "$indent}\n"; + } elseif ($objectType === 'host') { + return $this->getHostSnippet() + . "{$indent}if (host.vars$varSuffix) { " + . c::renderKeyOperatorValue($property, '+=', "host.vars$varSuffix }"); } - // $object->newFields = $properties['fields']; - unset($properties['fields']); - $object->setProperties($properties); + return ''; + } + + protected function getHostSnippet($indent = ' ') + { + return "{$indent}if (! host) {\n" + . "$indent var host = get_host(host_name)\n" + . "$indent}\n"; + } - return $object; + protected function renderSuffix() + { + return $this->renderUserVarsSuffixFor('users') + . $this->renderUserVarsSuffixFor('user_groups') + . parent::renderSuffix(); } /** - * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader - * @return array + * @codingStandardsIgnoreStart + * @return string */ - protected function loadFieldReferences() + protected function renderTimes_end() { - $db = $this->getDb(); - - $res = $db->fetchAll( - $db->select()->from([ - 'nf' => 'icinga_notification_field' - ], [ - 'nf.datafield_id', - 'nf.is_required', - 'nf.var_filter', - ])->join(['df' => 'director_datafield'], 'df.id = nf.datafield_id', []) - ->where('notification_id = ?', $this->get('id')) - ->order('varname ASC') - ); - - if (empty($res)) { - return []; - } else { - foreach ($res as $field) { - $field->datafield_id = (int) $field->datafield_id; - } - return $res; - } + // @codingStandardsIgnoreEnd + return c::renderKeyValue('times.end', c::renderInterval($this->get('times_end'))); + } + + public function getUniqueIdentifier() + { + return $this->getObjectName(); } /** diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php index 04ae32b..3b6236d 100644 --- a/library/Director/Objects/IcingaObject.php +++ b/library/Director/Objects/IcingaObject.php @@ -27,6 +27,7 @@ use RuntimeException; abstract class IcingaObject extends DbObject implements IcingaConfigRenderer { const RESOLVE_ERROR = '(unable to resolve)'; + const ALL_NON_GLOBAL_ZONES = '(all non-global zones)'; protected $keyName = 'object_name'; @@ -63,9 +64,6 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer protected $type; - /* key/value!! */ - protected $booleans = []; - // Property suffixed with _id must exist protected $relations = [ // property => PropertyClass @@ -142,11 +140,6 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer return $this->connection; } - public function propertyIsBoolean($property) - { - return array_key_exists($property, $this->booleans); - } - public function propertyIsInterval($property) { return array_key_exists($property, $this->intervalProperties); @@ -771,10 +764,6 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer return $this; } - if ($this->propertyIsBoolean($key)) { - return parent::set($key, DbDataFormatter::normalizeBoolean($value)); - } - // e.g. zone_id if ($this->propertyIsRelation($key)) { return $this->setRelation($key, $value); @@ -906,20 +895,20 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer $type = strtolower($this->getType()); $query = $this->db->select()->from( - ['gr' => "icinga_${type}group_${type}_resolved"], + ['gr' => "icinga_{$type}group_{$type}_resolved"], ['g.object_name'] )->join( - ['g' => "icinga_${type}group"], - "g.id = gr.${type}group_id", + ['g' => "icinga_{$type}group"], + "g.id = gr.{$type}group_id", [] )->joinLeft( - ['go' => "icinga_${type}group_${type}"], - "go.${type}group_id = gr.${type}group_id AND go.${type}_id = " . (int) $id, + ['go' => "icinga_{$type}group_{$type}"], + "go.{$type}group_id = gr.{$type}group_id AND go.{$type}_id = " . (int) $id, [] )->where( - "gr.${type}_id = ?", + "gr.{$type}_id = ?", (int) $id - )->where("go.${type}_id IS NULL")->order('g.object_name'); + )->where("go.{$type}_id IS NULL")->order('g.object_name'); return $this->db->fetchCol($query); } @@ -1812,9 +1801,21 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer return; } - $config->configFile( - 'zones.d/' . $this->getRenderingZone($config) . '/' . $this->getRenderingFilename() - )->addObject($this); + foreach ($this->getRenderingZones($config) as $zone) { + $config->configFile( + 'zones.d/' . $zone . '/' . $this->getRenderingFilename() + )->addObject($this); + } + } + + protected function getRenderingZones(IcingaConfig $config): array + { + $zone = $this->getRenderingZone($config); + if ($zone === self::ALL_NON_GLOBAL_ZONES) { + return $config->listNonGlobalZones(); + } + + return [$zone]; } public function getRenderingFilename() @@ -2193,7 +2194,12 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer protected function renderSuffix() { - return "}\n\n"; + $prefix = ''; + if ($this->rendersConditionalTemplate()) { + $prefix = '} '; + } + + return "$prefix}\n\n"; } protected function renderLegacySuffix() @@ -2418,14 +2424,25 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer protected function renderObjectHeader() { + $prefix = ''; + $renderedName = c::renderString($this->getObjectName()); + if ($this->rendersConditionalTemplate()) { + $prefix = sprintf('if (! get_template(%s, %s)) { ', $this->getType(), $renderedName); + } return sprintf( - "%s %s %s {\n", + "%s%s %s %s {\n", + $prefix, $this->getObjectTypeName(), $this->getType(), - c::renderString($this->getObjectName()) + $renderedName ); } + protected function rendersConditionalTemplate(): bool + { + return false; + } + public function getLegacyObjectType() { return strtolower($this->getType()); @@ -2674,16 +2691,16 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer /** @var DbObject $class */ $class = DbObjectTypeRegistry::classByType($type); + if ($keyColumn === null && is_array($class::create()->getKeyName())) { + return $class::loadAll($db, $query); + } + if ($keyColumn === null) { if (method_exists($class, 'getKeyColumnName')) { $keyColumn = $class::getKeyColumnName(); } } - if (is_array($class::create()->getKeyName())) { - return $class::loadAll($db, $query); - } - if (PrefetchCache::shouldBeUsed() && $query === null && $keyColumn === static::getKeyColumnName() @@ -2890,11 +2907,14 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer } } } + if ($this->propertyIsInterval($k) && is_string($v) && ctype_digit($v)) { + $v = (int) $v; + } // TODO: Do not ship null properties based on flag? if (!$skipDefaults || $this->differsFromDefaultValue($k, $v)) { if ($k === 'disabled' || $this->propertyIsBoolean($k)) { - $props[$k] = $this->booleanForDbValue($v); + $props[$k] = DbDataFormatter::booleanForDbValue($v); } else { $props[$k] = $v; } @@ -3005,18 +3025,6 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer return (object) $props; } - protected function booleanForDbValue($value) - { - if ($value === 'y') { - return true; - } - if ($value === 'n') { - return false; - } - - return $value; // let this fail elsewhere, if not null - } - public function listImportNames() { if ($this->gotImports()) { @@ -3161,7 +3169,7 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer if ($this->differsFromDefaultValue($k, $v)) { if ($k === 'disabled' || $this->propertyIsBoolean($k)) { - $props[$k] = $this->booleanForDbValue($v); + $props[$k] = DbDataFormatter::booleanForDbValue($v); } else { $props[$k] = $v; } diff --git a/library/Director/Objects/IcingaObjectGroup.php b/library/Director/Objects/IcingaObjectGroup.php index c0bec54..c076f52 100644 --- a/library/Director/Objects/IcingaObjectGroup.php +++ b/library/Director/Objects/IcingaObjectGroup.php @@ -2,9 +2,7 @@ namespace Icinga\Module\Director\Objects; -use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; abstract class IcingaObjectGroup extends IcingaObject implements ExportInterface { @@ -24,53 +22,51 @@ abstract class IcingaObjectGroup extends IcingaObject implements ExportInterface 'assign_filter' => null, ]; + protected $memberShipShouldBeRefreshed = false; + public function getUniqueIdentifier() { return $this->getObjectName(); } - /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() + protected function prefersGlobalZone() { - return $this->toPlainObject(); + return true; } - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaObjectGroup - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) + protected function beforeStore() { - $properties = (array) $plain; - $name = $properties['object_name']; - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Group "%s" already exists', - $name - ); + parent::beforeStore(); + if ($this->hasBeenLoadedFromDb()) { + if (!array_key_exists('assign_filter', $this->getModifiedProperties())) { + $this->memberShipShouldBeRefreshed = false; + return; + } } else { - $object = static::create([], $db); + if ($this->hasProperty('assign_filter') && $this->get('assign_filter') === null) { + $this->memberShipShouldBeRefreshed = false; + return; + } } - $object->setProperties($properties); + if ($this->hasProperty('assign_filter')) { + $this->memberShipShouldBeRefreshed = true; + } + } - return $object; + protected function notifyResolvers() + { + if ($this->memberShipShouldBeRefreshed) { + $resolver = $this->getMemberShipResolver(); + $resolver->addGroup($this); + $resolver->refreshDb(); + } + + return $this; } - protected function prefersGlobalZone() + protected function getMemberShipResolver() { - return true; + return null; } } diff --git a/library/Director/Objects/IcingaObjectGroups.php b/library/Director/Objects/IcingaObjectGroups.php index 8bef1b1..8683c77 100644 --- a/library/Director/Objects/IcingaObjectGroups.php +++ b/library/Director/Objects/IcingaObjectGroups.php @@ -183,6 +183,10 @@ class IcingaObjectGroups implements Iterator, Countable, IcingaConfigRenderer return $this; } + if (is_int($group)) { + $group = (string) $group; + } + /** @var IcingaObjectGroup $class */ $class = $this->getGroupClass(); @@ -224,7 +228,7 @@ class IcingaObjectGroups implements Iterator, Countable, IcingaConfigRenderer } else { throw new RuntimeException( 'Invalid group object: %s', - var_export($group, 1) + var_export($group, true) ); } diff --git a/library/Director/Objects/IcingaObjectLegacyAssignments.php b/library/Director/Objects/IcingaObjectLegacyAssignments.php index 6ab75c8..2db29b4 100644 --- a/library/Director/Objects/IcingaObjectLegacyAssignments.php +++ b/library/Director/Objects/IcingaObjectLegacyAssignments.php @@ -36,7 +36,7 @@ class IcingaObjectLegacyAssignments $assigns = array(); $ignores = array(); foreach ($values as $type => $value) { - if (strpos($value, '|') !== false || strpos($value, '&' !== false)) { + if (strpos($value, '|') !== false || strpos($value, '&') !== false) { $value = '(' . $value . ')'; } diff --git a/library/Director/Objects/IcingaObjectMultiRelations.php b/library/Director/Objects/IcingaObjectMultiRelations.php index a1ec9a2..5931595 100644 --- a/library/Director/Objects/IcingaObjectMultiRelations.php +++ b/library/Director/Objects/IcingaObjectMultiRelations.php @@ -239,7 +239,7 @@ class IcingaObjectMultiRelations implements Iterator, Countable, IcingaConfigRen } else { throw new ProgrammingError( 'Invalid related object: %s', - var_export($relation, 1) + var_export($relation, true) ); } diff --git a/library/Director/Objects/IcingaRelatedObject.php b/library/Director/Objects/IcingaRelatedObject.php index d35bcb0..ca33688 100644 --- a/library/Director/Objects/IcingaRelatedObject.php +++ b/library/Director/Objects/IcingaRelatedObject.php @@ -180,7 +180,7 @@ class IcingaRelatedObject } else { throw new ProgrammingError( 'Related object can be name or object, got: %s', - var_export($related, 1) + var_export($related, true) ); } diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php index 9479ef7..cbf9ca5 100644 --- a/library/Director/Objects/IcingaService.php +++ b/library/Director/Objects/IcingaService.php @@ -8,7 +8,6 @@ use Icinga\Module\Director\Data\PropertiesFilter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Cache\PrefetchCache; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfig; use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; @@ -170,94 +169,6 @@ class IcingaService extends IcingaObject implements ExportInterface } /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() - { - // TODO: ksort in toPlainObject? - $props = (array) $this->toPlainObject(); - $props['fields'] = $this->loadFieldReferences(); - ksort($props); - - return (object) $props; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaService - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['object_name']; - if ($properties['object_type'] !== 'template') { - throw new InvalidArgumentException(sprintf( - 'Can import only Templates, got "%s" for "%s"', - $properties['object_type'], - $name - )); - } - $key = [ - 'object_type' => 'template', - 'object_name' => $name - ]; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Service Template "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - // $object->newFields = $properties['fields']; - unset($properties['fields']); - $object->setProperties($properties); - - return $object; - } - - /** - * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader - * @return array - */ - protected function loadFieldReferences() - { - $db = $this->getDb(); - - $res = $db->fetchAll( - $db->select()->from([ - 'sf' => 'icinga_service_field' - ], [ - 'sf.datafield_id', - 'sf.is_required', - 'sf.var_filter', - ])->join(['df' => 'director_datafield'], 'df.id = sf.datafield_id', []) - ->where('service_id = ?', $this->get('id')) - ->order('varname ASC') - ); - - if (empty($res)) { - return []; - } else { - foreach ($res as $field) { - $field->datafield_id = (int) $field->datafield_id; - } - - return $res; - } - } - - /** * @param string $key * @return $this */ @@ -473,6 +384,11 @@ class IcingaService extends IcingaObject implements ExportInterface } } + protected function rendersConditionalTemplate(): bool + { + return $this->getRenderingZone() === self::ALL_NON_GLOBAL_ZONES; + } + /** * @return bool */ @@ -667,11 +583,22 @@ class IcingaService extends IcingaObject implements ExportInterface protected function getDefaultZone(IcingaConfig $config = null) { + // Hint: this isn't possible yet, as we're unable to render dependent apply rules to multiple zones as well + // if ($this->isTemplate()) { + // return self::ALL_NON_GLOBAL_ZONES; + // } if ($this->get('host_id') === null) { return parent::getDefaultZone(); } else { - return $this->getRelatedObject('host', $this->get('host_id')) + $zone = $this->getRelatedObject('host', $this->get('host_id')) ->getRenderingZone($config); + + // Hint: this avoids problems with host templates rendered to all non-global zones + if ($zone === self::ALL_NON_GLOBAL_ZONES) { + $zone = $this->connection->getDefaultGlobalZoneName(); + } + + return $zone; } } diff --git a/library/Director/Objects/IcingaServiceGroup.php b/library/Director/Objects/IcingaServiceGroup.php index ae43ff3..92fb1cb 100644 --- a/library/Director/Objects/IcingaServiceGroup.php +++ b/library/Director/Objects/IcingaServiceGroup.php @@ -31,12 +31,8 @@ class IcingaServiceGroup extends IcingaObjectGroup return $this; } - protected function notifyResolvers() + protected function getMemberShipResolver() { - $resolver = $this->getServiceGroupMembershipResolver(); - $resolver->addGroup($this); - $resolver->refreshDb(); - - return $this; + return $this->getServiceGroupMembershipResolver(); } } diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index 8217a59..252c52a 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -5,16 +5,14 @@ namespace Icinga\Module\Director\Objects; use Exception; use Icinga\Data\Filter\Filter; use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder; -use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Cache\PrefetchCache; -use Icinga\Module\Director\Db\DbUtil; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfig; use Icinga\Module\Director\Resolver\HostServiceBlacklist; use InvalidArgumentException; use Ramsey\Uuid\Uuid; -use RuntimeException; +use stdClass; class IcingaServiceSet extends IcingaObject implements ExportInterface { @@ -46,6 +44,33 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface 'host' => 'IcingaHost', ); + /** @var IcingaService[] Cached services */ + protected $cachedServices = []; + + /** @var IcingaService[]|null */ + private $services; + + /** + * Set the services to be cached + * + * @param $services IcingaService[] + * @return void + */ + public function setCachedServices($services) + { + $this->cachedServices = $services; + } + + /** + * Get the cached services + * + * @return IcingaService[] + */ + public function getCachedServices() + { + return $this->cachedServices; + } + public function isDisabled() { return false; @@ -79,6 +104,61 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface } /** + * @param stdClass[] $services + * @return void + */ + public function setServices(array $services) + { + $existing = $this->getServices(); + $uuidMap = []; + foreach ($existing as $service) { + $uuidMap[$service->getUniqueId()->getBytes()] = $service; + } + $this->services = []; + foreach ($services as $service) { + if (isset($service->uuid)) { + $uuid = Uuid::fromString($service->uuid)->getBytes(); + $current = $uuidMap[$uuid] ?? IcingaService::create([], $this->connection); + } else { + if (! is_object($service)) { + var_dump($service); + exit; + } + $current = $existing[$service->object_name] ?? IcingaService::create([], $this->connection); + } + $current->setProperties((array) $service); + $this->services[] = $current; + } + } + + protected function storeRelatedServices() + { + if ($this->services === null) { + $cachedServices = $this->getCachedServices(); + if ($cachedServices) { + $this->services = $cachedServices; + } else { + return; + } + } + + $seen = []; + /** @var IcingaService $service */ + foreach ($this->services as $service) { + $seen[$service->getUniqueId()->getBytes()] = true; + $service->set('service_set_id', $this->get('id')); + $service->store(); + } + + foreach ($this->fetchServices() as $service) { + if (!isset($seen[$service->getUniqueId()->getBytes()])) { + $service->delete(); + } + } + } + + /** + * @deprecated * @return IcingaService[] * @throws \Icinga\Exception\NotFoundError */ @@ -126,129 +206,9 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface return $this->getObjectName(); } - /** - * @return object - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @throws \Icinga\Exception\NotFoundError - */ - public function export() - { - if ($this->get('host_id')) { - $result = $this->exportSetOnHost(); - } else { - $result = $this->exportTemplate(); - } - - unset($result->uuid); - return $result; - } - - protected function exportSetOnHost() - { - // TODO. - throw new RuntimeException('Not yet'); - } - - /** - * @return object - * @deprecated - * @throws \Icinga\Exception\NotFoundError - */ - protected function exportTemplate() - { - $props = $this->getProperties(); - unset($props['id'], $props['host_id']); - $props['services'] = []; - foreach ($this->getServiceObjects() as $serviceObject) { - $props['services'][$serviceObject->getObjectName()] = $serviceObject->export(); - } - ksort($props); - - return (object) $props; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaServiceSet - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['object_name']; - if (isset($properties['services'])) { - $services = $properties['services']; - unset($properties['services']); - } else { - $services = []; - } - - if ($properties['object_type'] !== 'template') { - throw new InvalidArgumentException(sprintf( - 'Can import only Templates, got "%s" for "%s"', - $properties['object_type'], - $name - )); - } - if ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::exists($name, $db)) { - throw new DuplicateKeyException( - 'Service Set "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - $object->setProperties($properties); - - // This is not how other imports work, but here we need an ID - if (! $object->hasBeenLoadedFromDb()) { - $object->store(); - } - - $setId = $object->get('id'); - $sQuery = $db->getDbAdapter()->select()->from( - ['s' => 'icinga_service'], - 's.*' - )->where('service_set_id = ?', $setId); - $existingServices = IcingaService::loadAll($db, $sQuery, 'object_name'); - $serviceNames = []; - foreach ($services as $service) { - if (isset($service->fields)) { - unset($service->fields); - } - $name = $service->object_name; - $serviceNames[] = $name; - if (isset($existingServices[$name])) { - $existing = $existingServices[$name]; - $existing->setProperties((array) $service); - $existing->set('service_set_id', $setId); - if ($existing->hasBeenModified()) { - $existing->store(); - } - } else { - $new = IcingaService::create((array) $service, $db); - $new->set('service_set_id', $setId); - $new->store(); - } - } - - foreach ($existingServices as $existing) { - if (!in_array($existing->getObjectName(), $serviceNames)) { - $existing->delete(); - } - } - - return $object; - } - public function beforeDelete() { + $this->setCachedServices($this->getServices()); // check if this is a template, or directly assigned to a host if ($this->get('host_id') === null) { // find all host sets and delete them @@ -295,8 +255,8 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface */ public function renderToConfig(IcingaConfig $config) { - // always print the header, so you have minimal info present - $file = $this->getConfigFileWithHeader($config); + $files = []; + $zone = $this->getRenderingZone($config) ; if ($this->get('assign_filter') === null && $this->isTemplate()) { return; @@ -334,7 +294,15 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface } $this->copyVarsToService($service); - $file->addObject($service); + foreach ($service->getRenderingZones($config) as $serviceZone) { + $file = $this->getConfigFileWithHeader($config, $serviceZone, $files); + $file->addObject($service); + } + } + + if (empty($files)) { + // always print the header, so you have minimal info present + $this->getConfigFileWithHeader($config, $zone, $files); } } @@ -355,14 +323,18 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface return $lookup->getBlacklistedHostnamesForService($service); } - protected function getConfigFileWithHeader(IcingaConfig $config) + protected function getConfigFileWithHeader(IcingaConfig $config, $zone, &$files = []) { - $file = $config->configFile( - 'zones.d/' . $this->getRenderingZone($config) . '/servicesets' - ); + if (!isset($files[$zone])) { + $file = $config->configFile( + 'zones.d/' . $zone . '/servicesets' + ); - $file->addContent($this->getConfigHeaderComment($config)); - return $file; + $file->addContent($this->getConfigHeaderComment($config)); + $files[$zone] = $file; + } + + return $files[$zone]; } protected function getConfigHeaderComment(IcingaConfig $config) @@ -372,13 +344,13 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface if ($config->isLegacy()) { if ($assign !== null) { - return "## applied Service Set '${name}'\n\n"; + return "## applied Service Set '{$name}'\n\n"; } else { - return "## Service Set '${name}' on this host\n\n"; + return "## Service Set '{$name}' on this host\n\n"; } } else { $comment = [ - "Service Set: ${name}", + "Service Set: {$name}", ]; if (($host = $this->get('host')) !== null) { @@ -505,7 +477,23 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface /** * @return IcingaService[] */ - public function fetchServices() + public function getServices(): array + { + if ($this->services !== null) { + return $this->services; + } + + if ($this->hasBeenLoadedFromDb()) { + return $this->fetchServices(); + } + + return []; + } + + /** + * @return IcingaService[] + */ + public function fetchServices(): array { if ($store = self::$dbObjectStore) { $uuid = $store->getBranch()->getUuid(); @@ -569,6 +557,24 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface } } + public function onStore() + { + $this->storeRelatedServices(); + } + + public function hasBeenModified() + { + if ($this->services !== null) { + foreach ($this->services as $service) { + if ($service->hasBeenModified()) { + return true; + } + } + } + + return parent::hasBeenModified(); + } + public function toSingleIcingaConfig() { $config = parent::toSingleIcingaConfig(); diff --git a/library/Director/Objects/IcingaTemplateChoice.php b/library/Director/Objects/IcingaTemplateChoice.php index 1a1be90..a2be07a 100644 --- a/library/Director/Objects/IcingaTemplateChoice.php +++ b/library/Director/Objects/IcingaTemplateChoice.php @@ -3,9 +3,7 @@ namespace Icinga\Module\Director\Objects; use Icinga\Exception\ProgrammingError; -use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\Web\Form\QuickForm; class IcingaTemplateChoice extends IcingaObject implements ExportInterface @@ -36,63 +34,6 @@ class IcingaTemplateChoice extends IcingaObject implements ExportInterface return $this->getObjectName(); } - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaTemplateChoice - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - if (isset($properties['originalId'])) { - unset($properties['originalId']); - } - $name = $properties['object_name']; - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Template Choice "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - $object->setProperties($properties); - - return $object; - } - - /** - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @return array|object|\stdClass - */ - public function export() - { - $plain = (object) $this->getProperties(); - $plain->originalId = $plain->id; - unset($plain->id); - $requiredId = $plain->required_template_id; - unset($plain->required_template_id); - if ($requiredId) { - $db = $this->getDb(); - $query = $db->select() - ->from(['o' => $this->getObjectTableName()], 'o.object_name')->where("o.object_type = 'template'") - ->where('o.id = ?', $this->get('id')); - $plain->required_template = $db->fetchOne($query); - } - - $plain->members = array_values($this->getMembers()); - - return $plain; - } - public function isMainChoice() { return $this->hasBeenLoadedFromDb() @@ -155,7 +96,7 @@ class IcingaTemplateChoice extends IcingaObject implements ExportInterface public function hasBeenModified() { - if ($this->newChoices !== null && $this->choices !== $this->newChoices) { + if ($this->newChoices !== null && ($this->choices ?? $this->fetchChoices()) !== $this->newChoices) { return true; } @@ -284,7 +225,7 @@ class IcingaTemplateChoice extends IcingaObject implements ExportInterface } else { throw new ProgrammingError( 'Expected array or null for allowed_roles, got %s', - var_export($roles, 1) + var_export($roles, true) ); } } diff --git a/library/Director/Objects/IcingaTemplateResolver.php b/library/Director/Objects/IcingaTemplateResolver.php index 61122a0..7f14967 100644 --- a/library/Director/Objects/IcingaTemplateResolver.php +++ b/library/Director/Objects/IcingaTemplateResolver.php @@ -45,7 +45,6 @@ class IcingaTemplateResolver { $this->object = $object; $this->type = $object->getShortTableName(); - $this->table = $object->getTableName(); $this->connection = $object->getConnection(); $this->db = $this->connection->getDbAdapter(); diff --git a/library/Director/Objects/IcingaTimePeriod.php b/library/Director/Objects/IcingaTimePeriod.php index 1232366..081cdd9 100644 --- a/library/Director/Objects/IcingaTimePeriod.php +++ b/library/Director/Objects/IcingaTimePeriod.php @@ -2,9 +2,7 @@ namespace Icinga\Module\Director\Objects; -use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; -use Icinga\Module\Director\Exception\DuplicateKeyException; class IcingaTimePeriod extends IcingaObject implements ExportInterface { @@ -56,48 +54,6 @@ class IcingaTimePeriod extends IcingaObject implements ExportInterface } /** - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @return object - * @throws \Icinga\Exception\NotFoundError - */ - public function export() - { - $props = (array) $this->toPlainObject(); - ksort($props); - - return (object) $props; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['object_name']; - $key = $name; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Time Period "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - $object->setProperties($properties); - - return $object; - } - - /** * Render update property * * Avoid complaints for method names with underscore: diff --git a/library/Director/Objects/IcingaUser.php b/library/Director/Objects/IcingaUser.php index 394e849..4100245 100644 --- a/library/Director/Objects/IcingaUser.php +++ b/library/Director/Objects/IcingaUser.php @@ -48,43 +48,6 @@ class IcingaUser extends IcingaObject implements ExportInterface 'zone' => 'IcingaZone', ); - public function export() - { - return ImportExportHelper::simpleExport($this); - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return IcingaUser - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $key = $properties['object_name']; - - if ($replace && static::exists($key, $db)) { - $object = static::load($key, $db); - } elseif (static::exists($key, $db)) { - throw new DuplicateKeyException( - 'Cannot import, %s "%s" already exists', - static::create([])->getShortTableName(), - $key - ); - } else { - $object = static::create([], $db); - } - - // $object->newFields = $properties['fields']; - unset($properties['fields']); - $object->setProperties($properties); - - return $object; - } - public function getUniqueIdentifier() { return $this->getObjectName(); diff --git a/library/Director/Objects/ImportExportHelper.php b/library/Director/Objects/ImportExportHelper.php deleted file mode 100644 index 98d34c6..0000000 --- a/library/Director/Objects/ImportExportHelper.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php - -namespace Icinga\Module\Director\Objects; - -use Icinga\Module\Director\Db; - -/** - * Helper class, allows to reduce duplicate code. Might be moved elsewhere - * afterwards - */ -class ImportExportHelper -{ - /** - * Does not support every type out of the box - * - * @param IcingaObject $object - * @return object - * @throws \Icinga\Exception\NotFoundError - */ - public static function simpleExport(IcingaObject $object) - { - $props = (array) $object->toPlainObject(); - $props['fields'] = static::fetchFields($object); - ksort($props); // TODO: ksort in toPlainObject? - - return (object) $props; - } - - public static function fetchFields(IcingaObject $object) - { - return static::loadFieldReferences( - $object->getConnection(), - $object->getShortTableName(), - $object->get('id') - ); - } - - /** - * @param Db $connection - * @param string $type Warning: this will not be validated. - * @param int $id - * @return array - */ - public static function loadFieldReferences(Db $connection, $type, $id) - { - $db = $connection->getDbAdapter(); - $res = $db->fetchAll( - $db->select()->from([ - 'f' => "icinga_${type}_field" - ], [ - 'f.datafield_id', - 'f.is_required', - 'f.var_filter', - ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', []) - ->where("${type}_id = ?", $id) - ->order('varname ASC') - ); - - if (empty($res)) { - return []; - } - - foreach ($res as $field) { - $field->datafield_id = (int) $field->datafield_id; - } - return $res; - } -} diff --git a/library/Director/Objects/ImportRowModifier.php b/library/Director/Objects/ImportRowModifier.php index 76982c2..afebbad 100644 --- a/library/Director/Objects/ImportRowModifier.php +++ b/library/Director/Objects/ImportRowModifier.php @@ -18,13 +18,14 @@ class ImportRowModifier extends DbObjectWithSettings implements InstantiatedViaH protected $autoincKeyName = 'id'; protected $defaultProperties = [ - 'id' => null, - 'source_id' => null, - 'property_name' => null, - 'provider_class' => null, - 'target_property' => null, - 'priority' => null, - 'description' => null, + 'id' => null, + 'source_id' => null, + 'property_name' => null, + 'provider_class' => null, + 'target_property' => null, + 'filter_expression' => null, + 'priority' => null, + 'description' => null, ]; protected $settingsTable = 'import_row_modifier_setting'; diff --git a/library/Director/Objects/ImportSource.php b/library/Director/Objects/ImportSource.php index fd892ef..7477472 100644 --- a/library/Director/Objects/ImportSource.php +++ b/library/Director/Objects/ImportSource.php @@ -3,12 +3,14 @@ namespace Icinga\Module\Director\Objects; use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; use Icinga\Exception\NotFoundError; use Icinga\Module\Director\Application\MemoryLimit; use Icinga\Module\Director\Data\Db\DbObjectWithSettings; use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Filter\FilterEnrichment; use Icinga\Module\Director\Hook\PropertyModifierHook; use Icinga\Module\Director\Import\Import; use Icinga\Module\Director\Import\SyncUtils; @@ -52,68 +54,6 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface private $newRowModifiers; - /** - * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader - * @return \stdClass - */ - public function export() - { - $plain = $this->getProperties(); - $plain['originalId'] = $plain['id']; - unset($plain['id']); - - foreach ($this->stateProperties as $key) { - unset($plain[$key]); - } - - $plain['settings'] = (object) $this->getSettings(); - $plain['modifiers'] = $this->exportRowModifiers(); - ksort($plain); - - return (object) $plain; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return ImportSource - * @throws DuplicateKeyException - * @throws NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - if (isset($properties['originalId'])) { - $id = $properties['originalId']; - unset($properties['originalId']); - } else { - $id = null; - } - $name = $properties['source_name']; - - if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) { - $object = static::loadWithAutoIncId($id, $db); - } elseif ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::existsWithName($name, $db)) { - throw new DuplicateKeyException( - 'Import Source %s already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - if (! isset($properties['modifiers'])) { - $properties['modifiers'] = []; - } - - $object->setProperties($properties); - - return $object; - } - public function setModifiers(array $modifiers) { if ($this->loadedRowModifiers === null && $this->hasBeenLoadedFromDb()) { @@ -314,10 +254,14 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface foreach ($modifiers as $modPair) { /** @var PropertyModifierHook $modifier */ - list($property, $modifier) = $modPair; + /** @var ?Filter $filter */ + list($property, $modifier, $filter) = $modPair; $rejected = []; $newRows = []; foreach ($data as $key => $row) { + if ($filter && ! $filter->matches($row)) { + continue; + } $this->applyPropertyModifierToRow($modifier, $property, $row); if ($modifier->rejectsRow()) { $rejected[] = $key; @@ -434,7 +378,12 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface { $mods = []; foreach ($this->fetchRowModifiers() as $mod) { - $mods[] = [$mod->get('property_name'), $mod->getInstance()]; + if ($filterExpression = $mod->get('filter_expression')) { + $filter = FilterEnrichment::enrichFilter(Filter::fromQueryString($filterExpression)); + } else { + $filter = null; + } + $mods[] = [$mod->get('property_name'), $mod->getInstance(), $filter]; } return $mods; @@ -530,7 +479,7 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface protected function raiseLimits() { MemoryLimit::raiseTo('1024M'); - ini_set('max_execution_time', 0); + ini_set('max_execution_time', '0'); return $this; } diff --git a/library/Director/Objects/ObjectApplyMatches.php b/library/Director/Objects/ObjectApplyMatches.php index 018c880..d749d6a 100644 --- a/library/Director/Objects/ObjectApplyMatches.php +++ b/library/Director/Objects/ObjectApplyMatches.php @@ -172,7 +172,7 @@ abstract class ObjectApplyMatches $col = $filter->getColumn(); $type = static::$type; - if ($type && substr($col, 0, strlen($type) + 1) === "${type}.") { + if ($type && substr($col, 0, strlen($type) + 1) === "{$type}.") { $filter->setColumn($col = substr($col, strlen($type) + 1)); } diff --git a/library/Director/Objects/SyncRule.php b/library/Director/Objects/SyncRule.php index 89f7fd1..270a882 100644 --- a/library/Director/Objects/SyncRule.php +++ b/library/Director/Objects/SyncRule.php @@ -43,6 +43,10 @@ class SyncRule extends DbObject implements ExportInterface 'last_attempt', ]; + protected $booleans = [ + 'purge_existing' => 'purge_existing', + ]; + private $sync; private $purgeStrategy; @@ -60,8 +64,6 @@ class SyncRule extends DbObject implements ExportInterface private $newSyncProperties; - private $originalId; - public function listInvolvedSourceIds() { if (! $this->hasBeenLoadedFromDb()) { @@ -257,61 +259,13 @@ class SyncRule extends DbObject implements ExportInterface } /** - * @deprecated please use \Icinga\Module\Director\Data\Exporter - * @return object - */ - public function export() - { - $plain = $this->getProperties(); - $plain['originalId'] = $plain['id']; - unset($plain['id']); - - foreach ($this->stateProperties as $key) { - unset($plain[$key]); - } - $plain['properties'] = $this->exportSyncProperties(); - ksort($plain); - - return (object) $plain; - } - - /** - * @param object $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError + * Flat object has 'properties', but setProperties() is not available in DbObject + * + * @return void */ - public static function import($plain, Db $db, $replace = false) + public function setSyncProperties(?array $value) { - $properties = (array) $plain; - if (isset($properties['originalId'])) { - $id = $properties['originalId']; - unset($properties['originalId']); - } else { - $id = null; - } - $name = $properties['rule_name']; - - if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) { - $object = static::loadWithAutoIncId($id, $db); - } elseif ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::existsWithName($name, $db)) { - throw new DuplicateKeyException( - 'Sync Rule %s already exists', - $name - ); - } else { - $object = static::create([], $db); - } - - $object->newSyncProperties = $properties['properties']; - unset($properties['properties']); - $object->setProperties($properties); - - return $object; + $this->newSyncProperties = $value; } public function getUniqueIdentifier() @@ -329,12 +283,6 @@ class SyncRule extends DbObject implements ExportInterface $connection = $this->getConnection(); $db = $connection->getDbAdapter(); $myId = $this->get('id'); - if ($this->originalId === null) { - $originalId = $myId; - } else { - $originalId = $this->originalId; - $this->originalId = null; - } if ($this->hasBeenLoadedFromDb()) { $db->delete( 'sync_property', diff --git a/library/Director/PlainObjectRenderer.php b/library/Director/PlainObjectRenderer.php index 4dadf4f..e613f1f 100644 --- a/library/Director/PlainObjectRenderer.php +++ b/library/Director/PlainObjectRenderer.php @@ -105,7 +105,7 @@ class PlainObjectRenderer } elseif (is_string($object)) { return self::renderString($object); } else { - return '(UNKNOWN TYPE) ' . var_export($object, 1); + return '(UNKNOWN TYPE) ' . var_export($object, true); } } diff --git a/library/Director/PropertyModifier/PropertyModifierArrayFilter.php b/library/Director/PropertyModifier/PropertyModifierArrayFilter.php index 0b52987..a8fcbf7 100644 --- a/library/Director/PropertyModifier/PropertyModifierArrayFilter.php +++ b/library/Director/PropertyModifier/PropertyModifierArrayFilter.php @@ -116,7 +116,7 @@ class PropertyModifierArrayFilter extends PropertyModifierHook default: throw new ConfigurationError( '%s is not a valid value for an ArrayFilter filter_method', - var_export($method, 1) + var_export($method, true) ); } diff --git a/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php b/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php index c79c5b2..6b0651d 100644 --- a/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php +++ b/library/Director/PropertyModifier/PropertyModifierExtractFromDN.php @@ -74,7 +74,7 @@ class PropertyModifierExtractFromDN extends PropertyModifierHook default: throw new InvalidPropertyException( 'DN part extraction failed for %s', - var_export($value, 1) + var_export($value, true) ); } } diff --git a/library/Director/PropertyModifier/PropertyModifierGetHostByName.php b/library/Director/PropertyModifier/PropertyModifierGetHostByName.php index 36884e8..d7921de 100644 --- a/library/Director/PropertyModifier/PropertyModifierGetHostByName.php +++ b/library/Director/PropertyModifier/PropertyModifierGetHostByName.php @@ -34,7 +34,8 @@ class PropertyModifierGetHostByName extends PropertyModifierHook } $host = gethostbyname($value); - if (strlen(@inet_pton($host)) !== 4) { + $inAddr = inet_pton($host); + if (! $inAddr || strlen($inAddr) !== 4) { switch ($this->getSetting('on_failure')) { case 'null': return null; diff --git a/library/Director/PropertyModifier/PropertyModifierJsonDecode.php b/library/Director/PropertyModifier/PropertyModifierJsonDecode.php index f6b9af8..4ab119a 100644 --- a/library/Director/PropertyModifier/PropertyModifierJsonDecode.php +++ b/library/Director/PropertyModifier/PropertyModifierJsonDecode.php @@ -2,8 +2,9 @@ namespace Icinga\Module\Director\PropertyModifier; +use Exception; +use gipfl\Json\JsonString; use Icinga\Exception\InvalidPropertyException; -use Icinga\Module\Director\Exception\JsonException; use Icinga\Module\Director\Hook\PropertyModifierHook; use Icinga\Module\Director\Web\Form\QuickForm; @@ -38,16 +39,22 @@ class PropertyModifierJsonDecode extends PropertyModifierHook /** * @param $value * @return mixed|null - * @throws InvalidPropertyException + * @throws InvalidPropertyException|\gipfl\Json\JsonDecodeException */ public function transform($value) { if (null === $value) { - return $value; + return null; } - - $decoded = @json_decode($value); - if ($decoded === null && JSON_ERROR_NONE !== json_last_error()) { + try { + if (is_string($value)) { + $decoded = JsonString::decode($value); + } else { + throw new InvalidPropertyException( + 'JSON decode expects a string, got ' . gettype($value) + ); + } + } catch (Exception $e) { switch ($this->getSetting('on_failure')) { case 'null': return null; @@ -55,11 +62,7 @@ class PropertyModifierJsonDecode extends PropertyModifierHook return $value; case 'fail': default: - throw new InvalidPropertyException( - 'JSON decoding failed with "%s" for %s', - JsonException::getJsonErrorMessage(json_last_error()), - substr($value, 0, 128) - ); + throw $e; } } diff --git a/library/Director/PropertyModifier/PropertyModifierMap.php b/library/Director/PropertyModifier/PropertyModifierMap.php index a6cb422..e411c54 100644 --- a/library/Director/PropertyModifier/PropertyModifierMap.php +++ b/library/Director/PropertyModifier/PropertyModifierMap.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Director\PropertyModifier; use Icinga\Exception\InvalidPropertyException; use Icinga\Module\Director\Hook\PropertyModifierHook; +use Icinga\Module\Director\Import\SyncUtils; use Icinga\Module\Director\Web\Form\QuickForm; class PropertyModifierMap extends PropertyModifierHook @@ -30,15 +31,37 @@ class PropertyModifierMap extends PropertyModifierHook . ' or interrupt the import process' ), 'multiOptions' => $form->optionalEnum(array( - 'null' => $form->translate('Set null'), - 'keep' => $form->translate('Return lookup key unmodified'), - 'fail' => $form->translate('Let the import fail'), + 'null' => $form->translate('Set null'), + 'keep' => $form->translate('Return lookup key unmodified'), + 'custom' => $form->translate('Return custom default value'), + 'fail' => $form->translate('Let the import fail'), )), + 'class' => 'autosubmit', )); + $method = $form->getSetting('on_missing'); + if ($method == 'custom') { + $form->addElement('text', 'custom_value', array( + 'label' => $form->translate('Default value'), + 'required' => true, + 'description' => $form->translate( + 'This value will be evaluated, and variables like ${some_column}' + . ' will be filled accordingly. A typical use-case is generating' + . ' unique service identifiers via ${host}!${service} in case your' + . ' data source doesn\'t allow you to ship such. The chosen "property"' + . ' has no effect here and will be ignored.' + ) + )); + } + // TODO: ignore case } + public function requiresRow() + { + return true; + } + public function transform($value) { $this->loadCache(); @@ -53,6 +76,9 @@ class PropertyModifierMap extends PropertyModifierHook case 'keep': return $value; + case 'custom': + return SyncUtils::fillVariables($this->getSetting('custom_value'), $this->getRow()); + case 'fail': default: throw new InvalidPropertyException( diff --git a/library/Director/PropertyModifier/PropertyModifierRegexReplace.php b/library/Director/PropertyModifier/PropertyModifierRegexReplace.php index 59cb245..eeae69e 100644 --- a/library/Director/PropertyModifier/PropertyModifierRegexReplace.php +++ b/library/Director/PropertyModifier/PropertyModifierRegexReplace.php @@ -9,25 +9,36 @@ class PropertyModifierRegexReplace extends PropertyModifierHook { public static function addSettingsFormFields(QuickForm $form) { - $form->addElement('text', 'pattern', array( - 'label' => 'Regex pattern', + $form->addElement('text', 'pattern', [ + 'label' => $form->translate('Regex pattern'), 'description' => $form->translate( 'The pattern you want to search for. This can be a regular expression like /^www\d+\./' ), 'required' => true, - )); + ]); - $form->addElement('text', 'replacement', array( - 'label' => 'Replacement', + $form->addElement('text', 'replacement', [ + 'label' => $form->translate('Replacement'), 'description' => $form->translate( - 'The string that should be used as a preplacement' + 'The string that should be used as a replacement' ), - )); + ]); + $form->addElement('select', 'when_not_matched', [ + 'label' => $form->translate('When not matched'), + 'description' => $form->translate( + "What should happen, if the given pattern doesn't match" + ), + 'value' => 'keep', + 'multiOptions' => [ + 'keep' => $form->translate('Keep the given string'), + 'set_null' => $form->translate('Set the value to NULL') + ] + ]); } public function getName() { - return 'Regular expression based replacement'; + return mt('director', 'Regular expression based replacement'); } public function transform($value) @@ -36,10 +47,13 @@ class PropertyModifierRegexReplace extends PropertyModifierHook return null; } - return preg_replace( - $this->getSetting('pattern'), - $this->getSetting('replacement'), - $value - ); + $result = preg_replace($this->getSetting('pattern'), $this->getSetting('replacement'), $value); + if ($result === $value && $this->getSetting('when_not_matched', 'keep') === 'set_null') { + if (!preg_match($this->getSetting('pattern'), $value)) { + return null; + } + } + + return $result; } } diff --git a/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php b/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php index 1485d5d..04c49c5 100644 --- a/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php +++ b/library/Director/PropertyModifier/PropertyModifierRejectOrSelect.php @@ -128,7 +128,7 @@ class PropertyModifierRejectOrSelect extends PropertyModifierHook default: throw new ConfigurationError( '%s is not a valid value for an ArrayFilter filter_method', - var_export($method, 1) + var_export($method, true) ); } diff --git a/library/Director/PropertyModifier/PropertyModifierSplit.php b/library/Director/PropertyModifier/PropertyModifierSplit.php index 4a6fef6..3eec77d 100644 --- a/library/Director/PropertyModifier/PropertyModifierSplit.php +++ b/library/Director/PropertyModifier/PropertyModifierSplit.php @@ -33,7 +33,7 @@ class PropertyModifierSplit extends PropertyModifierHook public function transform($value) { - if (! strlen(trim($value))) { + if ($value === null || ! strlen(trim($value))) { if ($this->getSetting('when_empty', 'empty_array') === 'empty_array') { return array(); } else { diff --git a/library/Director/ProvidedHook/IcingaDbCubeLinks.php b/library/Director/ProvidedHook/IcingaDbCubeLinks.php index 234f61f..f3fe402 100644 --- a/library/Director/ProvidedHook/IcingaDbCubeLinks.php +++ b/library/Director/ProvidedHook/IcingaDbCubeLinks.php @@ -7,6 +7,7 @@ use Icinga\Exception\ProgrammingError; use Icinga\Module\Cube\Hook\IcingaDbActionsHook; use Icinga\Module\Cube\IcingaDb\IcingaDbCube; use Icinga\Module\Cube\IcingaDb\IcingaDbHostStatusCube; +use ipl\Stdlib\Filter\Condition; class IcingaDbCubeLinks extends IcingaDbActionsHook { @@ -25,17 +26,19 @@ class IcingaDbCubeLinks extends IcingaDbActionsHook if ($filterChain->count() === 1) { $url = 'director/host/edit?'; - $params = ['name' => $filterChain->getIterator()->current()->getValue()]; + /** @var Condition $rule */ + $rule = $filterChain->getIterator()->current(); + /** @var string $name */ + $name = $rule->getValue(); + $params = ['name' => $name]; $title = t('Modify a host'); - $description = sprintf( - t('This allows you to modify properties for "%s"'), - $filterChain->getIterator()->current()->getValue() - ); + $description = sprintf(t('This allows you to modify properties for "%s"'), $name); } else { $params = null; $urlFilter = Filter::matchAny(); + /** @var Condition $filter */ foreach ($filterChain as $filter) { $urlFilter->addFilter( Filter::matchAny( diff --git a/library/Director/ProvidedHook/Icingadb/HostActions.php b/library/Director/ProvidedHook/Icingadb/HostActions.php new file mode 100644 index 0000000..d7332ea --- /dev/null +++ b/library/Director/ProvidedHook/Icingadb/HostActions.php @@ -0,0 +1,78 @@ +<?php + +namespace Icinga\Module\Director\ProvidedHook\Icingadb; + +use Exception; +use Icinga\Application\Config; +use Icinga\Module\Director\Auth\Permission; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Integration\Icingadb\IcingadbBackend; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Util; +use Icinga\Module\Icingadb\Hook\HostActionsHook; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Web\Url; +use ipl\Web\Widget\Link; + +class HostActions extends HostActionsHook +{ + public function getActionsForObject(Host $host): array + { + try { + return $this->getThem($host); + } catch (Exception $e) { + return []; + } + } + + protected function getThem(Host $host): array + { + $actions = []; + $db = $this->db(); + if (! $db) { + return $actions; + } + $hostname = $host->name; + if (Util::hasPermission(Permission::INSPECT)) { + $actions[] = new Link( + mt('director', 'Inspect'), + Url::fromPath( + 'director/inspect/object', + ['type' => 'host', 'plural' => 'hosts', 'name' => $hostname] + ) + ); + } + + $allowEdit = false; + if (Util::hasPermission(Permission::HOSTS) && IcingaHost::exists($hostname, $db)) { + $allowEdit = true; + } + if (Util::hasPermission(Permission::ICINGADB_HOSTS)) { + if ((new IcingadbBackend())->canModifyHost($hostname)) { + $allowEdit = IcingaHost::exists($hostname, $db); + } + } + + if ($allowEdit) { + $label = mt('director', 'Modify'); + $actions[] = new Link( + $label, + Url::fromPath('director/host/edit', [ + 'name' => $hostname + ]) + ); + } + + return $actions; + } + + protected function db() + { + $resourceName = Config::module('director')->get('db', 'resource'); + if (! $resourceName) { + return false; + } + + return Db::fromResourceName($resourceName); + } +} diff --git a/library/Director/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Director/ProvidedHook/Icingadb/IcingadbSupport.php new file mode 100644 index 0000000..5a59304 --- /dev/null +++ b/library/Director/ProvidedHook/Icingadb/IcingadbSupport.php @@ -0,0 +1,10 @@ +<?php + +namespace Icinga\Module\Director\ProvidedHook\Icingadb; + +use Icinga\Module\Icingadb\Hook\IcingadbSupportHook; + +class IcingadbSupport extends IcingadbSupportHook +{ + +} diff --git a/library/Director/ProvidedHook/Icingadb/ServiceActions.php b/library/Director/ProvidedHook/Icingadb/ServiceActions.php new file mode 100644 index 0000000..1603dc3 --- /dev/null +++ b/library/Director/ProvidedHook/Icingadb/ServiceActions.php @@ -0,0 +1,87 @@ +<?php + +namespace Icinga\Module\Director\ProvidedHook\Icingadb; + +use Exception; +use Icinga\Application\Config; +use Icinga\Module\Director\Auth\Permission; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Integration\Icingadb\IcingadbBackend; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Util; +use Icinga\Module\Icingadb\Hook\ServiceActionsHook; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Web\Url; +use ipl\Web\Widget\Link; + +class ServiceActions extends ServiceActionsHook +{ + public function getActionsForObject(Service $service): array + { + try { + return $this->getThem($service); + } catch (Exception $e) { + return []; + } + } + + /** + * @param Service $service + * @return array + * @throws \Icinga\Exception\ProgrammingError + */ + protected function getThem(Service $service) + { + $actions = []; + $db = $this->db(); + if (! $db) { + return []; + } + + $hostname = $service->host->name; + $serviceName = $service->name; + if (Util::hasPermission(Permission::INSPECT)) { + $actions[] = new Link( + mt('director', 'Inspect'), + Url::fromPath('director/inspect/object', [ + 'type' => 'service', + 'plural' => 'services', + 'name' => sprintf('%s!%s', $hostname, $serviceName) + ]) + ); + } + + $title = null; + if (Util::hasPermission(Permission::HOSTS)) { + $title = mt('director', 'Modify'); + } elseif (Util::hasPermission(Permission::ICINGADB_SERVICES)) { + if ((new IcingadbBackend())->canModifyService($hostname, $serviceName)) { + $title = mt('director', 'Modify'); + } + } elseif (Util::hasPermission(Permission::ICINGADB_SERVICES_RO)) { + $title = mt('director', 'Configuration'); + } + + if ($title && IcingaHost::exists($hostname, $db)) { + $actions[] = new Link( + $title, + Url::fromPath('director/host/findservice', [ + 'name' => $hostname, + 'service' => $serviceName + ]) + ); + } + + return $actions; + } + + protected function db() + { + $resourceName = Config::module('director')->get('db', 'resource'); + if (! $resourceName) { + return false; + } + + return Db::fromResourceName($resourceName); + } +} diff --git a/library/Director/ProvidedHook/Monitoring/HostActions.php b/library/Director/ProvidedHook/Monitoring/HostActions.php index 2e3fba0..2d0469d 100644 --- a/library/Director/ProvidedHook/Monitoring/HostActions.php +++ b/library/Director/ProvidedHook/Monitoring/HostActions.php @@ -5,8 +5,9 @@ namespace Icinga\Module\Director\ProvidedHook\Monitoring; use Exception; use Icinga\Application\Config; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Db; -use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Integration\MonitoringModule\Monitoring; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Util; use Icinga\Module\Monitoring\Hook\HostActionsHook; @@ -32,7 +33,7 @@ class HostActions extends HostActionsHook return $actions; } $hostname = $host->host_name; - if (Util::hasPermission('director/inspect')) { + if (Util::hasPermission(Permission::INSPECT)) { $actions[mt('director', 'Inspect')] = Url::fromPath( 'director/inspect/object', array('type' => 'host', 'plural' => 'hosts', 'name' => $hostname) @@ -40,22 +41,17 @@ class HostActions extends HostActionsHook } $allowEdit = false; - if (Util::hasPermission('director/hosts') && IcingaHost::exists($hostname, $db)) { + if (Util::hasPermission(Permission::HOSTS) && IcingaHost::exists($hostname, $db)) { $allowEdit = true; } - $auth = Auth::getInstance(); - if (Util::hasPermission('director/monitoring/hosts')) { - $monitoring = new Monitoring(); - if ($monitoring->isAvailable() && $monitoring->authCanEditHost($auth, $hostname)) { + if (Util::hasPermission(Permission::MONITORING_HOSTS)) { + if ((new Monitoring(Auth::getInstance()))->canModifyHost($hostname)) { $allowEdit = IcingaHost::exists($hostname, $db); } } if ($allowEdit) { - $actions[mt('director', 'Modify')] = Url::fromPath( - 'director/host/edit', - array('name' => $hostname) - ); + $actions[mt('director', 'Modify')] = Url::fromPath('director/host/edit', ['name' => $hostname]); } return $actions; diff --git a/library/Director/ProvidedHook/Monitoring/ServiceActions.php b/library/Director/ProvidedHook/Monitoring/ServiceActions.php index b2e303a..834b166 100644 --- a/library/Director/ProvidedHook/Monitoring/ServiceActions.php +++ b/library/Director/ProvidedHook/Monitoring/ServiceActions.php @@ -5,8 +5,9 @@ namespace Icinga\Module\Director\ProvidedHook\Monitoring; use Exception; use Icinga\Application\Config; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Db; -use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Integration\MonitoringModule\Monitoring; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Util; use Icinga\Module\Monitoring\Hook\ServiceActionsHook; @@ -39,7 +40,7 @@ class ServiceActions extends ServiceActionsHook $hostname = $service->host_name; $serviceName = $service->service_description; - if (Util::hasPermission('director/inspect')) { + if (Util::hasPermission(Permission::INSPECT)) { $actions[mt('director', 'Inspect')] = Url::fromPath('director/inspect/object', [ 'type' => 'service', 'plural' => 'services', @@ -52,16 +53,13 @@ class ServiceActions extends ServiceActionsHook } $title = null; - if (Util::hasPermission('director/hosts')) { + if (Util::hasPermission(Permission::HOSTS)) { $title = mt('director', 'Modify'); - } elseif (Util::hasPermission('director/monitoring/services')) { - $monitoring = new Monitoring(); - if ($monitoring->isAvailable() - && $monitoring->authCanEditService(Auth::getInstance(), $hostname, $serviceName) - ) { + } elseif (Util::hasPermission(Permission::MONITORING_SERVICES)) { + if ((new Monitoring(Auth::getInstance()))->canModifyService($hostname, $serviceName)) { $title = mt('director', 'Modify'); } - } elseif (Util::hasPermission('director/monitoring/services-ro')) { + } elseif (Util::hasPermission(Permission::MONITORING_SERVICES_RO)) { $title = mt('director', 'Configuration'); } diff --git a/library/Director/Resolver/CommandUsage.php b/library/Director/Resolver/CommandUsage.php index 7e3e0c5..49fc31b 100644 --- a/library/Director/Resolver/CommandUsage.php +++ b/library/Director/Resolver/CommandUsage.php @@ -75,7 +75,7 @@ class CommandUsage $suffix = $urlSuffix[$objectType]; $links[] = Link::create( sprintf($caption, $res->$objectType), - "director/${type}s$suffix", + "director/{$type}s$suffix", ['command' => $name] ); } @@ -96,7 +96,7 @@ class CommandUsage $query = $this->db->select()->from("icinga_$table", $columns); foreach ($rels as $rel) { - $query->orWhere("${rel}_id = ?", $id); + $query->orWhere("{$rel}_id = ?", $id); } return $this->db->fetchRow($query); diff --git a/library/Director/Resolver/IcingaObjectResolver.php b/library/Director/Resolver/IcingaObjectResolver.php index 540e2c2..c751476 100644 --- a/library/Director/Resolver/IcingaObjectResolver.php +++ b/library/Director/Resolver/IcingaObjectResolver.php @@ -176,7 +176,7 @@ class IcingaObjectResolver $object->groups = $groups; } - $templates = $this->getTemplateNamesById($id); + $templates = $this->getTemplateNamesByID($id); if (! empty($templates)) { $object->templates = \array_reverse($templates); } @@ -198,7 +198,7 @@ class IcingaObjectResolver } $query = $this->db->select() ->from([ - 'oi' => "${baseTable}_inheritance" + 'oi' => "{$baseTable}_inheritance" ], [ $relColumn, $groupColumn @@ -494,12 +494,12 @@ class IcingaObjectResolver { $type = $this->getType(); $groupsTable = $this->baseTable . 'group'; - $groupMembershipTable = "${groupsTable}_$type"; + $groupMembershipTable = "{$groupsTable}_$type"; if ($resolved) { $groupMembershipTable .= '_resolved'; } - $oRef = "${type}_id"; - $gRef = "${type}group_id"; + $oRef = "{$type}_id"; + $gRef = "{$type}group_id"; return $this->db->select() ->from(['gm' => $groupMembershipTable], [ diff --git a/library/Director/Resolver/TemplateTree.php b/library/Director/Resolver/TemplateTree.php index f8d8fed..bf941e2 100644 --- a/library/Director/Resolver/TemplateTree.php +++ b/library/Director/Resolver/TemplateTree.php @@ -80,13 +80,13 @@ class TemplateTree $map = []; $db = $this->db; $type = $this->type; - $table = "icinga_${type}_inheritance"; + $table = "icinga_{$type}_inheritance"; $query = $db->select()->from( ['i' => $table], [ - 'object' => "i.${type}_id", - 'parent' => "i.parent_${type}_id", + 'object' => "i.{$type}_id", + 'parent' => "i.parent_{$type}_id", ] )->order('i.weight'); @@ -439,12 +439,12 @@ class TemplateTree if ($type === 'command') { $joinCondition = $db->quoteInto( - "p.id = i.parent_${type}_id", + "p.id = i.parent_{$type}_id", 'template' ); } else { $joinCondition = $db->quoteInto( - "p.id = i.parent_${type}_id AND p.object_type = ?", + "p.id = i.parent_{$type}_id AND p.object_type = ?", 'template' ); } @@ -466,7 +466,7 @@ class TemplateTree ['p' => $table], $joinCondition, [] - )->order('o.id')->order('i.weight'); + )->order('o.object_name')->order('i.weight'); if ($type !== 'command') { $query->where( diff --git a/library/Director/RestApi/RestApiClient.php b/library/Director/RestApi/RestApiClient.php index 2ebc4d4..6dcf93c 100644 --- a/library/Director/RestApi/RestApiClient.php +++ b/library/Director/RestApi/RestApiClient.php @@ -8,7 +8,6 @@ use RuntimeException; class RestApiClient { - /** @var resource */ private $curl; /** @var string HTTP or HTTPS */ @@ -287,7 +286,7 @@ class RestApiClient if ($statusCode >= 400) { throw new RuntimeException( - "Got $statusCode: " . \var_export($res, 1) + "Got $statusCode: " . \var_export($res, true) ); } @@ -295,14 +294,14 @@ class RestApiClient } /** - * @return resource + * @throws RuntimeException */ protected function curl() { if ($this->curl === null) { - $this->curl = \curl_init(\sprintf('https://%s:%d', $this->host, $this->port)); + $this->curl = curl_init(sprintf('https://%s:%d', $this->host, $this->port)); if (! $this->curl) { - throw new RuntimeException('CURL INIT ERROR: ' . \curl_error($this->curl)); + throw new RuntimeException('CURL INIT FAILED'); } } diff --git a/library/Director/RestApi/RestApiParams.php b/library/Director/RestApi/RestApiParams.php index c237ac5..741eeed 100644 --- a/library/Director/RestApi/RestApiParams.php +++ b/library/Director/RestApi/RestApiParams.php @@ -19,8 +19,9 @@ class RestApiParams } $exporter->enableHostServices(); } + /** @var ?string $properties */ $properties = $params->shift('properties'); - if ($properties !== null && strlen($properties)) { + if ($properties) { $exporter->filterProperties(preg_split('/\s*,\s*/', $properties, -1, PREG_SPLIT_NO_EMPTY)); } $exporter->resolveObjects($resolved); diff --git a/library/Director/Restriction/FilterByNameRestriction.php b/library/Director/Restriction/FilterByNameRestriction.php index 8c3b256..aef28c4 100644 --- a/library/Director/Restriction/FilterByNameRestriction.php +++ b/library/Director/Restriction/FilterByNameRestriction.php @@ -30,7 +30,7 @@ class FilterByNameRestriction extends ObjectRestriction protected function setNameForType($type) { - $this->name = "director/${type}/filter-by-name"; + $this->name = "director/{$type}/filter-by-name"; } public function allows(IcingaObject $object) diff --git a/library/Director/Restriction/HostgroupRestriction.php b/library/Director/Restriction/HostgroupRestriction.php index 1a6792b..f2face4 100644 --- a/library/Director/Restriction/HostgroupRestriction.php +++ b/library/Director/Restriction/HostgroupRestriction.php @@ -2,7 +2,7 @@ namespace Icinga\Module\Director\Restriction; -use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Auth\Restriction; use Icinga\Module\Director\Db\IcingaObjectFilterHelper; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaHostGroup; @@ -11,7 +11,7 @@ use Zend_Db_Select as ZfSelect; class HostgroupRestriction extends ObjectRestriction { - protected $name = 'director/filter/hostgroups'; + protected $name = Restriction::FILTER_HOSTGROUPS; public function allows(IcingaObject $object) { @@ -93,7 +93,7 @@ class HostgroupRestriction extends ObjectRestriction ['id'] )->where('id = ?', $hostgroup->id); - $this->filterHostGroupsQuery($query); + $this->filterHostGroupsQuery($query, 'h'); return (int) $this->db->fetchOne($query) === (int) $hostgroup->get('id'); } @@ -141,7 +141,7 @@ class HostgroupRestriction extends ObjectRestriction if (empty($groups)) { $query->where('(1 = 0)'); } else { - $query->where("${tableAlias}.object_name IN (?)", $groups); + $query->where("{$tableAlias}.object_name IN (?)", $groups); } } diff --git a/library/Director/StartupLogRenderer.php b/library/Director/StartupLogRenderer.php index bc7b3ea..9d5810f 100644 --- a/library/Director/StartupLogRenderer.php +++ b/library/Director/StartupLogRenderer.php @@ -33,7 +33,9 @@ class StartupLogRenderer implements ValidHtml // len [stage] + 1 $markReplace = ' ^'; - foreach (preg_split('/\n/', $log) as $line) { + /** @var string[] $logLines */ + $logLines = preg_split('/\n/', $log); + foreach ($logLines as $line) { if (preg_match('/^\[([\d\s\:\+\-]+)\]\s/', $line, $m)) { $time = $m[1]; // TODO: we might use new DateTime($time) and show a special "timeAgo" @@ -83,9 +85,9 @@ class StartupLogRenderer implements ValidHtml } if ($time === null) { - $lines[] .= $line; + $lines[] = $line; } else { - $lines[] .= "[$time] $line"; + $lines[] = "[$time] $line"; } } return implode("\n", $lines); diff --git a/library/Director/Test/BaseTestCase.php b/library/Director/Test/BaseTestCase.php index 611805b..f0cf8e5 100644 --- a/library/Director/Test/BaseTestCase.php +++ b/library/Director/Test/BaseTestCase.php @@ -10,20 +10,15 @@ use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Migrations; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Objects\IcingaZone; -use PHPUnit_Framework_TestCase; +use Icinga\Test\BaseTestCase as IcingaBaseTestCase; -abstract class BaseTestCase extends PHPUnit_Framework_TestCase +abstract class BaseTestCase extends IcingaBaseTestCase { private static $app; /** @var Db */ private static $db; - public function setUp() - { - $this->app(); - } - protected function skipForMissingDb() { if ($this->hasDb()) { @@ -69,6 +64,9 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase if (array_key_exists('DIRECTOR_TESTDB_HOST', $_SERVER)) { $dbConfig->host = $_SERVER['DIRECTOR_TESTDB_HOST']; } + if (array_key_exists('DIRECTOR_TESTDB_PORT', $_SERVER)) { + $dbConfig->port = $_SERVER['DIRECTOR_TESTDB_PORT']; + } if (array_key_exists('DIRECTOR_TESTDB_USER', $_SERVER)) { $dbConfig->username = $_SERVER['DIRECTOR_TESTDB_USER']; } @@ -78,11 +76,14 @@ abstract class BaseTestCase extends PHPUnit_Framework_TestCase self::$db = new Db($dbConfig); $migrations = new Migrations(self::$db); $migrations->applyPendingMigrations(); - IcingaZone::create([ + $zone = IcingaZone::create([ 'object_name' => 'director-global', 'object_type' => 'external_object', 'is_global' => 'y' - ])->store(self::$db); + ]); + if (! IcingaZone::exists($zone->getId(), self::$db)) { + $zone->store(self::$db); + } } return self::$db; diff --git a/library/Director/Test/IcingaObjectTestCase.php b/library/Director/Test/IcingaObjectTestCase.php index a37fced..5d0dde3 100644 --- a/library/Director/Test/IcingaObjectTestCase.php +++ b/library/Director/Test/IcingaObjectTestCase.php @@ -76,7 +76,7 @@ abstract class IcingaObjectTestCase extends BaseTestCase /** * @inheritdoc */ - public function tearDown() + public function tearDown(): void { if ($this->hasDb()) { /** @var IcingaObject $object */ @@ -88,5 +88,7 @@ abstract class IcingaObjectTestCase extends BaseTestCase $this->subject->delete(); } } + + parent::tearDown(); } } diff --git a/library/Director/Test/SyncTest.php b/library/Director/Test/SyncTest.php index 7614ff9..d118eea 100644 --- a/library/Director/Test/SyncTest.php +++ b/library/Director/Test/SyncTest.php @@ -29,8 +29,9 @@ abstract class SyncTest extends BaseTestCase /** @var Sync */ protected $sync; - public function setUp() + public function setUp(): void { + parent::setUp(); $this->source = ImportSource::create(array( 'source_name' => 'testimport', 'provider_class' => 'Icinga\\Module\\Director\\Test\\ImportSourceDummy', @@ -49,7 +50,7 @@ abstract class SyncTest extends BaseTestCase $this->sync = new Sync($this->rule); } - public function tearDown() + public function tearDown(): void { // properties should be deleted automatically if ($this->rule !== null && $this->rule->hasBeenLoadedFromDb()) { @@ -75,6 +76,7 @@ abstract class SyncTest extends BaseTestCase // make sure cache is clean for other tests PrefetchCache::forget(); DbObject::clearAllPrefetchCaches(); + parent::tearDown(); } /** diff --git a/library/Director/Test/TestSuiteLint.php b/library/Director/Test/TestSuiteLint.php index 41941eb..0010c28 100644 --- a/library/Director/Test/TestSuiteLint.php +++ b/library/Director/Test/TestSuiteLint.php @@ -10,6 +10,8 @@ class TestSuiteLint extends TestSuite protected $failed; + protected $result = []; + public function run() { $this->checked = $this->failed = array(); diff --git a/library/Director/Test/TestSuiteUnit.php b/library/Director/Test/TestSuiteUnit.php index 8156eba..93dc692 100644 --- a/library/Director/Test/TestSuiteUnit.php +++ b/library/Director/Test/TestSuiteUnit.php @@ -4,6 +4,8 @@ namespace Icinga\Module\Director\Test; abstract class TestSuiteUnit { + private $testdoxFile; + public function run() { } @@ -15,7 +17,7 @@ abstract class TestSuiteUnit public function __destruct() { if ($this->testdoxFile && file_exists($this->testdoxFile)) { - unlink($this->testDoxfile); + unlink($this->testdoxFile); } } diff --git a/library/Director/Web/Controller/ActionController.php b/library/Director/Web/Controller/ActionController.php index 6282a16..e851d82 100644 --- a/library/Director/Web/Controller/ActionController.php +++ b/library/Director/Web/Controller/ActionController.php @@ -4,10 +4,14 @@ namespace Icinga\Module\Director\Web\Controller; use gipfl\Translation\StaticTranslator; use Icinga\Application\Benchmark; +use Icinga\Application\Modules\Module; use Icinga\Data\Paginatable; use Icinga\Exception\NotFoundError; use Icinga\Exception\ProgrammingError; -use Icinga\Module\Director\Monitoring; +use Icinga\Module\Director\Integration\Icingadb\IcingadbBackend; +use Icinga\Module\Director\Integration\BackendInterface; +use Icinga\Module\Director\Integration\MonitoringModule\Monitoring; +use Icinga\Module\Director\ProvidedHook\Icingadb\IcingadbSupport; use Icinga\Module\Director\Web\Controller\Extension\CoreApi; use Icinga\Module\Director\Web\Controller\Extension\DirectorDb; use Icinga\Module\Director\Web\Controller\Extension\RestApi; @@ -36,8 +40,8 @@ abstract class ActionController extends Controller implements ControlsAndContent /** @var UrlParams Hint for IDE, somehow does not work in web */ protected $params; - /** @var Monitoring */ - private $monitoring; + /** @var BackendInterface */ + private $backend; /** * @throws SecurityException @@ -219,7 +223,7 @@ abstract class ActionController extends Controller implements ControlsAndContent // Hint -> $this->view->compact is the only way since v2.8.0 if ($this->view->compact || $this->getOriginalUrl()->getParam('view') === 'compact') { if ($this->view->controls) { - $this->controls()->getAttributes()->add('style', 'display: none;'); + $this->controls()->getAttributes()->add('class', 'compact'); } } } else { @@ -240,14 +244,18 @@ abstract class ActionController extends Controller implements ControlsAndContent } /** - * @return Monitoring + * @return BackendInterface */ - protected function monitoring() + protected function backend(): BackendInterface { - if ($this->monitoring === null) { - $this->monitoring = new Monitoring; + if ($this->backend === null) { + if (Module::exists('icingadb') && IcingadbSupport::useIcingaDbAsBackend()) { + $this->backend = new IcingadbBackend(); + } else { + $this->backend = new Monitoring($this->getAuth()); + } } - return $this->monitoring; + return $this->backend; } } diff --git a/library/Director/Web/Controller/BranchHelper.php b/library/Director/Web/Controller/BranchHelper.php index ac2a480..89aa6c1 100644 --- a/library/Director/Web/Controller/BranchHelper.php +++ b/library/Director/Web/Controller/BranchHelper.php @@ -3,12 +3,13 @@ namespace Icinga\Module\Director\Web\Controller; use Icinga\Module\Director\Data\Db\DbObjectStore; -use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Db\Branch\BranchStore; use Icinga\Module\Director\Db\Branch\BranchSupport; +use Icinga\Module\Director\Db\Branch\PreferredBranchSupport; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Web\Widget\NotInBranchedHint; +use Ramsey\Uuid\UuidInterface; trait BranchHelper { @@ -18,15 +19,21 @@ trait BranchHelper /** @var BranchStore */ protected $branchStore; + /** @var ?bool */ + protected $hasPreferredBranch = null; + /** - * @return false|\Ramsey\Uuid\UuidInterface + * @return ?UuidInterface */ - protected function getBranchUuid() + protected function getBranchUuid(): ?UuidInterface { return $this->getBranch()->getUuid(); } - protected function getBranch() + /** + * @return Branch + */ + protected function getBranch(): Branch { if ($this->branch === null) { /** @var ActionController $this */ @@ -39,7 +46,7 @@ trait BranchHelper /** * @return BranchStore */ - protected function getBranchStore() + protected function getBranchStore(): BranchStore { if ($this->branchStore === null) { $this->branchStore = new BranchStore($this->db()); @@ -48,12 +55,15 @@ trait BranchHelper return $this->branchStore; } - protected function hasBranch() + /** + * @return bool + */ + protected function hasBranch(): bool { return $this->getBranchUuid() !== null; } - protected function enableStaticObjectLoader($table) + protected function enableStaticObjectLoader($table): void { if (BranchSupport::existsForTableName($table)) { IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); @@ -64,7 +74,7 @@ trait BranchHelper * @param string $subject * @return bool */ - protected function showNotInBranch($subject) + protected function showNotInBranch($subject): bool { if ($this->getBranch()->isBranch()) { $this->content()->add(new NotInBranchedHint($subject, $this->getBranch(), $this->Auth())); @@ -73,4 +83,18 @@ trait BranchHelper return false; } + + protected function hasPreferredBranch(): bool + { + if ($this->hasPreferredBranch === null) { + $implementation = Branch::optionalHook(); + if ($implementation instanceof PreferredBranchSupport) { + $this->hasPreferredBranch = $implementation->hasPreferredBranch($this->Auth()); + } else { + $this->hasPreferredBranch = false; + } + } + + return $this->hasPreferredBranch; + } } diff --git a/library/Director/Web/Controller/Extension/DirectorDb.php b/library/Director/Web/Controller/Extension/DirectorDb.php index 03bec81..c36e358 100644 --- a/library/Director/Web/Controller/Extension/DirectorDb.php +++ b/library/Director/Web/Controller/Extension/DirectorDb.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Controller\Extension; +use Icinga\Module\Director\Auth\Restriction; use Icinga\Module\Director\Db; use Icinga\Module\Director\Web\Controller\ActionController; use Icinga\Module\Director\Web\Window; @@ -65,7 +66,7 @@ trait DirectorDb $auth = $this->Auth(); $available = $this->listAvailableDbResourceNames(); - if ($resourceNames = $auth->getRestrictions('director/db_resource')) { + if ($resourceNames = $auth->getRestrictions(Restriction::DB_RESOURCE)) { $names = []; foreach ($resourceNames as $rNames) { foreach ($this->splitList($rNames) as $name) { diff --git a/library/Director/Web/Controller/Extension/RestApi.php b/library/Director/Web/Controller/Extension/RestApi.php index 3158f49..fb10c86 100644 --- a/library/Director/Web/Controller/Extension/RestApi.php +++ b/library/Director/Web/Controller/Extension/RestApi.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Director\Web\Controller\Extension; use Icinga\Exception\AuthenticationException; use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Exception\JsonException; use Icinga\Web\Response; use InvalidArgumentException; @@ -55,7 +56,7 @@ trait RestApi */ protected function assertApiPermission() { - if (! $this->hasPermission('director/api')) { + if (! $this->hasPermission(Permission::API)) { throw new AuthenticationException('You are not allowed to access this API'); } } diff --git a/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php deleted file mode 100644 index bc51548..0000000 --- a/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php +++ /dev/null @@ -1,236 +0,0 @@ -<?php - -namespace Icinga\Module\Director\Web\Controller\Extension; - -use Exception; -use Icinga\Exception\IcingaException; -use Icinga\Exception\InvalidPropertyException; -use Icinga\Exception\NotFoundError; -use Icinga\Module\Director\Forms\IcingaDeleteObjectForm; -use Icinga\Module\Director\Objects\IcingaObject; -use Icinga\Web\Request; -use Icinga\Web\Response; - -class SingleObjectApiHandler -{ - use DirectorDb; - - /** @var IcingaObject */ - private $object; - - /** @var string */ - private $type; - - /** @var Request */ - private $request; - - /** @var Response */ - private $response; - - /** @var \Icinga\Web\UrlParams */ - private $params; - - public function __construct($type, Request $request, Response $response) - { - $this->type = $type; - $this->request = $request; - $this->response = $response; - $this->params = $request->getUrl()->getParams(); - } - - public function runFailSafe() - { - try { - $this->loadObject(); - $this->run(); - } catch (NotFoundError $e) { - $this->sendJsonError($e->getMessage(), 404); - } catch (Exception $e) { - $response = $this->response; - if ($response->getHttpResponseCode() === 200) { - $response->setHttpResponseCode(500); - } - - $this->sendJsonError($e->getMessage()); - } - } - - protected function retrieveObject() - { - $this->requireObject(); - $this->sendJson( - $this->object->toPlainObject( - $this->params->shift('resolved'), - ! $this->params->shift('withNull'), - $this->params->shift('properties') - ) - ); - } - - protected function deleteObject() - { - $this->requireObject(); - $obj = $this->object->toPlainObject(false, true); - $form = new IcingaDeleteObjectForm(); - $form->setObject($this->object) - ->setRequest($this->request) - ->onSuccess(); - - $this->sendJson($obj); - } - - protected function storeObject() - { - $data = json_decode($this->request->getRawBody()); - - if ($data === null) { - $this->response->setHttpResponseCode(400); - throw new IcingaException( - 'Invalid JSON: %s' . $this->request->getRawBody(), - $this->getLastJsonError() - ); - } else { - $data = (array) $data; - } - - if ($object = $this->object) { - if ($this->request->getMethod() === 'POST') { - $object->setProperties($data); - } else { - $data = array_merge([ - 'object_type' => $object->object_type, - 'object_name' => $object->object_name - ], $data); - $object->replaceWith( - IcingaObject::createByType($this->type, $data, $db) - ); - } - } else { - $object = IcingaObject::createByType($this->type, $data, $db); - } - - if ($object->hasBeenModified()) { - $status = $object->hasBeenLoadedFromDb() ? 200 : 201; - $object->store(); - $this->response->setHttpResponseCode($status); - } else { - $this->response->setHttpResponseCode(304); - } - - $this->sendJson($object->toPlainObject(false, true)); - } - - public function run() - { - switch ($this->request->getMethod()) { - case 'DELETE': - $this->deleteObject(); - break; - - case 'POST': - case 'PUT': - $this->storeObject(); - break; - - case 'GET': - $this->retrieveObject(); - break; - - default: - $this->response->setHttpResponseCode(400); - throw new IcingaException( - 'Unsupported method: %s', - $this->request->getMethod() - ); - } - } - - protected function requireObject() - { - if (! $this->object) { - $this->response->setHttpResponseCode(404); - if (! $this->params->get('name')) { - throw new NotFoundError('You need to pass a "name" parameter to access a specific object'); - } else { - throw new NotFoundError('No such object available'); - } - } - } - - // TODO: just return json_last_error_msg() for PHP >= 5.5.0 - protected function getLastJsonError() - { - switch (json_last_error()) { - case JSON_ERROR_DEPTH: - return 'The maximum stack depth has been exceeded'; - case JSON_ERROR_CTRL_CHAR: - return 'Control character error, possibly incorrectly encoded'; - case JSON_ERROR_STATE_MISMATCH: - return 'Invalid or malformed JSON'; - case JSON_ERROR_SYNTAX: - return 'Syntax error'; - case JSON_ERROR_UTF8: - return 'Malformed UTF-8 characters, possibly incorrectly encoded'; - default: - return 'An error occured when parsing a JSON string'; - } - } - - protected function sendJson($object) - { - $this->response->setHeader('Content-Type', 'application/json', true); - $this->_helper->layout()->disableLayout(); - $this->_helper->viewRenderer->setNoRender(true); - echo json_encode($object, JSON_PRETTY_PRINT) . "\n"; - } - - protected function sendJsonError($message, $code = null) - { - $response = $this->response; - - if ($code !== null) { - $response->setHttpResponseCode((int) $code); - } - - $this->sendJson((object) ['error' => $message]); - } - - protected function loadObject() - { - if ($this->object === null) { - if ($name = $this->params->get('name')) { - $this->object = IcingaObject::loadByType( - $this->type, - $name, - $this->db() - ); - - if (! $this->allowsObject($this->object)) { - $this->object = null; - throw new NotFoundError('No such object available'); - } - } elseif ($id = $this->params->get('id')) { - $this->object = IcingaObject::loadByType( - $this->type, - (int) $id, - $this->db() - ); - } elseif ($this->request->isApiRequest()) { - if ($this->request->isGet()) { - $this->response->setHttpResponseCode(422); - - throw new InvalidPropertyException( - 'Cannot load object, missing parameters' - ); - } - } - } - - return $this->object; - } - - protected function allowsObject(IcingaObject $object) - { - return true; - } -} diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php index 0c06937..994a717 100644 --- a/library/Director/Web/Controller/ObjectController.php +++ b/library/Director/Web/Controller/ObjectController.php @@ -10,6 +10,7 @@ use Icinga\Exception\ProgrammingError; use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Db\Branch\BranchedObject; +use Icinga\Module\Director\Db\Branch\BranchSupport; use Icinga\Module\Director\Db\Branch\UuidLookup; use Icinga\Module\Director\Deployment\DeploymentInfo; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; @@ -61,8 +62,11 @@ abstract class ObjectController extends ActionController public function init() { - parent::init(); $this->enableStaticObjectLoader($this->getTableName()); + if (! $this->getRequest()->isApiRequest()) { + $this->loadOptionalObject(); + } + parent::init(); if ($this->getRequest()->isApiRequest()) { $this->initializeRestApi(); } else { @@ -97,7 +101,6 @@ abstract class ObjectController extends ActionController protected function initializeWebRequest() { - $this->loadOptionalObject(); if ($this->getRequest()->getActionName() === 'add') { $this->addSingleTab( sprintf($this->translate('Add %s'), ucfirst($this->getType())), @@ -151,8 +154,11 @@ abstract class ObjectController extends ActionController $this->addObject(); } $branch = $this->getBranch(); - if ($branch->isBranch() && ! $this->getRequest()->isApiRequest()) { - $this->content()->add(new BranchedObjectHint($branch, $this->Auth())); + if (! $this->getRequest()->isApiRequest()) { + $hasPreferred = $this->hasPreferredBranch(); + if ($branch->isBranch() || $hasPreferred) { + $this->content()->add(new BranchedObjectHint($branch, $this->Auth(), null, $hasPreferred)); + } } $form->handleRequest(); @@ -267,7 +273,7 @@ abstract class ObjectController extends ActionController if ($id = $this->params->get('field_id')) { $form->loadObject([ - "${type}_id" => $object->id, + "{$type}_id" => $object->id, 'datafield_id' => $id ]); @@ -362,7 +368,7 @@ abstract class ObjectController extends ActionController $this->actions()->add([ Link::create( $this->translate('Usage'), - "director/${type}template/usage", + "director/{$type}template/usage", ['name' => $object->getObjectName()], ['class' => 'icon-sitemap'] ) @@ -558,8 +564,16 @@ abstract class ObjectController extends ActionController if (! $this->allowsObject($object)) { throw new NotFoundError('No such object available'); } - if ($showHint && $branch->isBranch() && $object->isObject() && ! $this->getRequest()->isApiRequest()) { - $this->content()->add(new BranchedObjectHint($branch, $this->Auth(), $branchedObject)); + if ($showHint) { + $hasPreferredBranch = $this->hasPreferredBranch(); + if (($hasPreferredBranch || $branch->isBranch()) + && $object->isObject() + && ! $this->getRequest()->isApiRequest() + ) { + $this->content()->add( + new BranchedObjectHint($branch, $this->Auth(), $branchedObject, $hasPreferredBranch) + ); + } } return $object; diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php index 8c10b44..c4c96c5 100644 --- a/library/Director/Web/Controller/ObjectsController.php +++ b/library/Director/Web/Controller/ObjectsController.php @@ -13,6 +13,7 @@ use Icinga\Module\Director\Forms\IcingaMultiEditForm; use Icinga\Module\Director\Objects\IcingaCommand; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaService; use Icinga\Module\Director\RestApi\IcingaObjectsHandler; use Icinga\Module\Director\Web\ActionBar\ObjectsActionBar; use Icinga\Module\Director\Web\ActionBar\TemplateActionBar; @@ -84,8 +85,9 @@ abstract class ObjectsController extends ActionController } elseif ($request->getActionName() === 'applyrules') { $table->filterObjectType('apply'); } + /** @var ?string $search */ $search = $this->params->get('q'); - if ($search !== null && \strlen($search) > 0) { + if ($search) { $table->search($search); } @@ -110,7 +112,7 @@ abstract class ObjectsController extends ActionController $type = $this->getType(); if ($this->params->get('format') === 'json') { $filename = sprintf( - "director-${type}_%s.json", + "director-{$type}_%s.json", date('YmdHis') ); $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true); @@ -124,7 +126,7 @@ abstract class ObjectsController extends ActionController ->addTitle($this->translate(ucfirst($this->getPluralType()))) ->actions(new ObjectsActionBar($this->getBaseObjectUrl(), $this->url())); - $this->content()->add(new BranchedObjectsHint($this->getBranch(), $this->Auth())); + $this->content()->add(new BranchedObjectsHint($this->getBranch(), $this->Auth(), $this->hasPreferredBranch())); if ($type === 'command' && $this->params->get('type') === 'external_object') { $this->tabs()->activate('external'); @@ -143,8 +145,7 @@ abstract class ObjectsController extends ActionController */ protected function getTable() { - $table = ObjectsTable::create($this->getType(), $this->db()) - ->setAuth($this->getAuth()) + $table = ObjectsTable::create($this->getType(), $this->db(), $this->getAuth()) ->setBranchUuid($this->getBranchUuid()) ->setBaseObjectUrl($this->getBaseObjectUrl()); @@ -157,7 +158,7 @@ abstract class ObjectsController extends ActionController */ protected function getApplyRulesTable() { - $table = new ApplyRulesTable($this->db()); + $table = (new ApplyRulesTable($this->db()))->setBranch($this->getBranch()); $table->setType($this->getType()) ->setBaseObjectUrl($this->getBaseObjectUrl()); $this->eventuallyFilterCommand($table); @@ -235,7 +236,7 @@ abstract class ObjectsController extends ActionController if ($this->params->get('format') === 'json') { $filename = sprintf( - "director-${type}-templates_%s.json", + "director-{$type}-templates_%s.json", date('YmdHis') ); $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true); @@ -290,7 +291,7 @@ abstract class ObjectsController extends ActionController if ($this->params->get('format') === 'json') { $filename = sprintf( - "director-${type}-applyrules_%s.json", + "director-{$type}-applyrules_%s.json", date('YmdHis') ); $this->getResponse()->setHeader('Content-disposition', "attachment; filename=$filename", true); @@ -313,7 +314,7 @@ abstract class ObjectsController extends ActionController ->add( Link::create( $this->translate('Add'), - "${baseUrl}/add", + "{$baseUrl}/add", ['type' => 'apply'], [ 'title' => sprintf( @@ -354,7 +355,7 @@ abstract class ObjectsController extends ActionController $this->actions()->add( Link::create( $this->translate('Add'), - "director/${type}set/add", + "director/{$type}set/add", null, [ 'title' => sprintf( @@ -409,7 +410,12 @@ abstract class ObjectsController extends ActionController $objects[$name] = $class::load($name, $db); } elseif ($col === 'uuid') { $object = $store->load($table, Uuid::fromString($ex->getExpression())); - $objects[$object->getObjectName()] = $object; + if ($object instanceof IcingaService) { + $host = $object->getRelated('host'); + $objects[$host->getObjectName() . ': ' . $object->getObjectName()] = $object; + } else { + $objects[$object->getObjectName()] = $object; + } } else { throw new InvalidArgumentException("'$col' is no a valid key component for '$type'"); } diff --git a/library/Director/Web/Controller/TemplateController.php b/library/Director/Web/Controller/TemplateController.php index c368a82..4f0898a 100644 --- a/library/Director/Web/Controller/TemplateController.php +++ b/library/Director/Web/Controller/TemplateController.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Web\Controller; use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; use Icinga\Module\Director\Exception\NestingError; use Icinga\Module\Director\Objects\IcingaCommand; @@ -10,6 +11,7 @@ use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Web\Controller\Extension\DirectorDb; use Icinga\Module\Director\Web\Table\ApplyRulesTable; use Icinga\Module\Director\Web\Table\ObjectsTable; +use Icinga\Module\Director\Web\Table\ObjectsTableSetMembers; use Icinga\Module\Director\Web\Table\TemplatesTable; use Icinga\Module\Director\Web\Table\TemplateUsageTable; use Icinga\Module\Director\Web\Tabs\ObjectTabs; @@ -23,6 +25,8 @@ abstract class TemplateController extends CompatController { use DirectorDb; + use BranchHelper; + /** @var IcingaObject */ protected $template; @@ -39,9 +43,29 @@ abstract class TemplateController extends CompatController $template->getObjectName() )->addBackToUsageLink($template); - ObjectsTable::create($this->getType(), $this->db()) - ->setAuth($this->Auth()) + ObjectsTable::create($this->getType(), $this->db(), $this->Auth()) + ->setBranch($this->getBranch()) + ->setBaseObjectUrl($this->getBaseObjectUrl()) + ->filterTemplate($template, $this->getInheritance()) + ->renderTo($this); + } + + public function setmembersAction() + { + $template = $this->requireTemplate(); + $plural = $this->getTranslatedPluralType(); + $this + ->addSingleTab($plural) + ->setAutorefreshInterval(10) + ->addTitle( + $this->translate('%s in service sets based on %s'), + $plural, + $template->getObjectName() + )->addBackToUsageLink($template); + + ObjectsTableSetMembers::create($this->getType(), $this->db(), $this->Auth()) ->setBaseObjectUrl($this->getBaseObjectUrl()) + ->setBranch($this->getBranch()) ->filterTemplate($template, $this->getInheritance()) ->renderTo($this); } @@ -60,6 +84,7 @@ abstract class TemplateController extends CompatController ApplyRulesTable::create($type, $this->db()) ->setBaseObjectUrl($this->getBaseObjectUrl()) + ->setBranch($this->getBranch()) ->filterTemplate($template, $this->params->get('inheritance', 'direct')) ->renderTo($this); } @@ -93,7 +118,7 @@ abstract class TemplateController extends CompatController $this->actions()->add( Link::create( $this->translate('Back'), - "director/${type}template/usage", + "director/{$type}template/usage", ['name' => $template->getObjectName()], ['class' => 'icon-left-big'] ) @@ -152,7 +177,7 @@ abstract class TemplateController extends CompatController )] )); } - if ($auth->hasPermission('director/admin')) { + if ($auth->hasPermission(Permission::ADMIN)) { $list->addItem(new FormattedString( $this->translate('Create a new %s inheriting from this one'), [Link::create( @@ -188,7 +213,7 @@ abstract class TemplateController extends CompatController try { $this->content()->add( - TemplateUsageTable::forTemplate($template) + TemplateUsageTable::forTemplate($template, $this->Auth(), $this->getBranch()) ); } catch (NestingError $e) { $this->content()->add(Hint::error($e->getMessage())); diff --git a/library/Director/Web/Form/CloneImportSourceForm.php b/library/Director/Web/Form/CloneImportSourceForm.php index 0849dd4..46dc7a3 100644 --- a/library/Director/Web/Form/CloneImportSourceForm.php +++ b/library/Director/Web/Form/CloneImportSourceForm.php @@ -2,8 +2,10 @@ namespace Icinga\Module\Director\Web\Form; +use gipfl\Web\Form; use Icinga\Module\Director\Data\Exporter; -use ipl\Html\Form; +use Icinga\Module\Director\Data\ObjectImporter; +use Icinga\Module\Director\Db; use ipl\Html\FormDecorator\DdDtDecorator; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Url; @@ -36,37 +38,25 @@ class CloneImportSourceForm extends Form ]); } - /** - * @return \Icinga\Module\Director\Db - */ - protected function getTargetDb() - { - return $this->source->getConnection(); - } - - /** - * @throws \Icinga\Module\Director\Exception\DuplicateKeyException - */ public function onSuccess() { - $db = $this->getTargetDb(); + $db = $this->source->getConnection(); + assert($db instanceof Db); $export = (new Exporter($db))->export($this->source); $newName = $this->getElement('source_name')->getValue(); $export->source_name = $newName; - unset($export->originalId); + unset($export->uuid); + if (ImportSource::existsWithName($newName, $db)) { $this->getElement('source_name')->addMessage('Name already exists'); } - $this->newSource = ImportSource::import($export, $db); + $importer = new ObjectImporter($db); + $this->newSource = $importer->import(ImportSource::class, $export); $this->newSource->store(); } public function getSuccessUrl() { - if ($this->newSource === null) { - return parent::getSuccessUrl(); - } else { - return Url::fromPath('director/importsource', ['id' => $this->newSource->get('id')]); - } + return Url::fromPath('director/importsource', ['id' => $this->newSource->get('id')]); } } diff --git a/library/Director/Web/Form/CloneSyncRuleForm.php b/library/Director/Web/Form/CloneSyncRuleForm.php index f90b593..ccd61ec 100644 --- a/library/Director/Web/Form/CloneSyncRuleForm.php +++ b/library/Director/Web/Form/CloneSyncRuleForm.php @@ -2,8 +2,10 @@ namespace Icinga\Module\Director\Web\Form; +use gipfl\Web\Form; use Icinga\Module\Director\Data\Exporter; -use ipl\Html\Form; +use Icinga\Module\Director\Data\ObjectImporter; +use Icinga\Module\Director\Db; use ipl\Html\FormDecorator\DdDtDecorator; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Url; @@ -37,40 +39,30 @@ class CloneSyncRuleForm extends Form } /** - * @return \Icinga\Module\Director\Db - */ - protected function getTargetDb() - { - return $this->rule->getConnection(); - } - - /** * @throws \Icinga\Exception\NotFoundError * @throws \Icinga\Module\Director\Exception\DuplicateKeyException */ public function onSuccess() { - $db = $this->getTargetDb(); + $db = $this->rule->getConnection(); + assert($db instanceof Db); $exporter = new Exporter($db); $export = $exporter->export($this->rule); $newName = $this->getValue('rule_name'); $export->rule_name = $newName; - unset($export->originalId); + unset($export->uuid); if (SyncRule::existsWithName($newName, $db)) { $this->getElement('rule_name')->addMessage('Name already exists'); } - $this->newRule = SyncRule::import($export, $db); + $importer = new ObjectImporter($db); + $this->newRule = $importer->import(SyncRule::class, $export); $this->newRule->store(); } public function getSuccessUrl() { - if ($this->newRule === null) { - return parent::getSuccessUrl(); - } else { - return Url::fromPath('director/syncrule', ['id' => $this->newRule->get('id')]); - } + return Url::fromPath('director/syncrule', ['id' => $this->newRule->get('id')]); } } diff --git a/library/Director/Web/Form/CsrfToken.php b/library/Director/Web/Form/CsrfToken.php index 24edf88..f6c29ec 100644 --- a/library/Director/Web/Form/CsrfToken.php +++ b/library/Director/Web/Form/CsrfToken.php @@ -17,7 +17,7 @@ class CsrfToken return false; } - list($seed, $token) = explode('|', $elementValue); + list($seed, $token) = explode('|', $token); if (!is_numeric($seed)) { return false; diff --git a/library/Director/Web/Form/DbSelectorForm.php b/library/Director/Web/Form/DbSelectorForm.php index 52fe5ea..8b4f432 100644 --- a/library/Director/Web/Form/DbSelectorForm.php +++ b/library/Director/Web/Form/DbSelectorForm.php @@ -69,7 +69,7 @@ class DbSelectorForm extends Form $params = []; } - if (array_key_exists($name, $params)) { + if (is_array($params) && array_key_exists($name, $params)) { return $params[$name]; } diff --git a/library/Director/Web/Form/DirectorForm.php b/library/Director/Web/Form/DirectorForm.php index 145be5b..36c0577 100644 --- a/library/Director/Web/Form/DirectorForm.php +++ b/library/Director/Web/Form/DirectorForm.php @@ -34,7 +34,7 @@ abstract class DirectorForm extends QuickForm public static function load() { return new static([ - 'icingaModule' => Icinga::App()->getModuleManager()->getModule('director') + 'icingaModule' => Icinga::app()->getModuleManager()->getModule('director') ]); } diff --git a/library/Director/Web/Form/DirectorObjectForm.php b/library/Director/Web/Form/DirectorObjectForm.php index b70bd7b..abbd4f0 100644 --- a/library/Director/Web/Form/DirectorObjectForm.php +++ b/library/Director/Web/Form/DirectorObjectForm.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Director\Web\Form; use Exception; use gipfl\IcingaWeb2\Url; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Data\Db\DbObjectStore; use Icinga\Module\Director\Db; use Icinga\Module\Director\Data\Db\DbObject; @@ -443,7 +444,7 @@ abstract class DirectorObjectForm extends DirectorForm $this->setInheritedValue( $el, $object->getRelatedObjectName($k, $v), - $origins->{"${k}_id"} + $origins->{"{$k}_id"} ); } } @@ -540,7 +541,9 @@ abstract class DirectorObjectForm extends DirectorForm 'inherited_groups', 'applied_groups', 'users', + 'users_var', 'user_groups', + 'user_groups_var', 'apply_to', 'command_id', // Notification 'notification_interval', @@ -788,7 +791,9 @@ abstract class DirectorObjectForm extends DirectorForm return; } - $post = $values = $this->getRequest()->getPost(); + /** @var array $post */ + $post = $this->getRequest()->getPost(); + $values = $post; foreach ($post as $key => $value) { if (preg_match('/^(.+?)_(\d+)__(MOVE_DOWN|MOVE_UP|REMOVE)$/', $key, $m)) { @@ -1239,7 +1244,7 @@ abstract class DirectorObjectForm extends DirectorForm if ($this->hasBeenSent()) { $this->addError($this->translate('No template has been chosen')); } else { - if ($this->hasPermission('director/admin')) { + if ($this->hasPermission(Permission::ADMIN)) { $html = $this->translate('Please define a related template first'); } else { $html = $this->translate('No related template has been provided yet'); @@ -1274,7 +1279,7 @@ abstract class DirectorObjectForm extends DirectorForm 'required' => $required, 'spellcheck' => 'false', 'hideOptions' => $choiceNames, - 'suggest' => "${type}templates", + 'suggest' => "{$type}templates", // 'multiOptions' => $this->optionallyAddFromEnum($enum), 'sorted' => true, 'value' => $this->presetImports, @@ -1516,6 +1521,7 @@ abstract class DirectorObjectForm extends DirectorForm return []; } + /** @var int|string $id */ $id = $object->get('id'); if (array_key_exists($id, $tpl)) { diff --git a/library/Director/Web/Form/Element/DataFilter.php b/library/Director/Web/Form/Element/DataFilter.php index adae07d..7beb651 100644 --- a/library/Director/Web/Form/Element/DataFilter.php +++ b/library/Director/Web/Form/Element/DataFilter.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Form\Element; +use gipfl\Json\JsonString; use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\FilterChain; use Icinga\Data\Filter\FilterExpression; @@ -268,13 +269,13 @@ class DataFilter extends FormElement return Filter::expression( $entry['column'], '=', - json_encode(true) + $this->jsonEncode(true) ); } elseif ($entry['sign'] === 'false') { return Filter::expression( $entry['column'], '=', - json_encode(false) + $this->jsonEncode(false) ); } elseif ($entry['sign'] === 'in') { if (array_key_exists('value', $entry)) { @@ -291,13 +292,13 @@ class DataFilter extends FormElement return Filter::expression( $entry['column'], '=', - json_encode($value) + $this->jsonEncode($value) ); } elseif ($entry['sign'] === 'contains') { $value = array_key_exists('value', $entry) ? $entry['value'] : null; return Filter::expression( - json_encode($value), + $this->jsonEncode($value), '=', $entry['column'] ); @@ -307,11 +308,20 @@ class DataFilter extends FormElement return Filter::expression( $entry['column'], $entry['sign'], - json_encode($value) + $this->jsonEncode($value) ); } } + protected function jsonEncode($string) + { + return preg_replace( + ['/&/u', '/\|/u', '/!/u', '/=/u', '/>/u', '/</u'], + ['\u0026', '\u007c', '\u0021', '\u003d', '\u003e', '\u003c'], + JsonString::encode($string) + ); + } + protected function entryAction($entry) { if (array_key_exists('action', $entry)) { diff --git a/library/Director/Web/Form/Element/ExtensibleSet.php b/library/Director/Web/Form/Element/ExtensibleSet.php index f3c968f..e443b06 100644 --- a/library/Director/Web/Form/Element/ExtensibleSet.php +++ b/library/Director/Web/Form/Element/ExtensibleSet.php @@ -28,7 +28,7 @@ class ExtensibleSet extends FormElement if (! is_array($value)) { throw new InvalidArgumentException(sprintf( 'ExtensibleSet expects to work with Arrays, got %s', - var_export($value, 1) + var_export($value, true) )); } $value = array_filter($value, 'strlen'); diff --git a/library/Director/Web/Form/IcingaObjectFieldLoader.php b/library/Director/Web/Form/IcingaObjectFieldLoader.php index c900edf..ae00855 100644 --- a/library/Director/Web/Form/IcingaObjectFieldLoader.php +++ b/library/Director/Web/Form/IcingaObjectFieldLoader.php @@ -613,7 +613,7 @@ class IcingaObjectFieldLoader $fields = []; /** @var HostFieldHook|ServiceFieldHook $hook */ $type = ucfirst($object->getShortTableName()); - foreach (Hook::all("Director\\${type}Field") as $hook) { + foreach (Hook::all("Director\\{$type}Field") as $hook) { if ($hook->wants($object)) { $id = $object->get('id'); $spec = $hook->getFieldSpec($object); diff --git a/library/Director/Web/Form/IplElement/ExtensibleSetElement.php b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php index a4dbb20..b723d47 100644 --- a/library/Director/Web/Form/IplElement/ExtensibleSetElement.php +++ b/library/Director/Web/Form/IplElement/ExtensibleSetElement.php @@ -26,6 +26,8 @@ class ExtensibleSetElement extends BaseHtmlElement private $description; + private $descriptions; + private $multiOptions; private $validOptions; @@ -109,7 +111,7 @@ class ExtensibleSetElement extends BaseHtmlElement if (null !== $value && ! is_array($value)) { throw new ProgrammingError( 'Got unexpected value, no array: %s', - var_export($value, 1) + var_export($value, true) ); } @@ -323,7 +325,7 @@ class ExtensibleSetElement extends BaseHtmlElement } else { return \sprintf( $this->translate('%s (not an Array!)'), - \var_export($this->inherited, 1) + \var_export($this->inherited, true) ); } } diff --git a/library/Director/Web/Form/QuickForm.php b/library/Director/Web/Form/QuickForm.php index 91c8f00..6100ec9 100644 --- a/library/Director/Web/Form/QuickForm.php +++ b/library/Director/Web/Form/QuickForm.php @@ -620,6 +620,10 @@ abstract class QuickForm extends QuickBaseForm $this->hasBeenSent = true; } elseif ($req->isPost()) { $post = $req->getPost(); + if (! CsrfToken::isValid($post[self::CSRF])) { + throw new Exception('Invalid CSRF token provided'); + } + $this->hasBeenSent = array_key_exists(self::ID, $post) && $post[self::ID] === $this->getName(); } else { diff --git a/library/Director/Web/SelfService.php b/library/Director/Web/SelfService.php index 33756b7..723cbf6 100644 --- a/library/Director/Web/SelfService.php +++ b/library/Director/Web/SelfService.php @@ -155,19 +155,6 @@ class SelfService ['class' => 'logfile'], $wizard->renderIcinga4WindowsWizardCommand($key) ), - Html::tag('h3', $this->translate('Icinga 2 Powershell Module')), - Html::tag('p', Html::sprintf( - $this->translate('In case you\'re using the legacy %s, please run:'), - Html::tag('a', [ - 'href' => 'https://github.com/Icinga/icinga2-powershell-module', - 'target' => '_blank', - ], $this->translate('Icinga 2 Powershell Module')) - )), - Html::tag( - 'pre', - ['class' => 'logfile'], - $wizard->renderPowershellModuleInstaller($key) - ), ]; } @@ -264,6 +251,7 @@ class SelfService null, ['class' => 'icon-download', 'target' => '_blank'] ), + Html::tag('p', null, $this->translate('Just download and run this script on your Linux Client Machine:')), Html::tag('pre', $class, $wizard->renderLinuxInstaller()) ]); diff --git a/library/Director/Web/Table/ActivityLogTable.php b/library/Director/Web/Table/ActivityLogTable.php index 5460bc2..a57b86e 100644 --- a/library/Director/Web/Table/ActivityLogTable.php +++ b/library/Director/Web/Table/ActivityLogTable.php @@ -2,14 +2,15 @@ namespace Icinga\Module\Director\Web\Table; -use gipfl\Format\LocalTimeFormat; +use DateTime; use gipfl\IcingaWeb2\Link; -use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Util; +use IntlDateFormatter; use ipl\Html\Html; use ipl\Html\HtmlElement; -class ActivityLogTable extends ZfQueryBasedTable +class ActivityLogTable extends IntlZfQueryBasedTable { protected $filters = []; @@ -27,9 +28,6 @@ class ActivityLogTable extends ZfQueryBasedTable 'object_type', ]; - /** @var LocalTimeFormat */ - protected $timeFormat; - protected $ranges = []; /** @var ?object */ @@ -44,7 +42,6 @@ class ActivityLogTable extends ZfQueryBasedTable public function __construct($db) { parent::__construct($db); - $this->timeFormat = new LocalTimeFormat(); } public function assemble() @@ -96,7 +93,9 @@ class ActivityLogTable extends ZfQueryBasedTable if (! $this->hasObjectFilter) { $columns[] = $this->makeRangeInfo($row->id); } - $columns[] = $this::td($this->timeFormat->getTime($row->ts_change_time)); + + + $columns[] = $this::td($this->getTime($row->ts_change_time)); return $this::tr($columns)->addAttributes(['class' => $action]); } @@ -107,7 +106,7 @@ class ActivityLogTable extends ZfQueryBasedTable */ protected function renderDayIfNew($timestamp) { - $day = $this->getDateFormatter()->getFullDay($timestamp); + $day = $this->getDateFormatter()->format((new DateTime())->setTimestamp($timestamp)); if ($this->lastDay !== $day) { $this->nextHeader()->add( @@ -206,7 +205,7 @@ class ActivityLogTable extends ZfQueryBasedTable $type = substr($type, 7); } - if (Util::hasPermission('director/showconfig')) { + if (Util::hasPermission(Permission::SHOW_CONFIG)) { // Later on replacing, service_set -> serviceset // multi column key :( diff --git a/library/Director/Web/Table/ApplyRulesTable.php b/library/Director/Web/Table/ApplyRulesTable.php index a861bac..a5379b5 100644 --- a/library/Director/Web/Table/ApplyRulesTable.php +++ b/library/Director/Web/Table/ApplyRulesTable.php @@ -6,6 +6,7 @@ use Icinga\Authentication\Auth; use Icinga\Data\Filter\Filter; use Icinga\Exception\IcingaException; use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\DbSelectParenthesis; use Icinga\Module\Director\Db\DbUtil; use Icinga\Module\Director\Db\IcingaObjectFilterHelper; use Icinga\Module\Director\IcingaConfig\AssignRenderer; @@ -20,6 +21,8 @@ use Zend_Db_Select as ZfSelect; class ApplyRulesTable extends ZfQueryBasedTable { + use TableWithBranchSupport; + protected $searchColumns = [ 'o.object_name', 'o.assign_filter', @@ -94,10 +97,14 @@ class ApplyRulesTable extends ZfQueryBasedTable // NOT (YET) static::td($this->createActionLinks($row))->setSeparator(' ') ]); + $classes = $this->getRowClasses($row); + if ($row->disabled === 'y') { - $tr->getAttributes()->add('class', 'disabled'); + $classes[] = 'disabled'; } + $tr->getAttributes()->add('class', $classes); + return $tr; } @@ -117,7 +124,8 @@ class ApplyRulesTable extends ZfQueryBasedTable $this->getQuery(), $template, 'o', - $inheritance + $inheritance, + $this->branchUuid ); return $this; @@ -146,7 +154,7 @@ class ApplyRulesTable extends ZfQueryBasedTable $links = []; $links[] = Link::create( Icon::create('sitemap'), - "${baseUrl}template/applytargets", + "{$baseUrl}template/applytargets", ['id' => $row->id], ['title' => $this->translate('Show affected Objects')] ); @@ -196,6 +204,15 @@ class ApplyRulesTable extends ZfQueryBasedTable return FilterRenderer::applyToQuery($filter, $query); } + protected function getRowClasses($row) + { + // TODO: remove isset, to figure out where it is missing + if (isset($row->branch_uuid) && $row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; + } + /** * @return IcingaObject @@ -216,6 +233,7 @@ class ApplyRulesTable extends ZfQueryBasedTable 'id' => 'o.id', 'uuid' => 'o.uuid', 'object_name' => 'o.object_name', + 'object_type' => 'o.object_type', 'disabled' => 'o.disabled', 'assign_filter' => 'o.assign_filter', 'apply_for' => '(NULL)', @@ -224,17 +242,92 @@ class ApplyRulesTable extends ZfQueryBasedTable if ($table === 'icinga_service') { $columns['apply_for'] = 'o.apply_for'; } + + $conn = $this->connection(); $query = $this->db()->select()->from( ['o' => $table], $columns - )->where( - "object_type = 'apply'" )->order('o.object_name'); - if ($this->type === 'service') { - $query->where('service_set_id IS NULL'); + if ($this->branchUuid) { + $columns = $this->branchifyColumns($columns); + $columns['branch_uuid'] = 'bo.branch_uuid'; + if ($conn->isPgsql()) { + $columns['imports'] = 'CONCAT(\'[\', ARRAY_TO_STRING(ARRAY_AGG' + . '(CONCAT(\'"\', sub_o.object_name, \'"\')), \',\'), \']\')'; + } else { + $columns['imports'] = 'CONCAT(\'[\', ' + . 'GROUP_CONCAT(CONCAT(\'"\', sub_o.object_name, \'"\')), \']\')'; + } + + $this->stripSearchColumnAliases(); + + $query->reset('columns'); + $right = clone($query); + + $query->columns($columns) + ->joinLeft( + ['oi' => $table . '_inheritance'], + 'o.id = oi.' . $this->getType() . '_id', + [] + )->joinLeft( + ['sub_o' => $table], + 'sub_o.id = oi.parent_' . $this->getType() . '_id', + [] + )->group(['o.id', 'bo.uuid', 'bo.branch_uuid']); + + $query->joinLeft( + ['bo' => "branched_$table"], + // TODO: PgHexFunc + $this->db()->quoteInto( + 'bo.uuid = o.uuid AND bo.branch_uuid = ?', + DbUtil::quoteBinaryLegacy($this->branchUuid->getBytes(), $this->db()) + ), + [] + )->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')"); + + if ($this->type === 'service') { + $query->where('o.service_set_id IS NULL AND bo.service_set IS NULL'); + } + + $columns['imports'] = 'bo.imports'; + + $right->columns($columns) + ->joinRight( + ['bo' => "branched_$table"], + 'bo.uuid = o.uuid', + [] + ) + ->where('o.uuid IS NULL') + ->where('bo.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes())); + + $query = $this->db()->select()->union([ + 'l' => new DbSelectParenthesis($query), + 'r' => new DbSelectParenthesis($right), + ]); + + $query = $this->db()->select()->from(['u' => $query]); + $query->order('object_name')->limit(100); + } else { + if ($this->type === 'service') { + $query->where('service_set_id IS NULL'); + } } + $query->where( + "object_type = 'apply'" + ); + + $this->applyRestrictions($query); + return $this->applyRestrictions($query); } + + /** + * @return Db + */ + public function connection() + { + return parent::connection(); + } } diff --git a/library/Director/Web/Table/BasketSnapshotTable.php b/library/Director/Web/Table/BasketSnapshotTable.php index 08f808a..6fe0e16 100644 --- a/library/Director/Web/Table/BasketSnapshotTable.php +++ b/library/Director/Web/Table/BasketSnapshotTable.php @@ -4,13 +4,12 @@ namespace Icinga\Module\Director\Web\Table; use ipl\Html\Html; use gipfl\IcingaWeb2\Link; -use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use Icinga\Date\DateFormatter; use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\DirectorObject\Automation\Basket; use RuntimeException; -class BasketSnapshotTable extends ZfQueryBasedTable +class BasketSnapshotTable extends IntlZfQueryBasedTable { use DbHelper; @@ -67,7 +66,7 @@ class BasketSnapshotTable extends ZfQueryBasedTable if (! is_object($summary) && ! is_array($summary)) { throw new RuntimeException(sprintf( 'Got invalid basket summary: %s ', - var_export($summary, 1) + var_export($summary, true) )); } diff --git a/library/Director/Web/Table/BranchActivityTable.php b/library/Director/Web/Table/BranchActivityTable.php index e7131ef..cbf940d 100644 --- a/library/Director/Web/Table/BranchActivityTable.php +++ b/library/Director/Web/Table/BranchActivityTable.php @@ -2,15 +2,14 @@ namespace Icinga\Module\Director\Web\Table; -use gipfl\Format\LocalTimeFormat; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Branch\BranchActivity; use Icinga\Module\Director\Util; use gipfl\IcingaWeb2\Link; -use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use Ramsey\Uuid\UuidInterface; -class BranchActivityTable extends ZfQueryBasedTable +class BranchActivityTable extends IntlZfQueryBasedTable { protected $extraParams = []; @@ -20,16 +19,12 @@ class BranchActivityTable extends ZfQueryBasedTable /** @var ?UuidInterface */ protected $objectUuid; - /** @var LocalTimeFormat */ - protected $timeFormat; - protected $linkToObject = true; public function __construct(UuidInterface $branchUuid, $db, UuidInterface $objectUuid = null) { $this->branchUuid = $branchUuid; $this->objectUuid = $objectUuid; - $this->timeFormat = new LocalTimeFormat(); parent::__construct($db); } @@ -45,7 +40,7 @@ class BranchActivityTable extends ZfQueryBasedTable $activity = BranchActivity::fromDbRow($row); return $this::tr([ $this::td($this->makeBranchLink($activity))->setSeparator(' '), - $this::td($this->timeFormat->getTime($ts)) + $this::td($this->getTime($ts)) ])->addAttributes(['class' => ['action-' . $activity->getAction(), 'branched']]); } @@ -75,7 +70,7 @@ class BranchActivityTable extends ZfQueryBasedTable { $type = preg_replace('/^icinga_/', '', $activity->getObjectTable()); - if (Util::hasPermission('director/showconfig')) { + if (Util::hasPermission(Permission::SHOW_CONFIG)) { // Later on replacing, service_set -> serviceset return [ '[' . $activity->getAuthor() . ']', diff --git a/library/Director/Web/Table/ChoicesTable.php b/library/Director/Web/Table/ChoicesTable.php index 4ba2460..9574520 100644 --- a/library/Director/Web/Table/ChoicesTable.php +++ b/library/Director/Web/Table/ChoicesTable.php @@ -44,7 +44,7 @@ class ChoicesTable extends ZfQueryBasedTable public function renderRow($row) { $type = $this->getType(); - $url = Url::fromPath("director/templatechoice/${type}", [ + $url = Url::fromPath("director/templatechoice/{$type}", [ 'name' => $row->object_name ]); @@ -56,7 +56,7 @@ class ChoicesTable extends ZfQueryBasedTable protected function prepareQuery() { $type = $this->getType(); - $table = "icinga_${type}_template_choice"; + $table = "icinga_{$type}_template_choice"; return $this->db() ->select() ->from(['o' => $table], 'object_name') diff --git a/library/Director/Web/Table/CustomvarTable.php b/library/Director/Web/Table/CustomvarTable.php index f9a3844..1aead19 100644 --- a/library/Director/Web/Table/CustomvarTable.php +++ b/library/Director/Web/Table/CustomvarTable.php @@ -91,11 +91,11 @@ class CustomvarTable extends ZfQueryBasedTable $columns["cnt_$type"] = 'COUNT(*)'; $columns["distinct_$type"] = 'COUNT(DISTINCT varvalue)'; return $db->select()->from( - ['v' => "icinga_${type}_var"], + ['v' => "icinga_{$type}_var"], $columns )->join( - ['o' => "icinga_${type}"], - "o.id = v.${type}_id", + ['o' => "icinga_{$type}"], + "o.id = v.{$type}_id", [] )->where('o.object_type != ?', 'external_object')->group('varname'); } diff --git a/library/Director/Web/Table/CustomvarVariantsTable.php b/library/Director/Web/Table/CustomvarVariantsTable.php index 80fca70..8447e36 100644 --- a/library/Director/Web/Table/CustomvarVariantsTable.php +++ b/library/Director/Web/Table/CustomvarVariantsTable.php @@ -108,11 +108,11 @@ class CustomvarVariantsTable extends ZfQueryBasedTable $columns["cnt_$type"] = 'COUNT(*)'; $columns['format'] = 'v.format'; return $db->select()->from( - ['v' => "icinga_${type}_var"], + ['v' => "icinga_{$type}_var"], $columns )->join( - ['o' => "icinga_${type}"], - "o.id = v.${type}_id", + ['o' => "icinga_{$type}"], + "o.id = v.{$type}_id", [] )->where( 'v.varname = ?', diff --git a/library/Director/Web/Table/DatafieldTable.php b/library/Director/Web/Table/DatafieldTable.php index 4b321d7..0062626 100644 --- a/library/Director/Web/Table/DatafieldTable.php +++ b/library/Director/Web/Table/DatafieldTable.php @@ -5,12 +5,13 @@ namespace Icinga\Module\Director\Web\Table; use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use Zend_Db_Adapter_Abstract as ZfDbAdapter; +use Zend_Db_Expr as DbExpr; use Zend_Db_Select as ZfDbSelect; class DatafieldTable extends ZfQueryBasedTable { protected $searchColumns = [ - 'df.varname', + 'lc_varname', 'df.caption', ]; @@ -19,6 +20,7 @@ class DatafieldTable extends ZfQueryBasedTable return [ 'id' => 'df.id', 'varname' => 'df.varname', + 'lc_varname' => new DbExpr('LOWER(df.varname)'), 'caption' => 'df.caption', 'description' => 'df.description', 'datatype' => 'df.datatype', @@ -88,6 +90,15 @@ class DatafieldTable extends ZfQueryBasedTable )->group('df.id')->group('df.varname')->group('dfc.category_name')->order('caption ASC'); } + public function search($search) + { + if ($search !== null) { + $search = strtolower($search); + } + + return parent::search($search); + } + /** * @param $type * @param ZfDbAdapter $db @@ -96,7 +107,7 @@ class DatafieldTable extends ZfQueryBasedTable */ protected function makeDatafieldSub($type, ZfDbAdapter $db) { - return $db->select()->from("icinga_${type}_field", [ + return $db->select()->from("icinga_{$type}_field", [ 'cnt' => 'COUNT(*)', 'datafield_id' ])->group('datafield_id'); @@ -110,7 +121,7 @@ class DatafieldTable extends ZfQueryBasedTable */ protected function makeVarSub($type, ZfDbAdapter $db) { - return $db->select()->from("icinga_${type}_var", [ + return $db->select()->from("icinga_{$type}_var", [ 'cnt' => 'COUNT(*)', 'varname' ])->group('varname'); diff --git a/library/Director/Web/Table/DependencyTemplateUsageTable.php b/library/Director/Web/Table/DependencyTemplateUsageTable.php index d7537c5..2c1de50 100644 --- a/library/Director/Web/Table/DependencyTemplateUsageTable.php +++ b/library/Director/Web/Table/DependencyTemplateUsageTable.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Web\Table; +use Icinga\Module\Director\Db; + class DependencyTemplateUsageTable extends TemplateUsageTable { public function getTypes() @@ -12,11 +14,15 @@ class DependencyTemplateUsageTable extends TemplateUsageTable ]; } - protected function getTypeSummaryDefinitions() + protected function getSummaryTables(string $templateType, Db $connection) { return [ - 'templates' => $this->getSummaryLine('template'), - 'applyrules' => $this->getSummaryLine('apply'), + 'templates' => TemplatesTable::create( + $templateType, + $connection + ), + 'applyrules' => ApplyRulesTable::create($templateType, $connection) + ->setBranchUuid($this->branchUuid) ]; } } diff --git a/library/Director/Web/Table/DeploymentLogTable.php b/library/Director/Web/Table/DeploymentLogTable.php index 2d5cb94..4849218 100644 --- a/library/Director/Web/Table/DeploymentLogTable.php +++ b/library/Director/Web/Table/DeploymentLogTable.php @@ -3,10 +3,9 @@ namespace Icinga\Module\Director\Web\Table; use gipfl\IcingaWeb2\Link; -use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use Icinga\Date\DateFormatter; -class DeploymentLogTable extends ZfQueryBasedTable +class DeploymentLogTable extends IntlZfQueryBasedTable { use DbHelper; diff --git a/library/Director/Web/Table/GroupMemberTable.php b/library/Director/Web/Table/GroupMemberTable.php index b0814ad..c24c6af 100644 --- a/library/Director/Web/Table/GroupMemberTable.php +++ b/library/Director/Web/Table/GroupMemberTable.php @@ -109,7 +109,7 @@ class GroupMemberTable extends ZfQueryBasedTable ]; } - $url = Url::fromPath("director/${type}", $params); + $url = Url::fromPath("director/{$type}", $params); $tr = $this::tr(); @@ -163,7 +163,7 @@ class GroupMemberTable extends ZfQueryBasedTable 'o.id', 'o.object_type', 'o.object_name', - 'membership_type' => "CASE WHEN go.${type}_id IS NULL THEN 'apply' ELSE 'direct' END" + 'membership_type' => "CASE WHEN go.{$type}_id IS NULL THEN 'apply' ELSE 'direct' END" ]; if ($this->group === null) { @@ -176,19 +176,19 @@ class GroupMemberTable extends ZfQueryBasedTable } $query = $this->db()->select()->from( - ['gro' => "icinga_${type}group_${type}_resolved"], + ['gro' => "icinga_{$type}group_{$type}_resolved"], $columns )->join( - ['o' => "icinga_${type}"], - "o.id = gro.${type}_id", + ['o' => "icinga_{$type}"], + "o.id = gro.{$type}_id", [] )->join( - ['g' => "icinga_${type}group"], - "gro.${type}group_id = g.id", + ['g' => "icinga_{$type}group"], + "gro.{$type}group_id = g.id", [] )->joinLeft( - ['go' => "icinga_${type}group_${type}"], - "go.${type}_id = o.id AND go.${type}group_id = g.id", + ['go' => "icinga_{$type}group_{$type}"], + "go.{$type}_id = o.id AND go.{$type}group_id = g.id", [] )->order('o.object_name'); diff --git a/library/Director/Web/Table/HostTemplateUsageTable.php b/library/Director/Web/Table/HostTemplateUsageTable.php index 2d1ee2f..672691f 100644 --- a/library/Director/Web/Table/HostTemplateUsageTable.php +++ b/library/Director/Web/Table/HostTemplateUsageTable.php @@ -11,12 +11,4 @@ class HostTemplateUsageTable extends TemplateUsageTable 'objects' => $this->translate('Objects'), ]; } - - protected function getTypeSummaryDefinitions() - { - return [ - 'templates' => $this->getSummaryLine('template'), - 'objects' => $this->getSummaryLine('object'), - ]; - } } diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php index c205e66..2c3dbc4 100644 --- a/library/Director/Web/Table/IcingaServiceSetServiceTable.php +++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php @@ -12,6 +12,7 @@ use Icinga\Module\Director\Objects\IcingaServiceSet; use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use gipfl\IcingaWeb2\Url; +use Ramsey\Uuid\Uuid; class IcingaServiceSetServiceTable extends ZfQueryBasedTable { @@ -122,9 +123,12 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable ]; $url = 'director/host/servicesetservice'; } else { + if (is_resource($row->uuid)) { + $row->uuid =stream_get_contents($row->uuid); + } + $params = [ - 'name' => $row->service, - 'set' => $row->service_set + 'uuid' => Uuid::fromBytes($row->uuid)->toString(), ]; $url = 'director/service'; } @@ -194,14 +198,28 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable $connection = $this->connection(); assert($connection instanceof Db); $builder = new ServiceSetQueryBuilder($connection, $this->branchUuid); - return $builder->selectServicesForSet($this->set)->limit(100); + $query = $builder->selectServicesForSet($this->set); + $alias = $this->branchUuid ? 'u' : 'o'; + + if ($this->affectedHost) { + if ($hostId = $this->affectedHost->get('id')) { + $query->joinLeft( + ['hsb' => 'icinga_host_service_blacklist'], + $this->db()->quoteInto("$alias.id = hsb.service_id AND hsb.host_id = ?", $hostId), + [] + )->columns([ + 'blacklisted' => "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END" + ]); + } + } + + return $query->limit(100); } protected function createFakeRemoveLinkForReadonlyView() { return Html::tag('span', [ - 'class' => 'icon-paste', - 'style' => 'float: right; font-weight: normal', + 'class' => ['icon-paste', 'seviceset-obj-link'], ], $this->host->getObjectName()); } @@ -209,8 +227,7 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable { $hostname = $host->getObjectName(); return Link::create($hostname, 'director/host/services', ['name' => $hostname], [ - 'class' => 'icon-paste', - 'style' => 'float: right; font-weight: normal', + 'class' => ['icon-paste', 'seviceset-obj-link'], 'data-base-target' => '_next', 'title' => sprintf( $this->translate('This set has been inherited from %s'), diff --git a/library/Director/Web/Table/IntlZfQueryBasedTable.php b/library/Director/Web/Table/IntlZfQueryBasedTable.php new file mode 100644 index 0000000..81ef14c --- /dev/null +++ b/library/Director/Web/Table/IntlZfQueryBasedTable.php @@ -0,0 +1,51 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use DateTime; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use IntlDateFormatter; +use Locale; + +abstract class IntlZfQueryBasedTable extends ZfQueryBasedTable +{ + protected function getDateFormatter() + { + return (new IntlDateFormatter( + Locale::getDefault(), + IntlDateFormatter::FULL, + IntlDateFormatter::NONE + )); + } + + /** + * @param int $timestamp + */ + protected function renderDayIfNew($timestamp) + { + $day = $this->getDateFormatter()->format((new DateTime())->setTimestamp($timestamp)); + + if ($this->lastDay !== $day) { + $this->nextHeader()->add( + $this::th($day, [ + 'colspan' => 2, + 'class' => 'table-header-day' + ]) + ); + + $this->lastDay = $day; + $this->nextBody(); + } + } + + protected function getTime(int $timeStamp) + { + $timeFormatter = $this->getDateFormatter(); + + $timeFormatter->setPattern( + in_array(Locale::getDefault(), ['en_US', 'en_US.UTF-8']) ? 'h:mm:ss a': 'H:mm:ss' + ); + + return $timeFormatter->format((new DateTime())->setTimestamp($timeStamp)); + } +} diff --git a/library/Director/Web/Table/NotificationTemplateUsageTable.php b/library/Director/Web/Table/NotificationTemplateUsageTable.php index da411a3..d8cd3d8 100644 --- a/library/Director/Web/Table/NotificationTemplateUsageTable.php +++ b/library/Director/Web/Table/NotificationTemplateUsageTable.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Web\Table; +use Icinga\Module\Director\Db; + class NotificationTemplateUsageTable extends TemplateUsageTable { public function getTypes() @@ -12,11 +14,15 @@ class NotificationTemplateUsageTable extends TemplateUsageTable ]; } - protected function getTypeSummaryDefinitions() + protected function getSummaryTables(string $templateType, Db $connection) { return [ - 'templates' => $this->getSummaryLine('template'), - 'applyrules' => $this->getSummaryLine('apply', 'o.host_id IS NULL'), + 'templates' => TemplatesTable::create( + $templateType, + $connection + ), + 'applyrules' => ApplyRulesTable::create($templateType, $connection) + ->setBranchUuid($this->branchUuid) ]; } } diff --git a/library/Director/Web/Table/ObjectSetTable.php b/library/Director/Web/Table/ObjectSetTable.php index 2773841..4df9bdd 100644 --- a/library/Director/Web/Table/ObjectSetTable.php +++ b/library/Director/Web/Table/ObjectSetTable.php @@ -2,7 +2,9 @@ namespace Icinga\Module\Director\Web\Table; +use gipfl\IcingaWeb2\Zf1\Db\FilterRenderer; use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; use Icinga\Module\Director\Db; use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; @@ -28,6 +30,8 @@ class ObjectSetTable extends ZfQueryBasedTable /** @var Auth */ private $auth; + protected $queries = []; + public static function create($type, Db $db, Auth $auth) { $table = new static($db); @@ -53,7 +57,7 @@ class ObjectSetTable extends ZfQueryBasedTable 'uuid' => Uuid::fromBytes(Db\DbUtil::binaryResult($row->uuid))->toString(), ]; - $url = Url::fromPath("director/${type}set", $params); + $url = Url::fromPath("director/{$type}set", $params); $classes = $this->getRowClasses($row); $tr = static::tr([ @@ -85,7 +89,7 @@ class ObjectSetTable extends ZfQueryBasedTable { $type = $this->getType(); - $table = "icinga_${type}_set"; + $table = "icinga_{$type}_set"; $columns = [ 'id' => 'os.id', 'uuid' => 'os.uuid', @@ -106,15 +110,15 @@ class ObjectSetTable extends ZfQueryBasedTable ['os' => $table], $columns )->joinLeft( - ['o' => "icinga_${type}"], - "o.${type}_set_id = os.id", + ['o' => "icinga_{$type}"], + "o.{$type}_set_id = os.id", [] ); $nameFilter = new FilterByNameRestriction( $this->connection(), $this->auth, - "${type}_set" + "{$type}_set" ); $nameFilter->applyToQuery($query, 'os'); /** @var Db $conn */ @@ -145,7 +149,20 @@ class ObjectSetTable extends ZfQueryBasedTable $query->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid'); $right->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid'); } - + $right->joinLeft( + ['bo' => "branched_icinga_{$type}"], + "bo.{$type}_set = bos.object_name", + [] + )->group(['bo.object_name', 'o.object_name']); + $query->joinLeft( + ['bo' => "branched_icinga_{$type}"], + "bo.{$type}_set = bos.object_name", + [] + )->group(['bo.object_name', 'o.object_name']); + $this->queries = [ + $query, + $right + ]; $query = $this->db()->select()->union([ 'l' => new DbSelectParenthesis($query), 'r' => new DbSelectParenthesis($right), @@ -168,16 +185,16 @@ class ObjectSetTable extends ZfQueryBasedTable ->group('assign_filter') ->group('description') ->group('count_services'); - }; + } } else { // Disabled for now, check for correctness: // $query->joinLeft( - // ['osi' => "icinga_${type}_set_inheritance"], - // "osi.parent_${type}_set_id = os.id", + // ['osi' => "icinga_{$type}_set_inheritance"], + // "osi.parent_{$type}_set_id = os.id", // [] // )->joinLeft( - // ['oso' => "icinga_${type}_set"], - // "oso.id = oso.${type}_set_id", + // ['oso' => "icinga_{$type}_set"], + // "oso.id = oso.{$type}_set_id", // [] // ); // 'count_hosts' => 'COUNT(DISTINCT oso.id)', @@ -196,11 +213,40 @@ class ObjectSetTable extends ZfQueryBasedTable ->group('os.assign_filter') ->group('os.description'); }; + $this->queries = [$query]; } return $query; } + public function search($search) + { + if (! empty($search)) { + $columns = $this->getSearchColumns(); + if (strpos($search, ' ') === false) { + $filter = Filter::matchAny(); + foreach ($columns as $column) { + $filter->addFilter(Filter::expression($column, '=', "*$search*")); + } + } else { + $filter = Filter::matchAll(); + foreach (explode(' ', $search) as $s) { + $sub = Filter::matchAny(); + foreach ($columns as $column) { + $sub->addFilter(Filter::expression($column, '=', "*$s*")); + } + $filter->addFilter($sub); + } + } + + foreach ($this->queries as $query) { + FilterRenderer::applyToQuery($filter, $query); + } + } + + return $this; + } + /** * @return Db */ diff --git a/library/Director/Web/Table/ObjectsTable.php b/library/Director/Web/Table/ObjectsTable.php index 792cb6d..4ad1166 100644 --- a/library/Director/Web/Table/ObjectsTable.php +++ b/library/Director/Web/Table/ObjectsTable.php @@ -14,6 +14,7 @@ use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use gipfl\IcingaWeb2\Url; use Ramsey\Uuid\Uuid; +use Zend_Db_Adapter_Pdo_Pgsql; use Zend_Db_Select as ZfSelect; class ObjectsTable extends ZfQueryBasedTable @@ -50,12 +51,18 @@ class ObjectsTable extends ZfQueryBasedTable /** @var Auth */ private $auth; + public function __construct($db, Auth $auth) + { + $this->auth = $auth; + parent::__construct($db); + } + /** * @param $type * @param Db $db * @return static */ - public static function create($type, Db $db) + public static function create($type, Db $db, Auth $auth) { $class = __NAMESPACE__ . '\\ObjectsTable' . ucfirst($type); if (! class_exists($class)) { @@ -63,7 +70,7 @@ class ObjectsTable extends ZfQueryBasedTable } /** @var static $table */ - $table = new $class($db); + $table = new $class($db, $auth); $table->type = $type; return $table; } @@ -84,20 +91,6 @@ class ObjectsTable extends ZfQueryBasedTable return $this; } - /** - * @return Auth - */ - public function getAuth() - { - return $this->auth; - } - - public function setAuth(Auth $auth) - { - $this->auth = $auth; - return $this; - } - public function filterObjectType($type) { $this->filterObjectType = $type; @@ -124,11 +117,17 @@ class ObjectsTable extends ZfQueryBasedTable IcingaObject $template, $inheritance = Db\IcingaObjectFilterHelper::INHERIT_DIRECT ) { + if ($this->branchUuid) { + $tableAlias = 'u'; + } else { + $tableAlias = 'o'; + } IcingaObjectFilterHelper::filterByTemplate( $this->getQuery(), $template, - 'o', - $inheritance + $tableAlias, + $inheritance, + $this->branchUuid ); return $this; @@ -142,7 +141,7 @@ class ObjectsTable extends ZfQueryBasedTable protected function renderObjectNameColumn($row) { $type = $this->baseObjectUrl; - $url = Url::fromPath("director/${type}", [ + $url = Url::fromPath("director/{$type}", [ 'uuid' => Uuid::fromBytes($row->uuid)->toString() ]); @@ -227,11 +226,10 @@ class ObjectsTable extends ZfQueryBasedTable { /** @var Db $db */ $db = $this->connection(); - $auth = $this->getAuth(); return [ - new HostgroupRestriction($db, $auth), - new FilterByNameRestriction($db, $auth, $this->getDummyObject()->getShortTableName()) + new HostgroupRestriction($db, $this->auth), + new FilterByNameRestriction($db, $this->auth, $this->getDummyObject()->getShortTableName()) ]; } @@ -279,7 +277,39 @@ class ObjectsTable extends ZfQueryBasedTable $conn->quoteBinary($this->branchUuid->getBytes()) ), [] - )->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')"); + ); + + // keep the imported templates as columns + $leftColumns = $columns; + $rightColumns = $columns; + + if ($this->db() instanceof Zend_Db_Adapter_Pdo_Pgsql) { + $leftColumns['imports'] = 'CONCAT(\'[\', ARRAY_TO_STRING(ARRAY_AGG' + . '(CONCAT(\'"\', sub_o.object_name, \'"\')), \',\'), \']\')'; + } else { + $leftColumns['imports'] = 'CONCAT(\'[\', ' + . 'GROUP_CONCAT(CONCAT(\'"\', sub_o.object_name, \'"\')), \']\')'; + } + + $query->reset('columns'); + + $query->columns($leftColumns) + ->joinLeft( + ['oi' => $table . '_inheritance'], + 'o.id = oi.' . $this->getType() . '_id', + [] + )->joinLeft( + ['sub_o' => $table], + 'sub_o.id = oi.parent_' . $this->getType() . '_id', + [] + )->group(['o.id', 'bo.uuid', 'bo.branch_uuid']); + + $rightColumns['imports'] = 'bo.imports'; + + $right->reset('columns'); + $right->columns($rightColumns); + + $query->where("(bo.branch_deleted IS NULL OR bo.branch_deleted = 'n')"); $this->applyObjectTypeFilter($query, $right); $right->joinRight( ['bo' => "branched_$table"], @@ -298,6 +328,7 @@ class ObjectsTable extends ZfQueryBasedTable $query->order('object_name')->limit(100); } else { $this->applyObjectTypeFilter($query); + $query = $this->applyRestrictions($query); $query->order('o.object_name')->limit(100); } diff --git a/library/Director/Web/Table/ObjectsTableService.php b/library/Director/Web/Table/ObjectsTableService.php index 2d4ad41..c9bce72 100644 --- a/library/Director/Web/Table/ObjectsTableService.php +++ b/library/Director/Web/Table/ObjectsTableService.php @@ -203,8 +203,14 @@ class ObjectsTableService extends ObjectsTable 'hsb.service_id = o.id AND hsb.host_id = o.host_id', [] )->where('o.service_set_id IS NULL') + ->group(['o.id', 'h.id','hsb.service_id', 'hsb.host_id']) ->order('o.object_name')->order('h.object_name'); + if ($this->branchUuid) { + $subQuery->where('bo.service_set IS NULL') + ->group(['bo.uuid', 'bo.branch_uuid']); + } + if ($this->host) { if ($this->branchUuid) { $subQuery->where('COALESCE(h.object_name, bo.host) = ?', $this->host->getObjectName()); diff --git a/library/Director/Web/Table/ObjectsTableSetMembers.php b/library/Director/Web/Table/ObjectsTableSetMembers.php new file mode 100644 index 0000000..6b18ac9 --- /dev/null +++ b/library/Director/Web/Table/ObjectsTableSetMembers.php @@ -0,0 +1,255 @@ +<?php + +namespace Icinga\Module\Director\Web\Table; + +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db; +use gipfl\IcingaWeb2\Link; +use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; +use gipfl\IcingaWeb2\Url; +use Icinga\Module\Director\Db\DbSelectParenthesis; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Restriction\FilterByNameRestriction; +use Ramsey\Uuid\Uuid; + +class ObjectsTableSetMembers extends ZfQueryBasedTable +{ + use TableWithBranchSupport; + + protected $searchColumns = [ + 'os.object_name', + 'o.object_name', + ]; + + private $type; + + /** @var IcingaObject */ + protected $dummyObject; + + protected $baseObjectUrl; + + /** @var Auth */ + private $auth; + + public static function create($type, Db $db, Auth $auth) + { + $table = new static($db); + $table->type = $type; + $table->auth = $auth; + return $table; + } + + public function getType() + { + return $this->type; + } + + public function getColumnsToBeRendered() + { + return [ + 'os.object_name' => 'Service Set', + 'o.object_name' => 'Service Name' + ]; + } + + public function setBaseObjectUrl($url) + { + $this->baseObjectUrl = $url; + + return $this; + } + + protected function getRowClasses($row) + { + // TODO: remove isset, to figure out where it is missing + if (isset($row->branch_uuid) && $row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; + } + + /** + * Should be triggered from renderRow, still unused. + * + * @param IcingaObject $template + * @param string $inheritance + * @return $this + * @throws \Icinga\Exception\ProgrammingError + */ + public function filterTemplate( + IcingaObject $template, + $inheritance = IcingaObjectFilterHelper::INHERIT_DIRECT + ) { + IcingaObjectFilterHelper::filterByTemplate( + $this->getQuery(), + $template, + 'o', + $inheritance, + $this->branchUuid + ); + + return $this; + } + + + public function renderRow($row) + { + $url = Url::fromPath('director/service/edit', [ + 'name' => $row->object_name, + 'uuid' => Uuid::fromBytes($row->uuid)->toString(), + ]); + + return static::tr([ + static::td([ + Link::create($row->service_set, $url), + ]), + static::td($row->object_name), + ])->addAttributes(['class' => $this->getRowClasses($row)]); + } + + /** + * @return IcingaObject + */ + protected function getDummyObject() + { + if ($this->dummyObject === null) { + $type = $this->type; + $this->dummyObject = IcingaObject::createByType($type); + } + return $this->dummyObject; + } + + protected function prepareQuery() + { + $table = $this->getDummyObject()->getTableName(); + $type = $this->getType(); + + $columns = [ + 'id' => 'o.id', + 'uuid' => 'o.uuid', + 'service_set' => 'os.object_name', + 'object_name' => 'o.object_name', + 'object_type' => 'os.object_type', + 'assign_filter' => 'os.assign_filter', + 'description' => 'os.description', + ]; + + $query = $this->db()->select()->from( + ['o' => $table], + $columns + )->joinLeft( + ['os' => "icinga_{$type}_set"], + "o.{$type}_set_id = os.id", + [] + )->where('o.host_id IS NULL'); + + $nameFilter = new FilterByNameRestriction( + $this->connection(), + $this->auth, + "{$type}_set" + ); + $nameFilter->applyToQuery($query, 'os'); + + if ($this->branchUuid) { + $columns['branch_uuid'] = 'bos.branch_uuid'; + $conn = $this->connection(); + if ($conn->isPgsql()) { + $columns['imports'] = 'CONCAT(\'[\', ARRAY_TO_STRING(ARRAY_AGG' + . '(CONCAT(\'"\', sub_o.object_name, \'"\')), \',\'), \']\')'; + } else { + $columns['imports'] = 'CONCAT(\'[\', ' + . 'GROUP_CONCAT(CONCAT(\'"\', sub_o.object_name, \'"\')), \']\')'; + } + + $columns = $this->branchifyColumns($columns); + $this->stripSearchColumnAliases(); + + $query->reset('columns'); + $right = clone($query); + $conn = $this->connection(); + + $query->columns($columns)->joinLeft( + ['bos' => "branched_icinga_{$type}_set"], + // TODO: PgHexFunc + $this->db()->quoteInto( + 'bos.uuid = os.uuid AND bos.branch_uuid = ?', + $conn->quoteBinary($this->branchUuid->getBytes()) + ), + [] + )->joinLeft( + ['oi' => $table . '_inheritance'], + 'o.id = oi.' . $this->getType() . '_id', + [] + )->joinLeft( + ['sub_o' => $table], + 'sub_o.id = oi.parent_' . $this->getType() . '_id', + [] + )->where("(bos.branch_deleted IS NULL OR bos.branch_deleted = 'n')"); + + $columns['imports'] = 'bo.imports'; + $right->columns($columns)->joinRight( + ['bos' => "branched_icinga_{$type}_set"], + 'bos.uuid = os.uuid', + [] + ) + ->where('os.uuid IS NULL') + ->where('bos.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes())); + $query->group('COALESCE(os.uuid, bos.uuid)'); + $right->group('COALESCE(os.uuid, bos.uuid)'); + if ($conn->isPgsql()) { + // This is ugly, might want to modify the query - even a subselect looks better + $query->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid')->group('o.id'); + $right->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid')->group('o.id'); + } + $right->joinLeft( + ['bo' => "branched_icinga_{$type}"], + "bo.{$type}_set = bos.object_name", + [] + )->group(['bo.object_name', 'o.object_name', 'bo.uuid', 'bo.imports']); + $query->joinLeft( + ['bo' => "branched_icinga_{$type}"], + "bo.{$type}_set = bos.object_name", + [] + )->group(['bo.object_name', 'o.object_name', 'bo.uuid']); + + $query = $this->db()->select()->union([ + 'l' => new DbSelectParenthesis($query), + 'r' => new DbSelectParenthesis($right), + ]); + $query = $this->db()->select()->from(['u' => $query]); + $query->order('object_name')->limit(100); + + $query + ->group('uuid') + ->where('object_type = ?', 'template') + ->order('object_name'); + if ($conn->isPgsql()) { + $query + ->group('uuid') + ->group('id') + ->group('imports') + ->group('branch_uuid') + ->group('object_name') + ->group('object_type') + ->group('assign_filter') + ->group('description') + ->group('service_set'); + } + } else { + $query + ->where('o.object_type = ?', 'object') + ->order('os.object_name'); + } + + return $query; + } + + /** + * @return Db + */ + public function connection() + { + return parent::connection(); + } +} diff --git a/library/Director/Web/Table/ReadOnlyFormAvpTable.php b/library/Director/Web/Table/ReadOnlyFormAvpTable.php deleted file mode 100644 index c3b44f3..0000000 --- a/library/Director/Web/Table/ReadOnlyFormAvpTable.php +++ /dev/null @@ -1,113 +0,0 @@ -<?php - -namespace Icinga\Module\Director\Web\Table; - -use Icinga\Module\Director\PlainObjectRenderer; -use Icinga\Module\Director\Web\Form\QuickForm; -use Zend_Form_Element as ZfElement; -use Zend_Form_DisplayGroup as ZfDisplayGroup; - -class ReadOnlyFormAvpTable -{ - protected $form; - - public function __construct(QuickForm $form) - { - $this->form = $form; - } - - protected function renderDisplayGroups(QuickForm $form) - { - $html = ''; - - foreach ($form->getDisplayGroups() as $group) { - $elements = $this->filterGroupElements($group); - - if (empty($elements)) { - continue; - } - - $html .= '<tr><th colspan="2" style="text-align: right">' . $group->getLegend() . '</th></tr>'; - $html .= $this->renderElements($elements); - } - - return $html; - } - - /** - * @param ZfDisplayGroup $group - * @return ZfElement[] - */ - protected function filterGroupElements(ZfDisplayGroup $group) - { - $blacklist = array('disabled', 'assign_filter'); - $elements = array(); - /** @var ZfElement $element */ - foreach ($group->getElements() as $element) { - if ($element->getValue() === null) { - continue; - } - - if ($element->getType() === 'Zend_Form_Element_Hidden') { - continue; - } - - if (in_array($element->getName(), $blacklist)) { - continue; - } - - - $elements[] = $element; - } - - return $elements; - } - - protected function renderElements($elements) - { - $html = ''; - foreach ($elements as $element) { - $html .= $this->renderElement($element); - } - - return $html; - } - - /** - * @param ZfElement $element - * - * @return string - */ - protected function renderElement(ZfElement $element) - { - $value = $element->getValue(); - return '<tr><th>' - . $this->escape($element->getLabel()) - . '</th><td>' - . $this->renderValue($value) - . '</td></tr>'; - } - - protected function renderValue($value) - { - if (is_string($value)) { - return $this->escape($value); - } elseif (is_array($value)) { - return $this->escape(implode(', ', $value)); - } - return $this->escape(PlainObjectRenderer::render($value)); - } - - protected function escape($string) - { - return htmlspecialchars($string); - } - - public function render() - { - $this->form->initializeForObject(); - return '<table class="name-value-table">' . "\n" - . $this->renderDisplayGroups($this->form) - . '</table>'; - } -} diff --git a/library/Director/Web/Table/ServiceTemplateUsageTable.php b/library/Director/Web/Table/ServiceTemplateUsageTable.php index 82f9643..c2806f6 100644 --- a/library/Director/Web/Table/ServiceTemplateUsageTable.php +++ b/library/Director/Web/Table/ServiceTemplateUsageTable.php @@ -2,6 +2,9 @@ namespace Icinga\Module\Director\Web\Table; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db; + class ServiceTemplateUsageTable extends TemplateUsageTable { public function getTypes() @@ -10,18 +13,27 @@ class ServiceTemplateUsageTable extends TemplateUsageTable 'templates' => $this->translate('Templates'), 'objects' => $this->translate('Objects'), 'applyrules' => $this->translate('Apply Rules'), - // 'setmembers' => $this->translate('Set Members'), + 'setmembers' => $this->translate('Set Members'), ]; } - protected function getTypeSummaryDefinitions() + protected function getSummaryTables(string $templateType, Db $connection) { + $auth = Auth::getInstance(); return [ - 'templates' => $this->getSummaryLine('template'), - 'objects' => $this->getSummaryLine('object'), - 'applyrules' => $this->getSummaryLine('apply', 'o.service_set_id IS NULL'), - // TODO: re-enable - // 'setmembers' => $this->getSummaryLine('apply', 'o.service_set_id IS NOT NULL'), + 'templates' => TemplatesTable::create( + $templateType, + $connection + ), + 'objects' => ObjectsTable::create($templateType, $connection, $this->auth) + ->setBranchUuid($this->branchUuid), + 'applyrules' => ApplyRulesTable::create($templateType, $connection) + ->setBranchUuid($this->branchUuid), + 'setmembers' => ObjectsTableSetMembers::create( + $templateType, + $connection, + $auth + ) ]; } } diff --git a/library/Director/Web/Table/SyncRunTable.php b/library/Director/Web/Table/SyncRunTable.php index e08aad7..19f2678 100644 --- a/library/Director/Web/Table/SyncRunTable.php +++ b/library/Director/Web/Table/SyncRunTable.php @@ -2,22 +2,17 @@ namespace Icinga\Module\Director\Web\Table; -use gipfl\Format\LocalTimeFormat; use Icinga\Module\Director\Objects\SyncRule; use gipfl\IcingaWeb2\Link; -use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; -class SyncRunTable extends ZfQueryBasedTable +class SyncRunTable extends IntlZfQueryBasedTable { /** @var SyncRule */ protected $rule; - protected $timeFormat; - public function __construct(SyncRule $rule) { parent::__construct($rule->getConnection()); - $this->timeFormat = new LocalTimeFormat(); $this->getAttributes() ->set('data-base-target', '_self') ->add('class', 'history'); @@ -31,7 +26,7 @@ class SyncRunTable extends ZfQueryBasedTable return $this::tr([ $this::td($this->makeSummary($row)), $this::td(new Link( - $this->timeFormat->getTime($time), + $this->getTime($time), 'director/syncrule/history', [ 'id' => $row->rule_id, diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php index 7c5b15c..9e412c3 100644 --- a/library/Director/Web/Table/TableWithBranchSupport.php +++ b/library/Director/Web/Table/TableWithBranchSupport.php @@ -56,6 +56,9 @@ trait TableWithBranchSupport $result[$alias] = $column; } + if (isset($result['count_services'])) { + $result['count_services'] = 'COUNT(DISTINCT COALESCE(o.uuid, bo.uuid))'; + } return $result; } diff --git a/library/Director/Web/Table/TemplateUsageTable.php b/library/Director/Web/Table/TemplateUsageTable.php index 66e56ea..376a499 100644 --- a/library/Director/Web/Table/TemplateUsageTable.php +++ b/library/Director/Web/Table/TemplateUsageTable.php @@ -2,9 +2,12 @@ namespace Icinga\Module\Director\Web\Table; +use Icinga\Authentication\Auth; use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; use Icinga\Module\Director\Objects\IcingaObject; -use Icinga\Module\Director\Resolver\TemplateTree; use gipfl\IcingaWeb2\Link; use ipl\Html\Table; use gipfl\Translation\TranslationHelper; @@ -13,10 +16,17 @@ class TemplateUsageTable extends Table { use TranslationHelper; + use TableWithBranchSupport; + + /** @var Auth */ + protected $auth; + protected $defaultAttributes = ['class' => 'pivot']; protected $objectType; + protected $searchColumns = []; + public function getTypes() { return [ @@ -25,26 +35,22 @@ class TemplateUsageTable extends Table ]; } - protected function getTypeSummaryDefinitions() - { - return [ - 'templates' => $this->getSummaryLine('template'), - 'objects' => $this->getSummaryLine('object'), - ]; - } - /** * @param IcingaObject $template + * @param Branch|null $branch + * * @return TemplateUsageTable + * + * @throws ProgrammingError */ - public static function forTemplate(IcingaObject $template) + public static function forTemplate(IcingaObject $template, Auth $auth, Branch $branch = null) { $type = ucfirst($template->getShortTableName()); - $class = __NAMESPACE__ . "\\${type}TemplateUsageTable"; + $class = __NAMESPACE__ . "\\{$type}TemplateUsageTable"; if (class_exists($class)) { - return new $class($template); + return new $class($template, $auth, $branch); } else { - return new static($template); + return new static($template, $auth, $branch); } } @@ -58,8 +64,9 @@ class TemplateUsageTable extends Table ]; } - protected function __construct(IcingaObject $template) + protected function __construct(IcingaObject $template, Auth $auth, Branch $branch = null) { + $this->auth = $auth; if ($template->get('object_type') !== 'template') { throw new ProgrammingError( @@ -68,6 +75,7 @@ class TemplateUsageTable extends Table ); } + $this->setBranch($branch); $this->objectType = $objectType = $template->getShortTableName(); $types = $this->getTypes(); $usage = $this->getUsageSummary($template); @@ -85,7 +93,7 @@ class TemplateUsageTable extends Table Table::td( Link::create( $count, - "director/${objectType}template/$type", + "director/{$objectType}template/$type", [ 'name' => $template->getObjectName(), 'inheritance' => $inheritance @@ -98,6 +106,7 @@ class TemplateUsageTable extends Table } if ($used) { + $this->getHeader()->add(Table::row($this->getColumnsToBeRendered(), null, 'th')); $this->add($rows); } else { $this->add($this->translate('This template is not in use')); @@ -106,52 +115,51 @@ class TemplateUsageTable extends Table protected function getUsageSummary(IcingaObject $template) { - $id = $template->getAutoincId(); $connection = $template->getConnection(); $db = $connection->getDbAdapter(); - $oType = $this->objectType; - $tree = new TemplateTree($oType, $connection); - $ids = $tree->listDescendantIdsFor($template); - if (empty($ids)) { - $ids = [0]; + + $types = array_keys($this->getTypes()); + $direct = []; + $indirect = []; + $templateType = $template->getShortTableName(); + + foreach ($this->getSummaryTables($templateType, $connection) as $type => $summaryTable) { + $directTable = clone $summaryTable; + $inDirectTable = clone $summaryTable; + + $direct[$type] = $db->query( + $directTable + ->filterTemplate($template, IcingaObjectFilterHelper::INHERIT_DIRECT) + ->getQuery() + )->rowCount(); + $indirect[$type] = $db->query( + $inDirectTable + ->filterTemplate($template, IcingaObjectFilterHelper::INHERIT_INDIRECT) + ->getQuery() + )->rowCount(); } - $baseQuery = $db->select()->from( - ['o' => 'icinga_' . $oType], - $this->getTypeSummaryDefinitions() - )->joinLeft( - ['oi' => "icinga_${oType}_inheritance"], - "oi.${oType}_id = o.id", - [] - ); - - $query = clone($baseQuery); - $direct = $db->fetchRow( - $query->where("oi.parent_${oType}_id = ?", $id) - ); - $query = clone($baseQuery); - $indirect = $db->fetchRow( - $query->where("oi.parent_${oType}_id IN (?)", $ids) - ); - //$indirect->templates = count($ids) - 1; $total = []; - $types = array_keys($this->getTypes()); foreach ($types as $type) { - $total[$type] = $direct->$type + $indirect->$type; + $total[$type] = $direct[$type] + $indirect[$type]; } return (object) [ - 'direct' => $direct, - 'indirect' => $indirect, + 'direct' => (object) $direct, + 'indirect' => (object) $indirect, 'total' => (object) $total ]; } - protected function getSummaryLine($type, $extra = null) + protected function getSummaryTables(string $templateType, Db $connection) { - if ($extra !== null) { - $extra = " AND $extra"; - } - return "COALESCE(SUM(CASE WHEN o.object_type = '${type}'${extra} THEN 1 ELSE 0 END), 0)"; + return [ + 'templates' => TemplatesTable::create( + $templateType, + $connection + ), + 'objects' => ObjectsTable::create($templateType, $connection, $this->auth) + ->setBranchUuid($this->branchUuid) + ]; } } diff --git a/library/Director/Web/Table/TemplatesTable.php b/library/Director/Web/Table/TemplatesTable.php index be195b2..d6582fd 100644 --- a/library/Director/Web/Table/TemplatesTable.php +++ b/library/Director/Web/Table/TemplatesTable.php @@ -36,8 +36,8 @@ class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage { $type = $this->type; $this->enableMultiSelect( - "director/${type}s/edittemplates", - "director/${type}template", + "director/{$type}s/edittemplates", + "director/{$type}template", ['name'] ); } @@ -60,12 +60,12 @@ class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage $name, Html::tag( 'span', - ['style' => 'font-style: italic'], + ['class' => 'font-italic'], $this->translate(' - not in use -') ) ]; - $url = Url::fromPath("director/${type}template/usage", [ + $url = Url::fromPath("director/{$type}template/usage", [ 'name' => $name ]); @@ -101,8 +101,8 @@ class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage { $type = $this->getType(); $this->getQuery()->where( - "(EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance" - . " WHERE parent_${type}_id = o.id))" + "(EXISTS (SELECT {$type}_id FROM icinga_{$type}_inheritance" + . " WHERE parent_{$type}_id = o.id))" ); } @@ -110,8 +110,8 @@ class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage { $type = $this->getType(); $this->getQuery()->where( - "(NOT EXISTS (SELECT ${type}_id FROM icinga_${type}_inheritance" - . " WHERE parent_${type}_id = o.id))" + "(NOT EXISTS (SELECT {$type}_id FROM icinga_{$type}_inheritance" + . " WHERE parent_{$type}_id = o.id))" ); } @@ -135,8 +135,8 @@ class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage protected function prepareQuery() { $type = $this->getType(); - $used = "CASE WHEN EXISTS(SELECT 1 FROM icinga_${type}_inheritance oi" - . " WHERE oi.parent_${type}_id = o.id) THEN 'y' ELSE 'n' END"; + $used = "CASE WHEN EXISTS(SELECT 1 FROM icinga_{$type}_inheritance oi" + . " WHERE oi.parent_{$type}_id = o.id) THEN 'y' ELSE 'n' END"; $columns = [ 'object_name' => 'o.object_name', @@ -145,7 +145,7 @@ class TemplatesTable extends ZfQueryBasedTable implements FilterableByUsage 'is_used' => $used, ]; $query = $this->db()->select()->from( - ['o' => "icinga_${type}"], + ['o' => "icinga_{$type}"], $columns )->where( "o.object_type = 'template'" diff --git a/library/Director/Web/Tabs/InfraTabs.php b/library/Director/Web/Tabs/InfraTabs.php index 8a65c4e..ea7a0cc 100644 --- a/library/Director/Web/Tabs/InfraTabs.php +++ b/library/Director/Web/Tabs/InfraTabs.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Director\Web\Tabs; use Icinga\Authentication\Auth; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Widget\Tabs; +use Icinga\Module\Director\Auth\Permission; class InfraTabs extends Tabs { @@ -24,21 +25,21 @@ class InfraTabs extends Tabs { $auth = $this->auth; - if ($auth->hasPermission('director/audit')) { + if ($auth->hasPermission(Permission::AUDIT)) { $this->add('activitylog', [ 'label' => $this->translate('Activity Log'), 'url' => 'director/config/activities' ]); } - if ($auth->hasPermission('director/deploy')) { + if ($auth->hasPermission(Permission::DEPLOY)) { $this->add('deploymentlog', [ 'label' => $this->translate('Deployments'), 'url' => 'director/config/deployments' ]); } - if ($auth->hasPermission('director/admin')) { + if ($auth->hasPermission(Permission::ADMIN)) { $this->add('infrastructure', [ 'label' => $this->translate('Infrastructure'), 'url' => 'director/dashboard', diff --git a/library/Director/Web/Tabs/MainTabs.php b/library/Director/Web/Tabs/MainTabs.php index 5ea2e9b..48ffa7c 100644 --- a/library/Director/Web/Tabs/MainTabs.php +++ b/library/Director/Web/Tabs/MainTabs.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Director\Web\Tabs; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Widget\Tabs; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Web\Widget\Daemon\BackgroundDaemonState; use Icinga\Module\Director\Db; use Icinga\Module\Director\Health; @@ -26,7 +27,7 @@ class MainTabs extends Tabs 'label' => $this->translate('Overview'), 'url' => 'director' ]); - if ($this->auth->hasPermission('director/admin')) { + if ($this->auth->hasPermission(Permission::ADMIN)) { $this->add('health', [ 'label' => $this->translate('Health'), 'url' => 'director/health' @@ -39,7 +40,7 @@ class MainTabs extends Tabs public function render() { - if ($this->auth->hasPermission('director/admin')) { + if ($this->auth->hasPermission(Permission::ADMIN)) { if ($this->getActiveName() !== 'health') { $state = $this->getHealthState(); if ($state->isProblem()) { diff --git a/library/Director/Web/Tabs/ObjectTabs.php b/library/Director/Web/Tabs/ObjectTabs.php index cbd3f15..c355304 100644 --- a/library/Director/Web/Tabs/ObjectTabs.php +++ b/library/Director/Web/Tabs/ObjectTabs.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Web\Tabs; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Objects\IcingaObject; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Widget\Tabs; @@ -46,10 +47,10 @@ class ObjectTabs extends Tabs protected function addTabsForNewObject() { $type = $this->type; - $this->add('add', array( + $this->add('add', [ 'url' => sprintf('director/%s/add', $type), 'label' => sprintf($this->translate('Add %s'), ucfirst($type)), - )); + ]); } protected function addTabsForExistingObject() @@ -59,14 +60,12 @@ class ObjectTabs extends Tabs $object = $this->object; $params = $object->getUrlParams(); - if (! $object->isExternal() - || in_array($object->getShortTableName(), $this->allowedExternals) - ) { - $this->add('modify', array( + if (! $object->isExternal() || in_array($object->getShortTableName(), $this->allowedExternals)) { + $this->add('modify', [ 'url' => sprintf('director/%s', $type), 'urlParams' => $params, 'label' => $this->translate(ucfirst($type)) - )); + ]); } if ($object->getShortTableName() === 'host') { $this->add('services', [ @@ -76,19 +75,17 @@ class ObjectTabs extends Tabs ]); } - if ($auth->hasPermission('director/showconfig')) { - if ($object->getShortTableName() !== 'service' - || $object->get('service_set_id') === null - ) { - $this->add('render', array( + if ($auth->hasPermission(Permission::SHOW_CONFIG)) { + if ($object->getShortTableName() !== 'service' || $object->get('service_set_id') === null) { + $this->add('render', [ 'url' => sprintf('director/%s/render', $type), 'urlParams' => $params, 'label' => $this->translate('Preview'), - )); + ]); } } - if ($auth->hasPermission('director/audit')) { + if ($auth->hasPermission(Permission::AUDIT)) { $this->add('history', array( 'url' => sprintf('director/%s/history', $type), 'urlParams' => $params, @@ -96,7 +93,7 @@ class ObjectTabs extends Tabs )); } - if ($auth->hasPermission('director/admin') && $this->hasFields()) { + if ($auth->hasPermission(Permission::ADMIN) && $this->hasFields()) { $this->add('fields', array( 'url' => sprintf('director/%s/fields', $type), 'urlParams' => $params, @@ -117,7 +114,7 @@ class ObjectTabs extends Tabs if ($object->supportsRanges()) { $this->add('ranges', [ - 'url' => "director/${type}/ranges", + 'url' => "director/{$type}/ranges", 'urlParams' => $params, 'label' => $this->translate('Ranges') ]); @@ -138,11 +135,11 @@ class ObjectTabs extends Tabs ]); } - if ($object->getShortTableName() === 'host' && $auth->hasPermission('director/hosts')) { + if ($object->getShortTableName() === 'host' && $auth->hasPermission(Permission::HOSTS)) { $this->add('agent', [ - 'url' => 'director/host/agent', + 'url' => 'director/host/agent', 'urlParams' => $params, - 'label' => $this->translate('Agent') + 'label' => $this->translate('Agent') ]); } } diff --git a/library/Director/Web/Tabs/ObjectsTabs.php b/library/Director/Web/Tabs/ObjectsTabs.php index 4f9e5a8..9d2a737 100644 --- a/library/Director/Web/Tabs/ObjectsTabs.php +++ b/library/Director/Web/Tabs/ObjectsTabs.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Web\Tabs; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; use Icinga\Module\Director\Objects\IcingaObject; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Widget\Tabs; @@ -21,65 +22,66 @@ class ObjectsTabs extends Tabs $plType = strtolower(preg_replace('/cys$/', 'cies', $shortName . 's')); $plType = str_replace('_', '-', $plType); - if ($auth->hasPermission("director/${plType}")) { - $this->add('index', array( + if ($auth->hasPermission("director/{$plType}")) { + $this->add('index', [ 'url' => sprintf('director/%s', $plType), 'label' => $this->translate(ucfirst($plType)), - )); + ]); } if ($object->getShortTableName() === 'command') { - $this->add('external', array( - 'url' => sprintf('director/%s', strtolower($plType)), + $this->add('external', [ + 'url' => sprintf('director/%s', strtolower($plType)), 'urlParams' => ['type' => 'external_object'], - 'label' => $this->translate('External'), - )); + 'label' => $this->translate('External'), + ]); } - if ($auth->hasPermission('director/admin') || ( + if ($auth->hasPermission(Permission::ADMIN) + || ( $object->getShortTableName() === 'notification' - && $auth->hasPermission('director/notifications') + && $auth->hasPermission(Permission::NOTIFICATIONS) ) || ( $object->getShortTableName() === 'scheduled_downtime' - && $auth->hasPermission('director/scheduled-downtimes') + && $auth->hasPermission(Permission::SCHEDULED_DOWNTIMES) )) { if ($object->supportsApplyRules()) { - $this->add('applyrules', array( - 'url' => sprintf('director/%s/applyrules', $plType), + $this->add('applyrules', [ + 'url' => sprintf('director/%s/applyrules', $plType), 'label' => $this->translate('Apply') - )); + ]); } } - if ($auth->hasPermission('director/admin') && $type !== 'zone') { + if ($auth->hasPermission(Permission::ADMIN) && $type !== 'zone') { if ($object->supportsImports()) { - $this->add('templates', array( - 'url' => sprintf('director/%s/templates', $plType), + $this->add('templates', [ + 'url' => sprintf('director/%s/templates', $plType), 'label' => $this->translate('Templates'), - )); + ]); } if ($object->supportsGroups()) { - $this->add('groups', array( - 'url' => sprintf('director/%sgroups', $typeUrl), + $this->add('groups', [ + 'url' => sprintf('director/%sgroups', $typeUrl), 'label' => $this->translate('Groups') - )); + ]); } } - if ($auth->hasPermission('director/admin')) { + if ($auth->hasPermission(Permission::ADMIN)) { if ($object->supportsChoices()) { - $this->add('choices', array( - 'url' => sprintf('director/templatechoices/%s', $shortName), + $this->add('choices', [ + 'url' => sprintf('director/templatechoices/%s', $shortName), 'label' => $this->translate('Choices') - )); + ]); } } - if ($object->supportsSets() && $auth->hasPermission("director/${typeUrl}sets")) { - $this->add('sets', array( - 'url' => sprintf('director/%s/sets', $plType), + if ($object->supportsSets() && $auth->hasPermission("director/{$typeUrl}sets")) { + $this->add('sets', [ + 'url' => sprintf('director/%s/sets', $plType), 'label' => $this->translate('Sets') - )); + ]); } } } diff --git a/library/Director/Web/Tree/TemplateTreeRenderer.php b/library/Director/Web/Tree/TemplateTreeRenderer.php index e238ded..8b23518 100644 --- a/library/Director/Web/Tree/TemplateTreeRenderer.php +++ b/library/Director/Web/Tree/TemplateTreeRenderer.php @@ -71,7 +71,7 @@ class TemplateTreeRenderer extends BaseHtmlElement } else { $li->add(Link::create( $tree['name'], - "director/${type}template/usage", + "director/{$type}template/usage", array('name' => $tree['name']), array('class' => 'icon-' .$type) )); diff --git a/library/Director/Web/Widget/ActivityLogInfo.php b/library/Director/Web/Widget/ActivityLogInfo.php index 8454b26..2b64fd4 100644 --- a/library/Director/Web/Widget/ActivityLogInfo.php +++ b/library/Director/Web/Widget/ActivityLogInfo.php @@ -3,7 +3,10 @@ namespace Icinga\Module\Director\Web\Widget; use gipfl\Json\JsonString; +use Icinga\Module\Director\Data\FieldReferenceLoader; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver; use Icinga\Module\Director\Objects\DirectorActivityLog; +use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use Icinga\Date\DateFormatter; @@ -83,8 +86,7 @@ class ActivityLogInfo extends HtmlDocument /** @var Url $url */ $url = $url->without('checksum')->without('show'); $div = Html::tag('div', [ - 'class' => 'pagination-control', - 'style' => 'float: right; width: 5em' + 'class' => ['pagination-control', 'activity-log-control'], ]); $ul = Html::tag('ul', ['class' => 'nav tab-nav']); @@ -434,11 +436,30 @@ class ActivityLogInfo extends HtmlDocument { if ($object instanceof IcingaService) { return $this->previewService($object); + } elseif ($object instanceof IcingaServiceSet) { + return $this->previewServiceSet($object); } else { return $object->toSingleIcingaConfig(); } } + /** + * Render service set to be previewed + * + * @param IcingaServiceSet $object + * + * @return IcingaConfig + */ + protected function previewServiceSet(IcingaServiceSet $object) + { + $config = $object->toSingleIcingaConfig(); + foreach ($object->getCachedServices() as $service) { + $service->renderToConfig($config); + } + + return $config; + } + protected function previewService(IcingaService $service) { if (($set = $service->get('service_set')) !== null) { @@ -625,10 +646,27 @@ class ActivityLogInfo extends HtmlDocument $newProps['object_type'] = $props->object_type; } - return IcingaObject::createByType( + $object = IcingaObject::createByType( $type, $newProps, $this->db - )->setProperties((array) $props); + ); + + if ($type === 'icinga_service_set' && isset($props->services)) { + $services = []; + foreach ($props->services as $service) { + $services[$service->object_name] = IcingaObject::createByType( + 'icinga_service', + (array) $service, + $this->db + ); + } + + /** @var IcingaServiceSet $object */ + $object->setCachedServices($services); + unset($props->services); + } + + return $object->setProperties((array) $props); } } diff --git a/library/Director/Web/Widget/AdditionalTableActions.php b/library/Director/Web/Widget/AdditionalTableActions.php index 978f399..7495189 100644 --- a/library/Director/Web/Widget/AdditionalTableActions.php +++ b/library/Director/Web/Widget/AdditionalTableActions.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Widget; +use Icinga\Module\Director\Auth\Permission; use ipl\Html\Html; use ipl\Html\HtmlDocument; use gipfl\IcingaWeb2\Icon; @@ -35,10 +36,10 @@ class AdditionalTableActions public function appendTo(HtmlDocument $parent) { $links = []; - if ($this->hasPermission('director/admin')) { + if ($this->hasPermission(Permission::ADMIN)) { $links[] = $this->createDownloadJsonLink(); } - if ($this->hasPermission('director/showsql')) { + if ($this->hasPermission(Permission::SHOW_SQL)) { $links[] = $this->createShowSqlToggle(); } diff --git a/library/Director/Web/Widget/BranchedObjectHint.php b/library/Director/Web/Widget/BranchedObjectHint.php index ec16094..c50f923 100644 --- a/library/Director/Web/Widget/BranchedObjectHint.php +++ b/library/Director/Web/Widget/BranchedObjectHint.php @@ -15,33 +15,45 @@ class BranchedObjectHint extends HtmlDocument { use TranslationHelper; - public function __construct(Branch $branch, Auth $auth, BranchedObject $object = null) + public function __construct(Branch $branch, Auth $auth, BranchedObject $object = null, $hasPreferredBranch = false) { if (! $branch->isBranch()) { - return; - } - $hook = Branch::requireHook(); - - $name = $branch->getName(); - if (substr($name, 0, 1) === '/') { - $label = $this->translate('this configuration branch'); + if ($hasPreferredBranch) { + $main = true; + $hintMethod = 'warning'; + $link = $this->translate('the main configuration branch'); + $deployHint = ' ' . $this->translate('This will be part of the next deployment'); + } else { + return; + } } else { - $label = $name; + $main = false; + $hintMethod = 'info'; + $deployHint = ' ' . $this->translate('This will not be part of any deployment, unless being merged'); + $hook = Branch::requireHook(); + $name = $branch->getName(); + if (substr($name, 0, 1) === '/') { + $label = $this->translate('this configuration branch'); + } else { + $label = $name; + } + $link = $hook->linkToBranch($branch, $auth, $label); } - $link = $hook->linkToBranch($branch, $auth, $label); + if ($object === null) { - $this->add(Hint::info(Html::sprintf($this->translate( - 'This object will be created in %s. It will not be part of any deployment' - . ' unless being merged' - ), $link))); + $this->add(Hint::$hintMethod(Html::sprintf($this->translate( + 'This object will be created in %s.' + ) . $deployHint, $link))); return; } if (! $object->hasBeenTouchedByBranch()) { - $this->add(Hint::info(Html::sprintf($this->translate( - 'Your changes will be stored in %s. The\'ll not be part of any deployment' - . ' unless being merged' - ), $link))); + $this->add(Hint::$hintMethod(Html::sprintf($this->translate( + 'Your changes are going to be stored in %s.' + ) . $deployHint, $link))); + return; + } + if ($main) { return; } diff --git a/library/Director/Web/Widget/BranchedObjectsHint.php b/library/Director/Web/Widget/BranchedObjectsHint.php index d689178..3f00f9e 100644 --- a/library/Director/Web/Widget/BranchedObjectsHint.php +++ b/library/Director/Web/Widget/BranchedObjectsHint.php @@ -13,9 +13,14 @@ class BranchedObjectsHint extends HtmlDocument { use TranslationHelper; - public function __construct(Branch $branch, Auth $auth) + public function __construct(Branch $branch, Auth $auth, $hasPreferredBranch = false) { if (! $branch->isBranch()) { + if ($hasPreferredBranch) { + $this->add(Hint::warning($this->translate( + "You're currently in the master branch, your changes will make part of the next Deployment" + ))); + } return; } $hook = Branch::requireHook(); diff --git a/library/Director/Web/Widget/DeploymentInfo.php b/library/Director/Web/Widget/DeploymentInfo.php index 110200f..1f87abc 100644 --- a/library/Director/Web/Widget/DeploymentInfo.php +++ b/library/Director/Web/Widget/DeploymentInfo.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Widget; +use Icinga\Module\Director\Auth\Permission; use ipl\Html\HtmlDocument; use Icinga\Authentication\Auth; use Icinga\Module\Director\IcingaConfig\IcingaConfig; @@ -55,7 +56,7 @@ class DeploymentInfo extends HtmlDocument 'url' => $request->getUrl() ))->activate('deployment'); - if ($dep->config_checksum !== null && $auth->hasPermission('director/showconfig')) { + if ($dep->config_checksum !== null && $auth->hasPermission(Permission::SHOW_CONFIG)) { $tabs->add('config', array( 'label' => $this->translate('Config'), 'url' => 'director/config/files', @@ -72,7 +73,8 @@ class DeploymentInfo extends HtmlDocument protected function createInfoTable() { $dep = $this->deployment; - $table = new NameValueTable(); + $table = (new NameValueTable()) + ->addAttributes(['class' => 'deployment-details']); $table->addNameValuePairs([ $this->translate('Deployment time') => $dep->start_time, $this->translate('Sent to') => $dep->peer_identity, @@ -135,16 +137,21 @@ class DeploymentInfo extends HtmlDocument } else { return [$this->translate('Unknown, failed to collect related information'), new Icon('help')]; } - } elseif ($dep->startup_succeeded === 'y') { - return $this->colored('green', [$this->translate('Succeeded'), new Icon('ok')]); } else { - return $this->colored('red', [$this->translate('Failed'), new Icon('cancel')]); - } - } + $div = Html::tag('div')->setSeparator(' '); - protected function colored($color, array $content) - { - return Html::tag('div', ['style' => "color: $color;"], $content)->setSeparator(' '); + if ($dep->startup_succeeded === 'y') { + $div + ->addAttributes(['class' => 'succeeded']) + ->add([$this->translate('Succeeded'), new Icon('ok')]); + } else { + $div + ->addAttributes(['class' => 'failed']) + ->add([$this->translate('Failed'), new Icon('cancel')]); + } + + return $div; + } } public function render() diff --git a/library/Director/Web/Widget/IcingaObjectInspection.php b/library/Director/Web/Widget/IcingaObjectInspection.php index 61f3567..d9cf69d 100644 --- a/library/Director/Web/Widget/IcingaObjectInspection.php +++ b/library/Director/Web/Widget/IcingaObjectInspection.php @@ -205,7 +205,7 @@ class IcingaObjectInspection extends BaseHtmlElement $this->add(Html::tag('p')->add(Html::sprintf( 'The configuration for this object has been rendered by Icinga' . ' Director %s to %s', - DateFormatter::timeAgo(strtotime($deployment->start_time, false)), + DateFormatter::timeAgo(strtotime($deployment->start_time)), $this->linkToSourceLocation($deployment, $source) ))); } |