From 2c898ba7b9a28e56a88344417cd2f40084e5932c Mon Sep 17 00:00:00 2001 From: Ravi Kumar Kempapura Srinivasa Date: Tue, 7 Nov 2023 16:38:57 +0100 Subject: [PATCH 01/17] cli: Add migrate command --- application/clicommands/MigrateCommand.php | 679 +++++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 application/clicommands/MigrateCommand.php diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php new file mode 100644 index 000000000..81af67a99 --- /dev/null +++ b/application/clicommands/MigrateCommand.php @@ -0,0 +1,679 @@ +setLevel(Logger::INFO); + } + + /** + * Migrate monitoring navigation items to the Icinga DB Web actions + * + * USAGE + * + * icingacli icingadb migrate navigation [options] + * + * REQUIRED OPTIONS: + * + * --user= Migrate monitoring navigation items only for + * the given user or all similar users if a + * wildcard is used. (* matches all users) + * + * OPTIONS: + * + * --override Override the existing Icinga DB navigation items + * + * --delete Remove the legacy files after successfully + * migrated the navigation items. + */ + public function navigationAction(): void + { + $preferencesPath = Config::resolvePath('preferences'); + $sharedNavigation = Config::resolvePath('navigation'); + if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) { + Logger::info('There are no user navigation items to migrate'); + return; + } + + $rc = 0; + /** @var string $user */ + $user = $this->params->getRequired('user'); + $directories = new DirectoryIterator($preferencesPath); + + foreach ($directories as $directory) { + /** @var string $username */ + $username = $directories->key() === false ? '' : $directories->key(); + if (fnmatch($user, $username) === false) { + continue; + } + + $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc); + $icingadbHostActions = $this->readFromIni($directory . '/icingadb-host-actions.ini', $rc); + $icingadbServiceActions = $this->readFromIni($directory . '/icingadb-service-actions.ini', $rc); + + Logger::info( + 'Transforming legacy wildcard filters of existing Icinga DB Web actions for user "%s"', + $username + ); + + if (! $icingadbHostActions->isEmpty()) { + $this->migrateNavigationItems($icingadbHostActions, false, $rc); + } + + if (! $icingadbServiceActions->isEmpty()) { + $this->migrateNavigationItems( + $icingadbServiceActions, + false, + $rc + ); + } + + Logger::info('Migrating monitoring navigation items for user "%s" to the Icinga DB Web actions', $username); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems($hostActions, false, $rc, $directory . '/icingadb-host-actions.ini'); + } + + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems( + $serviceActions, + false, + $rc, + $directory . '/icingadb-service-actions.ini' + ); + } + } + + // Start migrating shared navigation items + $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc); + $icingadbHostActions = $this->readFromIni($sharedNavigation . '/icingadb-host-actions.ini', $rc); + $icingadbServiceActions = $this->readFromIni($sharedNavigation . '/icingadb-service-actions.ini', $rc); + + Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web actions'); + + if (! $icingadbHostActions->isEmpty()) { + $this->migrateNavigationItems($icingadbHostActions, true, $rc); + } + + if (! $icingadbServiceActions->isEmpty()) { + $this->migrateNavigationItems( + $icingadbServiceActions, + true, + $rc + ); + } + + Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems($hostActions, true, $rc, $sharedNavigation . '/icingadb-host-actions.ini'); + } + + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems( + $serviceActions, + true, + $rc, + $sharedNavigation . '/icingadb-service-actions.ini' + ); + } + + if ($rc > 0) { + Logger::error('Failed to migrate some monitoring navigation items'); + exit($rc); + } + + Logger::info('Successfully migrated all local user monitoring navigation items'); + } + + + /** + * Migrate monitoring restrictions and permissions in a role to Icinga DB Web restrictions and permissions + * + * USAGE + * + * icingacli icingadb migrate role [options] + * + * OPTIONS: + * + * --group= Migrate monitoring restrictions and permissions for all roles, + * the given group or the groups matching the given + * group belongs to. + * (wildcard * migrates monitoring restrictions and permissions + * for all roles) + * + * --role= Migrate monitoring restrictions and permissions for the + * given role or all the matching roles. + * (wildcard * migrates monitoring restrictions and permissions + * for all roles) + * + * --override Override the existing Icinga DB restrictions and permissions + */ + public function roleAction(): void + { + /** @var ?bool $override */ + $override = $this->params->get('override'); + + /** @var ?string $groupName */ + $groupName = $this->params->get('group'); + /** @var ?string $roleName */ + $roleName = $this->params->get('role'); + + if ($roleName === null && $groupName === null) { + $this->fail("One of the parameters 'group' or 'role' must be supplied"); + } elseif ($roleName !== null && $groupName !== null) { + $this->fail("Use either 'group' or 'role'. Both cannot be used as role overrules group."); + } + + $rc = 0; + $restrictions = Config::$configDir . '/roles.ini'; + $rolesConfig = $this->readFromIni($restrictions, $rc); + $monitoringRestriction = 'monitoring/filter/objects'; + $monitoringPropertyBlackList = 'monitoring/blacklist/properties'; + $icingadbRestrictions = [ + 'objects' => 'icingadb/filter/objects', + 'hosts' => 'icingadb/filter/hosts', + 'services' => 'icingadb/filter/services' + ]; + + $icingadbPropertyDenyList = 'icingadb/denylist/variables'; + foreach ($rolesConfig as $name => $role) { + /** @var string[] $role */ + $role = iterator_to_array($role); + + if ($roleName === '*' || $groupName === '*') { + $updateRole = $this->shouldUpdateRole($role, $override); + } elseif ($roleName !== null && fnmatch($roleName, $name)) { + $updateRole = $this->shouldUpdateRole($role, $override); + } elseif ($groupName !== null && isset($role['groups'])) { + $roleGroups = array_map('trim', explode(',', $role['groups'])); + $updateRole = false; + foreach ($roleGroups as $roleGroup) { + if (fnmatch($groupName, $roleGroup)) { + $updateRole = $this->shouldUpdateRole($role, $override); + break; + } + } + } else { + $updateRole = false; + } + + if ($updateRole) { + if (isset($role[$monitoringRestriction])) { + Logger::info( + 'Migrating monitoring restriction filter for role "%s" to the Icinga DB Web restrictions', + $name + ); + $transformedFilter = UrlMigrator::transformFilter( + QueryString::parse($role[$monitoringRestriction]) + ); + + if ($transformedFilter) { + $role[$icingadbRestrictions['objects']] = rawurldecode( + QueryString::render($transformedFilter) + ); + } + } + + if (isset($role[$monitoringPropertyBlackList])) { + Logger::info( + 'Migrating monitoring blacklisted properties for role "%s" to the Icinga DB Web deny list', + $name + ); + + $icingadbProperties = []; + foreach (explode(',', $role[$monitoringPropertyBlackList]) as $property) { + $icingadbProperties[] = preg_replace('/^(?:host|service)\.vars\./i', '', $property, 1); + } + + $role[$icingadbPropertyDenyList] = str_replace( + '**', + '*', + implode(',', array_unique($icingadbProperties)) + ); + } + + if (isset($role['permissions'])) { + $updatedPermissions = []; + Logger::info( + 'Migrating monitoring permissions for role "%s" to the Icinga DB Web permissions', + $name + ); + + if (strpos($role['permissions'], 'monitoring')) { + $monitoringProtection = Config::module('monitoring') + ->get('security', 'protected_customvars'); + + if ($monitoringProtection !== null) { + $role['icingadb/protect/variables'] = $monitoringProtection; + } + } + + foreach (explode(',', $role['permissions']) as $permission) { + if (str_contains($permission, 'icingadb')) { + continue; + } elseif (fnmatch('monitoring/command*', $permission)) { + $updatedPermissions[] = $permission; + $updatedPermissions[] = str_replace('monitoring', 'icingadb', $permission); + } elseif ($permission === 'no-monitoring/contacts') { + $updatedPermissions[] = $permission; + $role['icingadb/denylist/routes'] = 'users,usergroups'; + } else { + $updatedPermissions[] = $permission; + } + } + + $role['permissions'] = implode(',', $updatedPermissions); + } + + if (isset($role['refusals']) && is_string($role['refusals'])) { + $updatedRefusals = []; + Logger::info( + 'Migrating monitoring refusals for role "%s" to the Icinga DB Web refusals', + $name + ); + + foreach (explode(',', $role['refusals']) as $refusal) { + if (str_contains($refusal, 'icingadb')) { + continue; + } elseif (fnmatch('monitoring/command*', $refusal)) { + $updatedRefusals[] = $refusal; + $updatedRefusals[] = str_replace('monitoring', 'icingadb', $refusal); + } else { + $updatedRefusals[] = $refusal; + } + } + + $role['refusals'] = implode(',', $updatedRefusals); + } + } + + foreach ($icingadbRestrictions as $object => $icingadbRestriction) { + if (isset($role[$icingadbRestriction]) && is_string($role[$icingadbRestriction])) { + $filter = QueryString::parse($role[$icingadbRestriction]); + $filter = $this->transformLegacyWildcardFilter($filter); + + if ($filter) { + $filter = rawurldecode(QueryString::render($filter)); + if ($filter !== $role[$icingadbRestriction]) { + Logger::info( + 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"', + $name, + $object, + $role[$icingadbRestriction], + $filter + ); + + $role[$icingadbRestriction] = $filter; + } + } + } + } + + $rolesConfig->setSection($name, $role); + } + + try { + $rolesConfig->saveIni(); + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + Logger::error('Failed to migrate monitoring restrictions'); + exit(256); + } + + Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); + } + + /** + * Checks if the given role should be updated + * + * @param string[] $role + * @param bool $override + * + * @return bool + */ + private function shouldUpdateRole(array $role, ?bool $override): bool + { + return ! ( + isset($role['icingadb/filter/objects']) + || isset($role['icingadb/filter/hosts']) + || isset($role['icingadb/filter/services']) + || isset($role['icingadb/denylist/routes']) + || isset($role['icingadb/denylist/variables']) + || isset($role['icingadb/protect/variables']) + || (isset($role['permissions']) && str_contains($role['permissions'], 'icingadb')) + ) + || $override; + } + + /** + * Migrate the monitoring dashboards to Icinga DB Web dashboards for all the matched users + * + * USAGE + * + * icingacli icingadb migrate dasboard [options] + * + * REQUIRED OPTIONS: + * + * --user= Migrate monitoring dashboards for all the + * users that are matched. (* all users) + * + * OPTIONS: + * + * --no-backup Migrate without creating a backup. (By Default + * a backup for monitoring dashboards is created) + */ + public function dashboardAction(): void + { + $dashboardsPath = Config::resolvePath('dashboards'); + if (! file_exists($dashboardsPath)) { + Logger::info('There are no dashboards to migrate'); + return; + } + + /** @var string $user */ + $user = $this->params->getRequired('user'); + $noBackup = $this->params->get('no-backup'); + + $rc = 0; + $directories = new DirectoryIterator($dashboardsPath); + foreach ($directories as $directory) { + /** @var string $userName */ + $userName = $directories->key() === false ? '' : $directories->key(); + if (fnmatch($user, $userName) === false) { + continue; + } + + $dashboardsConfig = $this->readFromIni($directory . '/dashboard.ini', $rc); + $backupConfig = $this->readFromIni($directory . '/dashboard.ini', $rc); + + Logger::info( + 'Migrating monitoring dashboards to Icinga DB Web dashboards for user "%s"', + $userName + ); + + $changed = false; + /** @var ConfigObject $dashboardConfig */ + foreach ($dashboardsConfig->getConfigObject() as $name => $dashboardConfig) { + /** @var ?string $dashboardUrlString */ + $dashboardUrlString = $dashboardConfig->get('url'); + if ($dashboardUrlString !== null) { + $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request()); + if (fnmatch('monitoring*', $dashboardUrlString)) { + $dashboardConfig->url = rawurldecode( + UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl() + ); + + $changed = true; + } + + if (fnmatch('icingadb*', ltrim($dashboardUrlString, '/'))) { + $filter = QueryString::parse($dashBoardUrl->getParams()->toString()); + $filter = $this->transformLegacyWildcardFilter($filter); + if ($filter) { + $oldFilterString = $dashBoardUrl->getParams()->toString(); + $newFilterString = rawurldecode(QueryString::render($filter)); + + if ($oldFilterString !== $newFilterString) { + Logger::info( + 'Icinga Db Web filter of dashboard "%s" has changed from "%s" to "%s"', + $name, + rawurldecode($dashBoardUrl->getParams()->toString()), + rawurldecode(QueryString::render($filter)) + ); + $dashBoardUrl->setParams([]); + $dashBoardUrl->setFilter($filter); + + $dashboardConfig->url = rawurldecode($dashBoardUrl->getRelativeUrl()); + $changed = true; + } + } + } + } + } + + + if ($changed && $noBackup === null) { + $counter = 0; + while (true) { + $filepath = $counter > 0 + ? $directory . "/dashboard.backup$counter.ini" + : $directory . '/dashboard.backup.ini'; + + if (! file_exists($filepath)) { + $backupConfig->saveIni($filepath); + break; + } else { + $counter++; + } + } + } + + try { + $dashboardsConfig->saveIni(); + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + $rc = 256; + } + } + + if ($rc > 0) { + Logger::error('Failed to migrate some monitoring dashboards'); + exit($rc); + } + + Logger::info('Successfully migrated dashboards for all the matched users'); + } + + /** + * Migrate the given config to the given new config path + * + * @param Config $config + * @param ?string $path + * @param bool $shared + * @param int $rc + */ + private function migrateNavigationItems($config, $shared, &$rc, $path = null): void + { + /** @var string $owner */ + $owner = $this->params->getRequired('user'); + if ($path === null) { + $newConfig = $config; + /** @var ConfigObject $newConfigObject */ + foreach ($newConfig->getConfigObject() as $section => $newConfigObject) { + /** @var string $configOwner */ + $configOwner = $newConfigObject->get('owner') ?? ''; + if ($shared && ! fnmatch($owner, $configOwner)) { + continue; + } + + /** @var ?string $legacyFilter */ + $legacyFilter = $newConfigObject->get('filter'); + if ($legacyFilter !== null) { + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); + if ($filter) { + $filter = rawurldecode(QueryString::render($filter)); + if ($legacyFilter !== $filter) { + $newConfigObject->filter = $filter; + $newConfig->setSection($section, $newConfigObject); + Logger::info( + 'Icinga DB Web filter of action "%s" is changed from %s to "%s"', + $section, + $legacyFilter, + $filter + ); + } + } + } + } + } else { + $deleteLegacyFiles = $this->params->get('delete'); + $override = $this->params->get('override'); + $newConfig = $this->readFromIni($path, $rc); + + /** @var ConfigObject $configObject */ + foreach ($config->getConfigObject() as $configObject) { + // Change the config type from "host-action" to icingadb's new action + /** @var string $configOwner */ + $configOwner = $configObject->get('owner') ?? ''; + if ($shared && ! fnmatch($owner, $configOwner)) { + continue; + } + + if (strpos($path, 'icingadb-host-actions') !== false) { + $configObject->type = 'icingadb-host-action'; + } else { + $configObject->type = 'icingadb-service-action'; + } + + /** @var ?string $urlString */ + $urlString = $configObject->get('url'); + if ($urlString !== null) { + $url = Url::fromPath($urlString, [], new Request()); + + try { + $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl(); + $configObject->url = rawurldecode($urlString); + } catch (\InvalidArgumentException $err) { + // Do nothing + } + } + + /** @var ?string $legacyFilter */ + $legacyFilter = $configObject->get('filter'); + if ($legacyFilter !== null) { + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformFilter($filter); + if ($filter !== false) { + $configObject->filter = rawurldecode(QueryString::render($filter)); + } else { + unset($configObject->filter); + } + } + + $section = $config->key(); + + if (! $newConfig->hasSection($section) || $override) { + /** @var string $type */ + $type = $configObject->get('type'); + $oldPath = $shared + ? sprintf( + '%s/%s/%ss.ini', + Config::resolvePath('preferences'), + $configOwner, + $type + ) + : sprintf( + '%s/%ss.ini', + Config::resolvePath('navigation'), + $type + ); + + $oldConfig = $this->readFromIni($oldPath, $rc); + + if ($override && $oldConfig->hasSection($section)) { + $oldConfig->removeSection($section); + $oldConfig->saveIni(); + } + + if (! $oldConfig->hasSection($section)) { + $newConfig->setSection($section, $configObject); + } + } + } + } + + try { + if (! $newConfig->isEmpty()) { + $newConfig->saveIni(); + + // Remove the legacy file only if explicitly requested + if ($path !== null && $deleteLegacyFiles) { + unlink($config->getConfigFile()); + } + } + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + $rc = 256; + } + } + + /** + * Get the navigation items config from the given ini path + * + * @param string $path Absolute path of the ini file + * @param int $rc The return code used to exit the action + * + * @return Config + */ + private function readFromIni($path, &$rc) + { + try { + $config = Config::fromIni($path); + } catch (NotReadableError $error) { + if ($error->getPrevious() !== null) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + } else { + Logger::error($error->getMessage()); + } + + $config = new Config(); + $rc = 128; + } + + return $config; + } + + /** + * Transform given legacy wirldcard filters + * + * @param $filter Filter\Rule + * + * @return Filter\Chain|Filter\Condition|null + */ + private function transformLegacyWildcardFilter(Filter\Rule $filter) + { + if ($filter instanceof Filter\Chain) { + foreach ($filter as $child) { + $newChild = $this->transformLegacyWildcardFilter($child); + if ($newChild !== null) { + $filter->replace($child, $newChild); + } + } + + return $filter; + } elseif ($filter instanceof Filter\Equal) { + if (is_string($filter->getValue()) && strpos($filter->getValue(), '*') !== false) { + return Filter::like($filter->getColumn(), $filter->getValue()); + } + } elseif ($filter instanceof Filter\Unequal) { + if (is_string($filter->getValue()) && strpos($filter->getValue(), '*') !== false) { + return Filter::unlike($filter->getColumn(), $filter->getValue()); + } + } + } +} From 4e8a3a13a78509a4c852a53dd2bfbdd6868279b5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 16:44:11 +0100 Subject: [PATCH 02/17] cli: Require mandatory params as early as possible --- application/clicommands/MigrateCommand.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 81af67a99..386c74d7b 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -49,6 +49,9 @@ public function init(): void */ public function navigationAction(): void { + /** @var string $user */ + $user = $this->params->getRequired('user'); + $preferencesPath = Config::resolvePath('preferences'); $sharedNavigation = Config::resolvePath('navigation'); if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) { @@ -57,8 +60,6 @@ public function navigationAction(): void } $rc = 0; - /** @var string $user */ - $user = $this->params->getRequired('user'); $directories = new DirectoryIterator($preferencesPath); foreach ($directories as $directory) { @@ -388,16 +389,16 @@ private function shouldUpdateRole(array $role, ?bool $override): bool */ public function dashboardAction(): void { + /** @var string $user */ + $user = $this->params->getRequired('user'); + $noBackup = $this->params->get('no-backup'); + $dashboardsPath = Config::resolvePath('dashboards'); if (! file_exists($dashboardsPath)) { Logger::info('There are no dashboards to migrate'); return; } - /** @var string $user */ - $user = $this->params->getRequired('user'); - $noBackup = $this->params->get('no-backup'); - $rc = 0; $directories = new DirectoryIterator($dashboardsPath); foreach ($directories as $directory) { From 78e5c757c0eb6ad47d65b470bfd76a1b171ee432 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 16:50:52 +0100 Subject: [PATCH 03/17] migrate: Add `filter` sub command --- application/clicommands/MigrateCommand.php | 223 ++++++++++++--------- 1 file changed, 128 insertions(+), 95 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 386c74d7b..75585852b 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -91,19 +91,26 @@ public function navigationAction(): void ); } - Logger::info('Migrating monitoring navigation items for user "%s" to the Icinga DB Web actions', $username); - - if (! $hostActions->isEmpty()) { - $this->migrateNavigationItems($hostActions, false, $rc, $directory . '/icingadb-host-actions.ini'); - } + if (! $this->skipMigration) { + Logger::info('Migrating monitoring navigation items for user "%s" to Icinga DB Web actions', $username); + + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems( + $hostActions, + false, + $rc, + $directory . '/icingadb-host-actions.ini' + ); + } - if (! $serviceActions->isEmpty()) { - $this->migrateNavigationItems( - $serviceActions, - false, - $rc, - $directory . '/icingadb-service-actions.ini' - ); + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems( + $serviceActions, + false, + $rc, + $directory . '/icingadb-service-actions.ini' + ); + } } } @@ -127,27 +134,43 @@ public function navigationAction(): void ); } - Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); + if (! $this->skipMigration) { + Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); - if (! $hostActions->isEmpty()) { - $this->migrateNavigationItems($hostActions, true, $rc, $sharedNavigation . '/icingadb-host-actions.ini'); - } + if (! $hostActions->isEmpty()) { + $this->migrateNavigationItems( + $hostActions, + true, + $rc, + $sharedNavigation . '/icingadb-host-actions.ini' + ); + } - if (! $serviceActions->isEmpty()) { - $this->migrateNavigationItems( - $serviceActions, - true, - $rc, - $sharedNavigation . '/icingadb-service-actions.ini' - ); + if (! $serviceActions->isEmpty()) { + $this->migrateNavigationItems( + $serviceActions, + true, + $rc, + $sharedNavigation . '/icingadb-service-actions.ini' + ); + } } if ($rc > 0) { - Logger::error('Failed to migrate some monitoring navigation items'); + if ($this->skipMigration) { + Logger::error('Failed to transform some icingadb navigation items'); + } else { + Logger::error('Failed to migrate some monitoring navigation items'); + } + exit($rc); } - Logger::info('Successfully migrated all local user monitoring navigation items'); + if ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb navigation item filters'); + } else { + Logger::info('Successfully migrated all monitoring navigation items'); + } } @@ -206,23 +229,23 @@ public function roleAction(): void $role = iterator_to_array($role); if ($roleName === '*' || $groupName === '*') { - $updateRole = $this->shouldUpdateRole($role, $override); + $roleMatch = true; } elseif ($roleName !== null && fnmatch($roleName, $name)) { - $updateRole = $this->shouldUpdateRole($role, $override); + $roleMatch = true; } elseif ($groupName !== null && isset($role['groups'])) { $roleGroups = array_map('trim', explode(',', $role['groups'])); - $updateRole = false; + $roleMatch = false; foreach ($roleGroups as $roleGroup) { if (fnmatch($groupName, $roleGroup)) { - $updateRole = $this->shouldUpdateRole($role, $override); + $roleMatch = true; break; } } } else { - $updateRole = false; + $roleMatch = false; } - if ($updateRole) { + if ($roleMatch && ! $this->skipMigration && $this->shouldUpdateRole($role, $override)) { if (isset($role[$monitoringRestriction])) { Logger::info( 'Migrating monitoring restriction filter for role "%s" to the Icinga DB Web restrictions', @@ -312,23 +335,25 @@ public function roleAction(): void } } - foreach ($icingadbRestrictions as $object => $icingadbRestriction) { - if (isset($role[$icingadbRestriction]) && is_string($role[$icingadbRestriction])) { - $filter = QueryString::parse($role[$icingadbRestriction]); - $filter = $this->transformLegacyWildcardFilter($filter); + if ($roleMatch) { + foreach ($icingadbRestrictions as $object => $icingadbRestriction) { + if (isset($role[$icingadbRestriction]) && is_string($role[$icingadbRestriction])) { + $filter = QueryString::parse($role[$icingadbRestriction]); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); - if ($filter) { - $filter = rawurldecode(QueryString::render($filter)); - if ($filter !== $role[$icingadbRestriction]) { - Logger::info( - 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"', - $name, - $object, - $role[$icingadbRestriction], - $filter - ); + if ($filter) { + $filter = rawurldecode(QueryString::render($filter)); + if ($filter !== $role[$icingadbRestriction]) { + Logger::info( + 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"', + $name, + $object, + $role[$icingadbRestriction], + $filter + ); - $role[$icingadbRestriction] = $filter; + $role[$icingadbRestriction] = $filter; + } } } } @@ -341,33 +366,20 @@ public function roleAction(): void $rolesConfig->saveIni(); } catch (NotWritableError $error) { Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); - Logger::error('Failed to migrate monitoring restrictions'); + if ($this->skipMigration) { + Logger::error('Failed to transform icingadb restrictions'); + } else { + Logger::error('Failed to migrate monitoring restrictions'); + } + exit(256); } - Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); - } - - /** - * Checks if the given role should be updated - * - * @param string[] $role - * @param bool $override - * - * @return bool - */ - private function shouldUpdateRole(array $role, ?bool $override): bool - { - return ! ( - isset($role['icingadb/filter/objects']) - || isset($role['icingadb/filter/hosts']) - || isset($role['icingadb/filter/services']) - || isset($role['icingadb/denylist/routes']) - || isset($role['icingadb/denylist/variables']) - || isset($role['icingadb/protect/variables']) - || (isset($role['permissions']) && str_contains($role['permissions'], 'icingadb')) - ) - || $override; + if ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb restrictions'); + } else { + Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); + } } /** @@ -423,7 +435,7 @@ public function dashboardAction(): void $dashboardUrlString = $dashboardConfig->get('url'); if ($dashboardUrlString !== null) { $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request()); - if (fnmatch('monitoring*', $dashboardUrlString)) { + if (! $this->skipMigration && fnmatch('monitoring*', $dashboardUrlString)) { $dashboardConfig->url = rawurldecode( UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl() ); @@ -482,11 +494,39 @@ public function dashboardAction(): void } if ($rc > 0) { - Logger::error('Failed to migrate some monitoring dashboards'); + if ($this->skipMigration) { + Logger::error('Failed to transform some icingadb dashboards'); + } else { + Logger::error('Failed to migrate some monitoring dashboards'); + } + exit($rc); } - Logger::info('Successfully migrated dashboards for all the matched users'); + if ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb dashboards'); + } else { + Logger::info('Successfully migrated dashboards for all the matched users'); + } + } + + /** + * Migrate Icinga DB Web wildcard filters of navigation items, dashboards and roles + * + * USAGE + * + * icingacli icingadb migrate filter + */ + public function filterAction(): void + { + $this->skipMigration = true; + + $this->params->set('user', '*'); + $this->navigationAction(); + $this->dashboardAction(); + + $this->params->set('role', '*'); + $this->roleAction(); } /** @@ -650,31 +690,24 @@ private function readFromIni($path, &$rc) } /** - * Transform given legacy wirldcard filters + * Checks if the given role should be updated * - * @param $filter Filter\Rule + * @param string[] $role + * @param bool $override * - * @return Filter\Chain|Filter\Condition|null + * @return bool */ - private function transformLegacyWildcardFilter(Filter\Rule $filter) + private function shouldUpdateRole(array $role, ?bool $override): bool { - if ($filter instanceof Filter\Chain) { - foreach ($filter as $child) { - $newChild = $this->transformLegacyWildcardFilter($child); - if ($newChild !== null) { - $filter->replace($child, $newChild); - } - } - - return $filter; - } elseif ($filter instanceof Filter\Equal) { - if (is_string($filter->getValue()) && strpos($filter->getValue(), '*') !== false) { - return Filter::like($filter->getColumn(), $filter->getValue()); - } - } elseif ($filter instanceof Filter\Unequal) { - if (is_string($filter->getValue()) && strpos($filter->getValue(), '*') !== false) { - return Filter::unlike($filter->getColumn(), $filter->getValue()); - } - } + return ! ( + isset($role['icingadb/filter/objects']) + || isset($role['icingadb/filter/hosts']) + || isset($role['icingadb/filter/services']) + || isset($role['icingadb/denylist/routes']) + || isset($role['icingadb/denylist/variables']) + || isset($role['icingadb/protect/variables']) + || (isset($role['permissions']) && str_contains($role['permissions'], 'icingadb')) + ) + || $override; } } From 921729f01003bdac1f2354646f220d9ec64a5999 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 16:53:18 +0100 Subject: [PATCH 04/17] migrate: Don't decode entire urls --- application/clicommands/MigrateCommand.php | 25 +++++++++------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 75585852b..bc1328b6d 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -256,9 +256,7 @@ public function roleAction(): void ); if ($transformedFilter) { - $role[$icingadbRestrictions['objects']] = rawurldecode( - QueryString::render($transformedFilter) - ); + $role[$icingadbRestrictions['objects']] = QueryString::render($transformedFilter); } } @@ -342,7 +340,7 @@ public function roleAction(): void $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { - $filter = rawurldecode(QueryString::render($filter)); + $filter = QueryString::render($filter); if ($filter !== $role[$icingadbRestriction]) { Logger::info( 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"', @@ -436,10 +434,7 @@ public function dashboardAction(): void if ($dashboardUrlString !== null) { $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request()); if (! $this->skipMigration && fnmatch('monitoring*', $dashboardUrlString)) { - $dashboardConfig->url = rawurldecode( - UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl() - ); - + $dashboardConfig->url = UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl(); $changed = true; } @@ -448,19 +443,19 @@ public function dashboardAction(): void $filter = $this->transformLegacyWildcardFilter($filter); if ($filter) { $oldFilterString = $dashBoardUrl->getParams()->toString(); - $newFilterString = rawurldecode(QueryString::render($filter)); + $newFilterString = QueryString::render($filter); if ($oldFilterString !== $newFilterString) { Logger::info( 'Icinga Db Web filter of dashboard "%s" has changed from "%s" to "%s"', $name, - rawurldecode($dashBoardUrl->getParams()->toString()), - rawurldecode(QueryString::render($filter)) + $dashBoardUrl->getParams()->toString(), + QueryString::render($filter) ); $dashBoardUrl->setParams([]); $dashBoardUrl->setFilter($filter); - $dashboardConfig->url = rawurldecode($dashBoardUrl->getRelativeUrl()); + $dashboardConfig->url = $finalUrl->getRelativeUrl(); $changed = true; } } @@ -557,7 +552,7 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v $filter = QueryString::parse($legacyFilter); $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { - $filter = rawurldecode(QueryString::render($filter)); + $filter = QueryString::render($filter); if ($legacyFilter !== $filter) { $newConfigObject->filter = $filter; $newConfig->setSection($section, $newConfigObject); @@ -598,7 +593,7 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v try { $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl(); - $configObject->url = rawurldecode($urlString); + $configObject->url = $urlString; } catch (\InvalidArgumentException $err) { // Do nothing } @@ -610,7 +605,7 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v $filter = QueryString::parse($legacyFilter); $filter = UrlMigrator::transformFilter($filter); if ($filter !== false) { - $configObject->filter = rawurldecode(QueryString::render($filter)); + $configObject->filter = QueryString::render($filter); } else { unset($configObject->filter); } From 455f59a487d229a78890532cedf16b8fa60df013 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 16:54:03 +0100 Subject: [PATCH 05/17] migrate: Preserve framework params during dashboard migration --- application/clicommands/MigrateCommand.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index bc1328b6d..934cc987a 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -439,21 +439,22 @@ public function dashboardAction(): void } if (fnmatch('icingadb*', ltrim($dashboardUrlString, '/'))) { - $filter = QueryString::parse($dashBoardUrl->getParams()->toString()); - $filter = $this->transformLegacyWildcardFilter($filter); + $finalUrl = $dashBoardUrl->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); + $params = $dashBoardUrl->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); + $filter = QueryString::parse($params->toString()); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { - $oldFilterString = $dashBoardUrl->getParams()->toString(); + $oldFilterString = $params->toString(); $newFilterString = QueryString::render($filter); if ($oldFilterString !== $newFilterString) { Logger::info( 'Icinga Db Web filter of dashboard "%s" has changed from "%s" to "%s"', $name, - $dashBoardUrl->getParams()->toString(), + $params->toString(), QueryString::render($filter) ); - $dashBoardUrl->setParams([]); - $dashBoardUrl->setFilter($filter); + $finalUrl->setFilter($filter); $dashboardConfig->url = $finalUrl->getRelativeUrl(); $changed = true; From 3e91e6a95549a4473c55cfdd9bcdfbc3c513a621 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 16:54:49 +0100 Subject: [PATCH 06/17] migrate: Always render relative urls --- application/clicommands/MigrateCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 934cc987a..bd4ac813c 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -593,7 +593,7 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v $url = Url::fromPath($urlString, [], new Request()); try { - $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl(); + $urlString = UrlMigrator::transformUrl($url)->getRelativeUrl(); $configObject->url = $urlString; } catch (\InvalidArgumentException $err) { // Do nothing From b3751a417ef8ba40725c4f27473478fcbffe4542 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 16:55:13 +0100 Subject: [PATCH 07/17] migrate: Fix that preference items are checked instead of shared ones --- application/clicommands/MigrateCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index bd4ac813c..741b0f9a6 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -617,7 +617,7 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v if (! $newConfig->hasSection($section) || $override) { /** @var string $type */ $type = $configObject->get('type'); - $oldPath = $shared + $oldPath = ! $shared ? sprintf( '%s/%s/%ss.ini', Config::resolvePath('preferences'), From 3dade7387f75f6ba0fd7881ae3f53c47266706da Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 17:05:02 +0100 Subject: [PATCH 08/17] migrate: Also migrate legacy macros in navigation items --- application/clicommands/MigrateCommand.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 741b0f9a6..096cf540d 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -590,6 +590,12 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v /** @var ?string $urlString */ $urlString = $configObject->get('url'); if ($urlString !== null) { + $urlString = $configObject->url = str_replace( + ['$SERVICEDESC$', '$HOSTNAME$', '$HOSTADDRESS$', '$HOSTADDRESS6$'], + ['$service.name$', '$host.name$', '$host.address$', '$host.address6$'], + $urlString + ); + $url = Url::fromPath($urlString, [], new Request()); try { From 4372be956d74d6a96ff730012f5be8f77d84c410 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 7 Nov 2023 17:30:47 +0100 Subject: [PATCH 09/17] test: Add case for the new migrate command --- .../clicommands/MigrateCommandTest.php | 1265 +++++++++++++++++ 1 file changed, 1265 insertions(+) create mode 100644 test/php/application/clicommands/MigrateCommandTest.php diff --git a/test/php/application/clicommands/MigrateCommandTest.php b/test/php/application/clicommands/MigrateCommandTest.php new file mode 100644 index 000000000..8e60726da --- /dev/null +++ b/test/php/application/clicommands/MigrateCommandTest.php @@ -0,0 +1,1265 @@ + [ + 'initial' => [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'hosts.group_members' => [ + 'title' => 'Group Members', + 'url' => 'monitoring/list/hosts?hostgroup_name=group1|hostgroup_name=%28group2%29' + ], + 'hosts.variables' => [ + 'title' => 'Host Variables', + 'url' => 'monitoring/list/hosts?(_host_foo=bar&_host_bar=foo)|_host_rab=oof' + ], + 'hosts.wildcards' => [ + 'title' => 'Host Wildcards', + 'url' => 'monitoring/list/hosts?host_name=%2Afoo%2A|host_name=%2Abar%2A' + . '&sort=host_severity&dir=asc&limit=25' + ], + 'hosts.encoded_params' => [ + 'title' => 'Host Encoded Params', + 'url' => 'monitoring/list/hosts?host_name=%28foo%29&sort=_host_%28foo%29' + ], + 'icingadb' => [ + 'title' => 'Icinga DB' + ], + 'icingadb.no-wildcards' => [ + 'title' => 'No Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=linux-hosts' + ], + 'icingadb.wildcards' => [ + 'title' => 'Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=%2Alinux%2A' + ], + 'icingadb.also-wildcards' => [ + 'title' => 'Also Wildcards', + 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' + ], + 'icingadb.with-sort-and-limit' => [ + 'title' => 'With Sort And Limit', + 'url' => 'icingadb/hosts?host.name=%2Afoo%2A|host.name=bar&sort=host.state.severity&limit=50' + ], + 'not-monitoring-or-icingadb' => [ + 'title' => 'Not Monitoring Or Icinga DB' + ], + 'not-monitoring-or-icingadb.something' => [ + 'title' => 'Something', + 'url' => 'somewhere/something?foo=%2Abar%2A' + ] + ], + 'expected' => [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ], + 'hosts.group_members' => [ + 'title' => 'Group Members', + 'url' => 'icingadb/hosts?hostgroup.name=group1|hostgroup.name=%28group2%29' + ], + 'hosts.variables' => [ + 'title' => 'Host Variables', + 'url' => 'icingadb/hosts?(host.vars.foo=bar&host.vars.bar=foo)|host.vars.rab=oof' + ], + 'hosts.wildcards' => [ + 'title' => 'Host Wildcards', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A|host.name~%2Abar%2A' + . '&sort=host.state.severity%20asc&limit=25' + ], + 'hosts.encoded_params' => [ + 'title' => 'Host Encoded Params', + 'url' => 'icingadb/hosts?host.name=%28foo%29&sort=host.vars.%28foo%29' + ], + 'icingadb' => [ + 'title' => 'Icinga DB' + ], + 'icingadb.no-wildcards' => [ + 'title' => 'No Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=linux-hosts' + ], + 'icingadb.wildcards' => [ + 'title' => 'Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name~%2Alinux%2A' + ], + 'icingadb.also-wildcards' => [ + 'title' => 'Also Wildcards', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' + ], + 'icingadb.with-sort-and-limit' => [ + 'title' => 'With Sort And Limit', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A|host.name=bar&sort=host.state.severity&limit=50' + ], + 'not-monitoring-or-icingadb' => [ + 'title' => 'Not Monitoring Or Icinga DB' + ], + 'not-monitoring-or-icingadb.something' => [ + 'title' => 'Something', + 'url' => 'somewhere/something?foo=%2Abar%2A' + ] + ] + ], + 'menu-items' => [ + 'initial' => [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' + ] + ], + 'expected' => [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' + ] + ] + ], + 'host-actions' => [ + 'initial' => [ + 'hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A' + ], + 'hosts_encoded_params' => [ + 'type' => 'host-action', + 'url' => 'monitoring/list/hosts?host_name=%28foo%29&sort=_host_%28foo%29', + 'filter' => '_host_%28foo%29=bar' + ] + ], + 'expected' => [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A' + ], + 'hosts_encoded_params' => [ + 'type' => 'icingadb-host-action', + 'url' => 'icingadb/hosts?host.name=%28foo%29&sort=host.vars.%28foo%29', + 'filter' => 'host.vars.%28foo%29=bar' + ] + ] + ], + 'icingadb-host-actions' => [ + 'initial' => [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name=%2Afoo%2A' + ] + ], + 'expected' => [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A' + ] + ] + ], + 'service-actions' => [ + 'initial' => [ + 'services' => [ + 'type' => 'service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' + ], + 'services_encoded_params' => [ + 'type' => 'host-action', + 'url' => 'monitoring/list/services?host_name=%28foo%29&sort=_host_%28foo%29', + 'filter' => '_host_%28foo%29=bar' + ] + ], + 'expected' => [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' + ], + 'services_encoded_params' => [ + 'type' => 'icingadb-service-action', + 'url' => 'icingadb/services?host.name=%28foo%29&sort=host.vars.%28foo%29', + 'filter' => 'host.vars.%28foo%29=bar' + ] + ] + ], + 'icingadb-service-actions' => [ + 'initial' => [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=%2Abar%2A' + ] + ], + 'expected' => [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo~%2Abar%2A' + ] + ] + ], + 'shared-host-actions' => [ + 'initial' => [ + 'shared-hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A', + 'owner' => 'test' + ], + 'hosts_encoded_params' => [ + 'type' => 'host-action', + 'url' => 'monitoring/list/hosts?host_name=%28foo%29&sort=_host_%28foo%29', + 'filter' => '_host_%28foo%29=bar', + 'owner' => 'test' + ], + 'other-hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A', + 'owner' => 'not-test' + ] + ], + 'expected' => [ + 'shared-hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A', + 'owner' => 'test' + ], + 'hosts_encoded_params' => [ + 'type' => 'icingadb-host-action', + 'url' => 'icingadb/hosts?host.name=%28foo%29&sort=host.vars.%28foo%29', + 'filter' => 'host.vars.%28foo%29=bar', + 'owner' => 'test' + ] + ] + ], + 'host-actions-legacy-macros' => [ + 'initial' => [ + 'hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$HOSTNAME$,$HOSTADDRESS$,$HOSTADDRESS6$', + 'filter' => 'host_name=%2Afoo%2A' + ] + ], + 'expected' => [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$,$host.address$,$host.address6$', + 'filter' => 'host.name~%2Afoo%2A' + ] + ] + ], + 'service-actions-legacy-macros' => [ + 'initial' => [ + 'services' => [ + 'type' => 'service-action', + 'url' => 'example.com/search?q=$SERVICEDESC$,$HOSTNAME$,$HOSTADDRESS$,$HOSTADDRESS6$', + 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' + ] + ], + 'expected' => [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' + ] + ] + ], + 'all-roles' => [ + 'initial' => [ + 'no-wildcards' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'wildcards' => [ + 'monitoring/filter/objects' => 'host_name=%2Afoo%2A|hostgroup_name=%2Afoo%2A' + ], + 'encoded_column' => [ + 'monitoring/filter/objects' => '_host_%28foo%29=bar' + ], + 'blacklist' => [ + 'monitoring/blacklist/properties' => 'host.vars.foo,service.vars.bar*,host.vars.a.**.d' + ], + 'full-access' => [ + 'permissions' => 'module/monitoring,monitoring/*' + ], + 'general-read-access' => [ + 'permissions' => 'module/monitoring' + ], + 'general-write-access' => [ + 'permissions' => 'module/monitoring,monitoring/command/*' + ], + 'full-fine-grained-access' => [ + 'permissions' => 'module/monitoring' + . ',monitoring/command/schedule-check' + . ',monitoring/command/acknowledge-problem' + . ',monitoring/command/remove-acknowledgement' + . ',monitoring/command/comment/add' + . ',monitoring/command/comment/delete' + . ',monitoring/command/downtime/schedule' + . ',monitoring/command/downtime/delete' + . ',monitoring/command/process-check-result' + . ',monitoring/command/feature/instance' + . ',monitoring/command/feature/object/active-checks' + . ',monitoring/command/feature/object/passive-checks' + . ',monitoring/command/feature/object/notifications' + . ',monitoring/command/feature/object/event-handler' + . ',monitoring/command/feature/object/flap-detection' + . ',monitoring/command/send-custom-notification' + ], + 'full-with-refusals' => [ + 'permissions' => 'module/monitoring,monitoring/command/*', + 'refusals' => 'monitoring/command/downtime/*,monitoring/command/feature/instance' + ], + 'active-only' => [ + 'permissions' => 'module/monitoring,monitoring/command/schedule-check/active-only' + ], + 'no-monitoring-contacts' => [ + 'permissions' => 'module/monitoring,no-monitoring/contacts' + ], + 'reporting-only' => [ + 'permissions' => 'module/reporting' + ], + 'icingadb' => [ + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A|hostgroup.name=%2Afoo%2A', + 'icingadb/filter/services' => 'service.name=%2Abar%2A&service.vars.env=prod', + 'icingadb/filter/hosts' => 'host.vars.env=%2Afoo%2A' + ] + ], + 'expected' => [ + 'no-wildcards' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo', + 'icingadb/filter/objects' => 'host.name=foo|hostgroup.name=foo' + ], + 'wildcards' => [ + 'monitoring/filter/objects' => 'host_name=%2Afoo%2A|hostgroup_name=%2Afoo%2A', + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A|hostgroup.name~%2Afoo%2A' + ], + 'encoded_column' => [ + 'monitoring/filter/objects' => '_host_%28foo%29=bar', + 'icingadb/filter/objects' => 'host.vars.%28foo%29=bar' + ], + 'blacklist' => [ + 'monitoring/blacklist/properties' => 'host.vars.foo,service.vars.bar*,host.vars.a.**.d', + 'icingadb/denylist/variables' => 'foo,bar*,a.*.d' + ], + 'full-access' => [ + 'permissions' => 'module/monitoring,monitoring/*' + ], + 'general-read-access' => [ + 'permissions' => 'module/monitoring' + ], + 'general-write-access' => [ + 'permissions' => 'module/monitoring,monitoring/command/*,icingadb/command/*' + ], + 'full-fine-grained-access' => [ + 'permissions' => 'module/monitoring' + . ',monitoring/command/schedule-check' + . ',icingadb/command/schedule-check' + . ',monitoring/command/acknowledge-problem' + . ',icingadb/command/acknowledge-problem' + . ',monitoring/command/remove-acknowledgement' + . ',icingadb/command/remove-acknowledgement' + . ',monitoring/command/comment/add' + . ',icingadb/command/comment/add' + . ',monitoring/command/comment/delete' + . ',icingadb/command/comment/delete' + . ',monitoring/command/downtime/schedule' + . ',icingadb/command/downtime/schedule' + . ',monitoring/command/downtime/delete' + . ',icingadb/command/downtime/delete' + . ',monitoring/command/process-check-result' + . ',icingadb/command/process-check-result' + . ',monitoring/command/feature/instance' + . ',icingadb/command/feature/instance' + . ',monitoring/command/feature/object/active-checks' + . ',icingadb/command/feature/object/active-checks' + . ',monitoring/command/feature/object/passive-checks' + . ',icingadb/command/feature/object/passive-checks' + . ',monitoring/command/feature/object/notifications' + . ',icingadb/command/feature/object/notifications' + . ',monitoring/command/feature/object/event-handler' + . ',icingadb/command/feature/object/event-handler' + . ',monitoring/command/feature/object/flap-detection' + . ',icingadb/command/feature/object/flap-detection' + . ',monitoring/command/send-custom-notification' + . ',icingadb/command/send-custom-notification' + ], + 'full-with-refusals' => [ + 'permissions' => 'module/monitoring,monitoring/command/*,icingadb/command/*', + 'refusals' => 'monitoring/command/downtime/*' + . ',icingadb/command/downtime/*' + . ',monitoring/command/feature/instance' + . ',icingadb/command/feature/instance' + ], + 'active-only' => [ + 'permissions' => 'module/monitoring' + . ',monitoring/command/schedule-check/active-only' + . ',icingadb/command/schedule-check/active-only' + ], + 'no-monitoring-contacts' => [ + 'permissions' => 'module/monitoring,no-monitoring/contacts', + 'icingadb/denylist/routes' => 'users,usergroups' + ], + 'reporting-only' => [ + 'permissions' => 'module/reporting' + ], + 'icingadb' => [ + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A|hostgroup.name~%2Afoo%2A', + 'icingadb/filter/services' => 'service.name~%2Abar%2A&service.vars.env=prod', + 'icingadb/filter/hosts' => 'host.vars.env~%2Afoo%2A' + ] + ] + ], + 'single-role-or-group' => [ + 'initial' => [ + 'one' => [ + 'groups' => 'support,helpdesk', + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'two' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'three' => [ + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' + ] + ], + 'expected' => [ + 'one' => [ + 'groups' => 'support,helpdesk', + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo', + 'icingadb/filter/objects' => 'host.name=foo|hostgroup.name=foo' + ], + 'two' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'three' => [ + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' + ] + ] + ] + ]; + + protected $defaultConfigDir; + + protected $fileStorage; + + protected function setUp(): void + { + $this->defaultConfigDir = Config::$configDir; + $this->fileStorage = new TemporaryLocalFileStorage(); + + Config::$configDir = dirname($this->fileStorage->resolvePath('bogus')); + } + + protected function tearDown(): void + { + Config::$configDir = $this->defaultConfigDir; + unset($this->fileStorage); // Should clean up automatically + Config::module('monitoring', 'config', true); + } + + protected function getConfig(string $case): array + { + return [$this->config[$case]['initial'], $this->config[$case]['expected']]; + } + + protected function createConfig(string $path, array $data): void + { + $config = new Config(new ConfigObject($data)); + $config->saveIni($this->fileStorage->resolvePath($path)); + } + + protected function loadConfig(string $path): array + { + return Config::fromIni($this->fileStorage->resolvePath($path))->toArray(); + } + + protected function createCommandInstance(string ...$params): MigrateCommand + { + array_unshift($params, 'program'); + + $app = $this->createConfiguredMock(Cli::class, [ + 'getParams' => new Params($params), + 'getModuleManager' => $this->createConfiguredMock(Manager::class, [ + 'loadEnabledModules' => null + ]) + ]); + + return new MigrateCommand( + $app, + 'migrate', + 'toicingadb', + 'dashboard', + false + ); + } + + /** + * Checks the following: + * - Whether only a single user is handled + * - Whether backups are made + * - Whether a second run changes nothing, if nothing changed + * - Whether a second run keeps the backup, if nothing changed + * - Whether a new backup isn't made, if nothing changed + * - Whether existing Icinga DB dashboards are transformed regarding wildcard filters + */ + public function testDashboardMigrationBehavesAsExpectedByDefault() + { + [$initialConfig, $expected] = $this->getConfig('dashboards'); + + $this->createConfig('dashboards/test/dashboard.ini', $initialConfig); + $this->createConfig('dashboards/test2/dashboard.ini', $initialConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->dashboardAction(); + + $config = $this->loadConfig('dashboards/test/dashboard.ini'); + $this->assertSame($expected, $config); + + $config2 = $this->loadConfig('dashboards/test2/dashboard.ini'); + $this->assertSame($initialConfig, $config2); + + $backup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); + $this->assertSame($initialConfig, $backup); + + $command = $this->createCommandInstance('--user', 'test'); + $command->dashboardAction(); + + $configAfterSecondRun = $this->loadConfig('dashboards/test/dashboard.ini'); + $this->assertSame($config, $configAfterSecondRun); + + $backupAfterSecondRun = $this->loadConfig('dashboards/test/dashboard.backup.ini'); + $this->assertSame($backup, $backupAfterSecondRun); + + $backup1AfterSecondRun = $this->loadConfig('dashboards/test/dashboard.backup1.ini'); + $this->assertEmpty($backup1AfterSecondRun); + } + + /** + * Checks the following: + * - Whether a second run creates a new backup, if something changed + */ + public function testDashboardMigrationCreatesMultipleBackups() + { + $initialOldConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ] + ]; + $initialNewConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ], + 'hosts.group_members' => [ + 'title' => 'Group Members', + 'url' => 'monitoring/list/hosts?hostgroup_name=group1|hostgroup_name=group2' + ] + ]; + $expectedNewConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ] + ]; + $expectedFinalConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ], + 'hosts.group_members' => [ + 'title' => 'Group Members', + 'url' => 'icingadb/hosts?hostgroup.name=group1|hostgroup.name=group2' + ] + ]; + + $this->createConfig('dashboards/test/dashboard.ini', $initialOldConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->dashboardAction(); + + $newConfig = $this->loadConfig('dashboards/test/dashboard.ini'); + $this->assertSame($expectedNewConfig, $newConfig); + $oldBackup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); + $this->assertSame($initialOldConfig, $oldBackup); + + $this->createConfig('dashboards/test/dashboard.ini', $initialNewConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->dashboardAction(); + + $finalConfig = $this->loadConfig('dashboards/test/dashboard.ini'); + $this->assertSame($expectedFinalConfig, $finalConfig); + $newBackup = $this->loadConfig('dashboards/test/dashboard.backup1.ini'); + $this->assertSame($initialNewConfig, $newBackup); + } + + /** + * Checks the following: + * - Whether backups are skipped + * + * @depends testDashboardMigrationBehavesAsExpectedByDefault + */ + public function testDashboardMigrationSkipsBackupIfRequested() + { + [$initialConfig, $expected] = $this->getConfig('dashboards'); + + $this->createConfig('dashboards/test/dashboard.ini', $initialConfig); + + $command = $this->createCommandInstance('--user', 'test', '--no-backup'); + $command->dashboardAction(); + + $config = $this->loadConfig('dashboards/test/dashboard.ini'); + $this->assertSame($expected, $config); + + $backup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); + $this->assertEmpty($backup); + } + + /** + * Checks the following: + * - Whether multiple users are handled + * - Whether multiple backups are made + * + * @depends testDashboardMigrationBehavesAsExpectedByDefault + */ + public function testDashboardMigrationMigratesAllUsers() + { + [$initialConfig, $expected] = $this->getConfig('dashboards'); + + $users = ['foo', 'bar', 'raboof']; + + foreach ($users as $user) { + $this->createConfig("dashboards/$user/dashboard.ini", $initialConfig); + } + + $command = $this->createCommandInstance('--user', '*'); + $command->dashboardAction(); + + foreach ($users as $user) { + $config = $this->loadConfig("dashboards/$user/dashboard.ini"); + $this->assertSame($expected, $config); + + $backup = $this->loadConfig("dashboards/$user/dashboard.backup.ini"); + $this->assertSame($initialConfig, $backup); + } + } + + public function testDashboardMigrationExpectsUserSwitch() + { + $this->expectException(MissingParameterException::class); + $this->expectExceptionMessage('Required parameter \'user\' missing'); + + $command = $this->createCommandInstance(); + $command->dashboardAction(); + } + + /** + * Checks the following: + * - Whether only a single user is handled + * - Whether shared host actions are migrated, depending on the owner + * - Whether old configs are kept + * - Whether a second run changes nothing + */ + public function testNavigationMigrationBehavesAsExpectedByDefault() + { + [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions'); + [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions'); + + $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); + $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); + $this->createConfig('preferences/test2/host-actions.ini', $initialHostConfig); + $this->createConfig('preferences/test2/service-actions.ini', $initialServiceConfig); + + [$initialSharedConfig, $expectedShared] = $this->getConfig('shared-host-actions'); + $this->createConfig('navigation/host-actions.ini', $initialSharedConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $this->assertSame($expectedHosts, $hosts); + $this->assertSame($expectedServices, $services); + + $sharedConfig = $this->loadConfig('navigation/icingadb-host-actions.ini'); + $this->assertSame($expectedShared, $sharedConfig); + + $hosts2 = $this->loadConfig('preferences/test2/icingadb-host-actions.ini'); + $services2 = $this->loadConfig('preferences/test2/icingadb-service-actions.ini'); + $this->assertEmpty($hosts2); + $this->assertEmpty($services2); + + $oldHosts = $this->loadConfig('preferences/test/host-actions.ini'); + $oldServices = $this->loadConfig('preferences/test/service-actions.ini'); + $this->assertSame($initialHostConfig, $oldHosts); + $this->assertSame($initialServiceConfig, $oldServices); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $hostsAfterSecondRun = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $servicesAfterSecondRun = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $this->assertSame($hosts, $hostsAfterSecondRun); + $this->assertSame($services, $servicesAfterSecondRun); + } + + /** + * Checks the following: + * - Whether existing Icinga DB Actions are transformed regarding wildcard filters + */ + public function testNavigationMigrationTransformsAlreadyExistingIcingaDBActions() + { + [$initialHostConfig, $expectedHosts] = $this->getConfig('icingadb-host-actions'); + [$initialServiceConfig, $expectedServices] = $this->getConfig('icingadb-service-actions'); + + $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialHostConfig); + $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialServiceConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $this->assertSame($expectedHosts, $hosts); + $this->assertSame($expectedServices, $services); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $hostsAfterSecondRun = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $servicesAfterSecondRun = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $this->assertSame($hosts, $hostsAfterSecondRun); + $this->assertSame($services, $servicesAfterSecondRun); + } + + /** + * Checks the following: + * - Whether legacy host/service macros are migrated + */ + public function testNavigationMigrationMigratesLegacyMacros() + { + [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions-legacy-macros'); + [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions-legacy-macros'); + + $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); + $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $this->assertSame($expectedHosts, $hosts); + $this->assertSame($expectedServices, $services); + } + + /** + * Checks the following: + * - Whether old configs are removed + */ + public function testNavigationMigrationDeletesOldConfigsIfRequested() + { + [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions'); + [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions'); + + $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); + $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); + + $command = $this->createCommandInstance('--user', 'test', '--delete'); + $command->navigationAction(); + + $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $this->assertSame($expectedHosts, $hosts); + $this->assertSame($expectedServices, $services); + + $oldHosts = $this->loadConfig('preferences/test/host-actions.ini'); + $oldServices = $this->loadConfig('preferences/test/service-actions.ini'); + $this->assertEmpty($oldHosts); + $this->assertEmpty($oldServices); + } + + /** + * Checks the following: + * - Whether existing configs are left alone by default + * - Whether existing configs are overridden if requested + */ + public function testNavigationMigrationOverridesExistingActionsIfRequested() + { + $initialOldUserConfig = [ + 'hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A' + ] + ]; + $initialOldSharedConfig = [ + 'hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A', + 'owner' => 'test' + ] + ]; + $initialNewUserConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Abar%2A' + ] + ]; + $initialNewSharedConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Abar%2A', + 'owner' => 'test' + ] + ]; + $expectedFinalUserConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A' + ] + ]; + $expectedFinalSharedConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A', + 'owner' => 'test' + ] + ]; + + $this->createConfig('preferences/test/host-actions.ini', $initialOldUserConfig); + $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialNewUserConfig); + $this->createConfig('navigation/host-actions.ini', $initialOldSharedConfig); + $this->createConfig('navigation/icingadb-host-actions.ini', $initialNewSharedConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $finalUserConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $this->assertSame($initialNewUserConfig, $finalUserConfig); + + $finalSharedConfig = $this->loadConfig('navigation/icingadb-host-actions.ini'); + $this->assertSame($initialNewSharedConfig, $finalSharedConfig); + + $command = $this->createCommandInstance('--user', 'test', '--override'); + $command->navigationAction(); + + $finalUserConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $this->assertSame($expectedFinalUserConfig, $finalUserConfig); + + $finalSharedConfig = $this->loadConfig('navigation/icingadb-host-actions.ini'); + $this->assertSame($expectedFinalSharedConfig, $finalSharedConfig); + } + + public function testNavigationMigrationExpectsUserSwitch() + { + $this->expectException(MissingParameterException::class); + $this->expectExceptionMessage('Required parameter \'user\' missing'); + + $command = $this->createCommandInstance(); + $command->navigationAction(); + } + + /** + * Checks the following: + * - Whether only a single role is handled + * - Whether role name matching works + */ + public function testRoleMigrationHandlesASingleRoleOnlyIfRequested() + { + [$initialConfig, $expected] = $this->getConfig('single-role-or-group'); + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--role', 'one'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expected, $config); + } + + /** + * Checks the following: + * - Whether only a single role is handled + * - Whether group matching works + */ + public function testRoleMigrationHandlesARoleWithMatchingGroups() + { + [$initialConfig, $expected] = $this->getConfig('single-role-or-group'); + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--group', 'support'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expected, $config); + } + + /** + * Checks the following: + * - Whether permissions are properly migrated + * - Whether refusals are properly migrated + * - Whether restrictions are properly migrated + * - Whether blacklists are properly migrated + */ + public function testRoleMigrationMigratesAllRoles() + { + [$initialConfig, $expected] = $this->getConfig('all-roles'); + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--role', '*'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expected, $config); + } + + /** + * Checks the following: + * - Whether monitoring's variable protection rules are migrated to all roles granting access to monitoring + */ + public function testRoleMigrationAlsoMigratesVariableProtections() + { + $initialConfig = [ + 'one' => [ + 'permissions' => 'module/monitoring' + ], + 'two' => [ + 'permissions' => 'module/monitoring' + ], + 'three' => [ + 'permissions' => 'module/reporting' + ] + ]; + $expectedConfig = [ + 'one' => [ + 'permissions' => 'module/monitoring', + 'icingadb/protect/variables' => 'ob.*,env' + ], + 'two' => [ + 'permissions' => 'module/monitoring', + 'icingadb/protect/variables' => 'ob.*,env' + ], + 'three' => [ + 'permissions' => 'module/reporting' + ] + ]; + + $this->createConfig('modules/monitoring/config.ini', [ + 'security' => [ + 'protected_customvars' => 'ob.*,env' + ] + ]); + + // Invalidate config cache + Config::module('monitoring', 'config', true); + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--role', '*'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expectedConfig, $config); + } + + /** + * Checks the following: + * - Whether already migrated roles are skipped during migration + * - Whether already migrated roles are transformed regarding wildcard filters + */ + public function testRoleMigrationSkipsRolesThatAlreadyGrantAccessToIcingaDbButTransformWildcardRestrictions() + { + $initialConfig = [ + 'only-monitoring' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*', + 'monitoring/filter/objects' => 'host_name=%2Afoo%2A' + ], + 'monitoring-and-icingadb' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*,module/icingadb', + 'monitoring/filter/objects' => 'host_name=%2Abar%2A', + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' + ] + ]; + $expectedConfig = [ + 'only-monitoring' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*,icingadb/command/comment/*', + 'monitoring/filter/objects' => 'host_name=%2Afoo%2A', + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' + ], + 'monitoring-and-icingadb' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*,module/icingadb', + 'monitoring/filter/objects' => 'host_name=%2Abar%2A', + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' + ] + ]; + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--role', '*'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expectedConfig, $config); + } + + /** + * Checks the following: + * - Whether already migrated roles are reset if requested + */ + public function testRoleMigrationOverridesAlreadyMigratedRolesIfRequested() + { + $initialConfig = [ + 'only-monitoring' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*', + 'monitoring/filter/objects' => 'host_name=%2Afoo%2A' + ], + 'monitoring-and-icingadb' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*,module/icingadb', + 'monitoring/filter/objects' => 'host_name=%2Abar%2A', + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' + ] + ]; + $expectedConfig = [ + 'only-monitoring' => [ + 'permissions' => 'module/monitoring,monitoring/command/comment/*,icingadb/command/comment/*', + 'monitoring/filter/objects' => 'host_name=%2Afoo%2A', + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' + ], + 'monitoring-and-icingadb' => [ + 'permissions' => 'module/monitoring' + . ',monitoring/command/comment/*' + . ',icingadb/command/comment/*', + 'monitoring/filter/objects' => 'host_name=%2Abar%2A', + 'icingadb/filter/objects' => 'host.name~%2Abar%2A' + ] + ]; + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--role', '*', '--override'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expectedConfig, $config); + } + + public function testRoleMigrationExpectsTheRoleOrGroupSwitch() + { + $this->expectException(IcingaException::class); + $this->expectExceptionMessage("One of the parameters 'group' or 'role' must be supplied"); + + $command = $this->createCommandInstance(); + $command->roleAction(); + } + + public function testRoleMigrationExpectsEitherTheRoleOrGroupSwitchButNotBoth() + { + $this->expectException(IcingaException::class); + $this->expectExceptionMessage("Use either 'group' or 'role'. Both cannot be used as role overrules group."); + + $command = $this->createCommandInstance('--role=foo', '--group=bar'); + $command->roleAction(); + } + + public function testFilterMigrationWorksAsExpected() + { + $initialHostActionConfig = [ + 'hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A' + ] + ]; + $expectedHostActionConfig = $initialHostActionConfig; + + $initialIcingadbHostActionConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name=%2Afoo%2A' + ] + ]; + $expectedIcingadbHostActionConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A' + ] + ]; + + $initialServiceActionConfig = [ + 'services' => [ + 'type' => 'service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' + ] + ]; + $expectedServiceActionConfig = $initialServiceActionConfig; + + $initialIcingadbServiceActionConfig = [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=bar&service.vars.bar=%2Afoo%2A' + ] + ]; + $expectedIcingadbServiceActionConfig = [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' + ] + ]; + + $initialDashboardConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb' => [ + 'title' => 'Icinga DB' + ], + 'icingadb.wildcards' => [ + 'title' => 'Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=%2Alinux%2A' + ] + ]; + $expectedDashboardConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb' => [ + 'title' => 'Icinga DB' + ], + 'icingadb.wildcards' => [ + 'title' => 'Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name~%2Alinux%2A' + ] + ]; + + $initialRoleConfig = [ + 'one' => [ + 'groups' => 'support,helpdesk', + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'two' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'three' => [ + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' + ] + ]; + $expectedRoleConfig = [ + 'one' => [ + 'groups' => 'support,helpdesk', + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'two' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'three' => [ + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' + ] + ]; + + $this->createConfig('preferences/test/host-actions.ini', $initialHostActionConfig); + $this->createConfig('preferences/test/service-actions.ini', $initialServiceActionConfig); + $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialIcingadbHostActionConfig); + $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialIcingadbServiceActionConfig); + $this->createConfig('dashboards/test/dashboard.ini', $initialDashboardConfig); + $this->createConfig('roles.ini', $initialRoleConfig); + + $command = $this->createCommandInstance(); + $command->filterAction(); + + $hostActionConfig = $this->loadConfig('preferences/test/host-actions.ini'); + $serviceActionConfig = $this->loadConfig('preferences/test/service-actions.ini'); + $icingadbHostActionConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $icingadbServiceActionConfig = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $dashboardConfig = $this->loadConfig('dashboards/test/dashboard.ini'); + $roleConfig = $this->loadConfig('roles.ini'); + + $this->assertSame($expectedHostActionConfig, $hostActionConfig); + $this->assertSame($expectedServiceActionConfig, $serviceActionConfig); + $this->assertSame($expectedIcingadbHostActionConfig, $icingadbHostActionConfig); + $this->assertSame($expectedIcingadbServiceActionConfig, $icingadbServiceActionConfig); + $this->assertSame($expectedDashboardConfig, $dashboardConfig); + $this->assertSame($expectedRoleConfig, $roleConfig); + } +} From cc43d994243407cc2d88b0b750eb6732c68f611b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 10 Nov 2023 15:10:59 +0100 Subject: [PATCH 10/17] migrate: Avoid false-positives --- application/clicommands/MigrateCommand.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 096cf540d..776f41a2f 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -13,7 +13,7 @@ use Icinga\Module\Icingadb\Compat\UrlMigrator; use Icinga\Util\DirectoryIterator; use Icinga\Web\Request; -use ipl\Stdlib\Filter; +use ipl\Stdlib\Str; use ipl\Web\Filter\QueryString; use ipl\Web\Url; @@ -295,11 +295,11 @@ public function roleAction(): void } foreach (explode(',', $role['permissions']) as $permission) { - if (str_contains($permission, 'icingadb')) { + if (Str::startsWith($permission, 'icingadb/') || $permission === 'module/icingadb') { continue; - } elseif (fnmatch('monitoring/command*', $permission)) { + } elseif (Str::startsWith($permission, 'monitoring/command/')) { $updatedPermissions[] = $permission; - $updatedPermissions[] = str_replace('monitoring', 'icingadb', $permission); + $updatedPermissions[] = str_replace('monitoring/', 'icingadb/', $permission); } elseif ($permission === 'no-monitoring/contacts') { $updatedPermissions[] = $permission; $role['icingadb/denylist/routes'] = 'users,usergroups'; @@ -319,11 +319,11 @@ public function roleAction(): void ); foreach (explode(',', $role['refusals']) as $refusal) { - if (str_contains($refusal, 'icingadb')) { + if (Str::startsWith($refusal, 'icingadb/') || $refusal === 'module/icingadb') { continue; - } elseif (fnmatch('monitoring/command*', $refusal)) { + } elseif (Str::startsWith($refusal, 'monitoring/command/')) { $updatedRefusals[] = $refusal; - $updatedRefusals[] = str_replace('monitoring', 'icingadb', $refusal); + $updatedRefusals[] = str_replace('monitoring/', 'icingadb/', $refusal); } else { $updatedRefusals[] = $refusal; } @@ -433,12 +433,12 @@ public function dashboardAction(): void $dashboardUrlString = $dashboardConfig->get('url'); if ($dashboardUrlString !== null) { $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request()); - if (! $this->skipMigration && fnmatch('monitoring*', $dashboardUrlString)) { + if (! $this->skipMigration && Str::startsWith(ltrim($dashboardUrlString, '/'), 'monitoring/')) { $dashboardConfig->url = UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl(); $changed = true; } - if (fnmatch('icingadb*', ltrim($dashboardUrlString, '/'))) { + if (Str::startsWith(ltrim($dashboardUrlString, '/'), 'icingadb/')) { $finalUrl = $dashBoardUrl->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); $params = $dashBoardUrl->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); $filter = QueryString::parse($params->toString()); From 8a3407b33cf3e7c54f60a65df8286ad78bdcd548 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 10 Nov 2023 15:12:31 +0100 Subject: [PATCH 11/17] migrate: Also transform/migrate menu items --- application/clicommands/MigrateCommand.php | 343 +++++++++++------- .../clicommands/MigrateCommandTest.php | 177 ++++++++- 2 files changed, 384 insertions(+), 136 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 776f41a2f..5e55d8621 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -44,13 +44,14 @@ public function init(): void * * --override Override the existing Icinga DB navigation items * - * --delete Remove the legacy files after successfully + * --no-backup Remove the legacy files after successfully * migrated the navigation items. */ public function navigationAction(): void { /** @var string $user */ $user = $this->params->getRequired('user'); + $noBackup = $this->params->get('no-backup'); $preferencesPath = Config::resolvePath('preferences'); $sharedNavigation = Config::resolvePath('navigation'); @@ -62,6 +63,7 @@ public function navigationAction(): void $rc = 0; $directories = new DirectoryIterator($preferencesPath); + /** @var string $directory */ foreach ($directories as $directory) { /** @var string $username */ $username = $directories->key() === false ? '' : $directories->key(); @@ -69,67 +71,91 @@ public function navigationAction(): void continue; } + $menuItems = $this->readFromIni($directory . '/menu.ini', $rc); $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc); $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc); $icingadbHostActions = $this->readFromIni($directory . '/icingadb-host-actions.ini', $rc); $icingadbServiceActions = $this->readFromIni($directory . '/icingadb-service-actions.ini', $rc); + $menuUpdated = false; + $originalMenuItems = $this->readFromIni($directory . '/menu.ini', $rc); + Logger::info( - 'Transforming legacy wildcard filters of existing Icinga DB Web actions for user "%s"', + 'Transforming legacy wildcard filters of existing Icinga DB Web items for user "%s"', $username ); + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->transformNavigationItems($menuItems, $username, $rc); + } + if (! $icingadbHostActions->isEmpty()) { - $this->migrateNavigationItems($icingadbHostActions, false, $rc); + $this->transformNavigationItems($icingadbHostActions, $username, $rc); } if (! $icingadbServiceActions->isEmpty()) { - $this->migrateNavigationItems( + $this->transformNavigationItems( $icingadbServiceActions, - false, + $username, $rc ); } if (! $this->skipMigration) { - Logger::info('Migrating monitoring navigation items for user "%s" to Icinga DB Web actions', $username); + Logger::info('Migrating monitoring navigation items for user "%s" to Icinga DB Web', $username); + + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->migrateNavigationItems($menuItems, $username, $directory . '/menu.ini', $rc); + } if (! $hostActions->isEmpty()) { $this->migrateNavigationItems( $hostActions, - false, - $rc, - $directory . '/icingadb-host-actions.ini' + $username, + $directory . '/icingadb-host-actions.ini', + $rc ); } if (! $serviceActions->isEmpty()) { $this->migrateNavigationItems( $serviceActions, - false, - $rc, - $directory . '/icingadb-service-actions.ini' + $username, + $directory . '/icingadb-service-actions.ini', + $rc ); } } + + if ($menuUpdated && ! $noBackup) { + $this->createBackupIni("$directory/menu", $originalMenuItems); + } } // Start migrating shared navigation items + $menuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc); $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc); $icingadbHostActions = $this->readFromIni($sharedNavigation . '/icingadb-host-actions.ini', $rc); $icingadbServiceActions = $this->readFromIni($sharedNavigation . '/icingadb-service-actions.ini', $rc); - Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web actions'); + $menuUpdated = false; + $originalMenuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); + + Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web'); + + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->transformNavigationItems($menuItems, $user, $rc); + } if (! $icingadbHostActions->isEmpty()) { - $this->migrateNavigationItems($icingadbHostActions, true, $rc); + $this->transformNavigationItems($icingadbHostActions, $user, $rc); } if (! $icingadbServiceActions->isEmpty()) { - $this->migrateNavigationItems( + $this->transformNavigationItems( $icingadbServiceActions, - true, + $user, $rc ); } @@ -137,25 +163,33 @@ public function navigationAction(): void if (! $this->skipMigration) { Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->migrateNavigationItems($menuItems, $user, $sharedNavigation . '/menu.ini', $rc); + } + if (! $hostActions->isEmpty()) { $this->migrateNavigationItems( $hostActions, - true, - $rc, - $sharedNavigation . '/icingadb-host-actions.ini' + $user, + $sharedNavigation . '/icingadb-host-actions.ini', + $rc ); } if (! $serviceActions->isEmpty()) { $this->migrateNavigationItems( $serviceActions, - true, - $rc, - $sharedNavigation . '/icingadb-service-actions.ini' + $user, + $sharedNavigation . '/icingadb-service-actions.ini', + $rc ); } } + if ($menuUpdated && ! $noBackup) { + $this->createBackupIni("$sharedNavigation/menu", $originalMenuItems); + } + if ($rc > 0) { if ($this->skipMigration) { Logger::error('Failed to transform some icingadb navigation items'); @@ -411,6 +445,8 @@ public function dashboardAction(): void $rc = 0; $directories = new DirectoryIterator($dashboardsPath); + + /** @var string $directory */ foreach ($directories as $directory) { /** @var string $userName */ $userName = $directories->key() === false ? '' : $directories->key(); @@ -466,19 +502,7 @@ public function dashboardAction(): void if ($changed && $noBackup === null) { - $counter = 0; - while (true) { - $filepath = $counter > 0 - ? $directory . "/dashboard.backup$counter.ini" - : $directory . '/dashboard.backup.ini'; - - if (! file_exists($filepath)) { - $backupConfig->saveIni($filepath); - break; - } else { - $counter++; - } - } + $this->createBackupIni("$directory/dashboard", $backupConfig); } try { @@ -525,28 +549,21 @@ public function filterAction(): void $this->roleAction(); } - /** - * Migrate the given config to the given new config path - * - * @param Config $config - * @param ?string $path - * @param bool $shared - * @param int $rc - */ - private function migrateNavigationItems($config, $shared, &$rc, $path = null): void + private function transformNavigationItems(Config $config, string $owner, int &$rc): bool { - /** @var string $owner */ - $owner = $this->params->getRequired('user'); - if ($path === null) { - $newConfig = $config; - /** @var ConfigObject $newConfigObject */ - foreach ($newConfig->getConfigObject() as $section => $newConfigObject) { - /** @var string $configOwner */ - $configOwner = $newConfigObject->get('owner') ?? ''; - if ($shared && ! fnmatch($owner, $configOwner)) { - continue; - } + $updated = false; + /** @var ConfigObject $newConfigObject */ + foreach ($config->getConfigObject() as $section => $newConfigObject) { + /** @var string $configOwner */ + $configOwner = $newConfigObject->get('owner') ?? ''; + if ($configOwner && $configOwner !== $owner) { + continue; + } + if ( + $newConfigObject->get('type') === 'icingadb-host-action' + || $newConfigObject->get('type') === 'icingadb-service-action' + ) { /** @var ?string $legacyFilter */ $legacyFilter = $newConfigObject->get('filter'); if ($legacyFilter !== null) { @@ -556,7 +573,7 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v $filter = QueryString::render($filter); if ($legacyFilter !== $filter) { $newConfigObject->filter = $filter; - $newConfig->setSection($section, $newConfigObject); + $updated = true; Logger::info( 'Icinga DB Web filter of action "%s" is changed from %s to "%s"', $section, @@ -567,102 +584,144 @@ private function migrateNavigationItems($config, $shared, &$rc, $path = null): v } } } - } else { - $deleteLegacyFiles = $this->params->get('delete'); - $override = $this->params->get('override'); - $newConfig = $this->readFromIni($path, $rc); - - /** @var ConfigObject $configObject */ - foreach ($config->getConfigObject() as $configObject) { - // Change the config type from "host-action" to icingadb's new action - /** @var string $configOwner */ - $configOwner = $configObject->get('owner') ?? ''; - if ($shared && ! fnmatch($owner, $configOwner)) { - continue; - } - if (strpos($path, 'icingadb-host-actions') !== false) { - $configObject->type = 'icingadb-host-action'; - } else { - $configObject->type = 'icingadb-service-action'; + /** @var string $url */ + $url = $newConfigObject->get('url'); + if ($url && Str::startsWith(ltrim($url, '/'), 'icingadb/')) { + $url = Url::fromPath($url, [], new Request()); + $finalUrl = $url->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); + $params = $url->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); + $filter = QueryString::parse($params->toString()); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); + if ($filter) { + $oldFilterString = $params->toString(); + $newFilterString = QueryString::render($filter); + + if ($oldFilterString !== $newFilterString) { + Logger::info( + 'Icinga Db Web filter of navigation item "%s" has changed from "%s" to "%s"', + $section, + $oldFilterString, + $newFilterString + ); + + $newConfigObject->url = $finalUrl->setFilter($filter)->getRelativeUrl(); + $updated = true; + } } + } + } - /** @var ?string $urlString */ - $urlString = $configObject->get('url'); - if ($urlString !== null) { - $urlString = $configObject->url = str_replace( - ['$SERVICEDESC$', '$HOSTNAME$', '$HOSTADDRESS$', '$HOSTADDRESS6$'], - ['$service.name$', '$host.name$', '$host.address$', '$host.address6$'], - $urlString - ); + if ($updated) { + try { + $config->saveIni(); + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + $rc = 256; - $url = Url::fromPath($urlString, [], new Request()); + return false; + } + } - try { - $urlString = UrlMigrator::transformUrl($url)->getRelativeUrl(); - $configObject->url = $urlString; - } catch (\InvalidArgumentException $err) { - // Do nothing - } - } + return $updated; + } - /** @var ?string $legacyFilter */ - $legacyFilter = $configObject->get('filter'); - if ($legacyFilter !== null) { - $filter = QueryString::parse($legacyFilter); - $filter = UrlMigrator::transformFilter($filter); - if ($filter !== false) { - $configObject->filter = QueryString::render($filter); - } else { - unset($configObject->filter); - } - } + /** + * Migrate the given config to the given new config path + * + * @param Config $config + * @param string $owner + * @param string $path + * @param int $rc + * + * @return bool + */ + private function migrateNavigationItems(Config $config, string $owner, string $path, int &$rc): bool + { + $deleteLegacyFiles = $this->params->get('no-backup'); + $override = $this->params->get('override'); + $newConfig = $config->getConfigFile() === $path ? $config : $this->readFromIni($path, $rc); + + $updated = false; + /** @var ConfigObject $configObject */ + foreach ($config->getConfigObject() as $configObject) { + /** @var string $configOwner */ + $configOwner = $configObject->get('owner') ?? ''; + if ($configOwner && $configOwner !== $owner) { + continue; + } - $section = $config->key(); - - if (! $newConfig->hasSection($section) || $override) { - /** @var string $type */ - $type = $configObject->get('type'); - $oldPath = ! $shared - ? sprintf( - '%s/%s/%ss.ini', - Config::resolvePath('preferences'), - $configOwner, - $type - ) - : sprintf( - '%s/%ss.ini', - Config::resolvePath('navigation'), - $type - ); + $migrateFilter = false; + if ($configObject->type === 'host-action') { + $updated = true; + $migrateFilter = true; + $configObject->type = 'icingadb-host-action'; + } elseif ($configObject->type === 'service-action') { + $updated = true; + $migrateFilter = true; + $configObject->type = 'icingadb-service-action'; + } - $oldConfig = $this->readFromIni($oldPath, $rc); + /** @var ?string $urlString */ + $urlString = $configObject->get('url'); + if ($urlString !== null) { + $urlString = str_replace( + ['$SERVICEDESC$', '$HOSTNAME$', '$HOSTADDRESS$', '$HOSTADDRESS6$'], + ['$service.name$', '$host.name$', '$host.address$', '$host.address6$'], + $urlString + ); + if ($urlString !== $configObject->url) { + $configObject->url = $urlString; + $updated = true; + } - if ($override && $oldConfig->hasSection($section)) { - $oldConfig->removeSection($section); - $oldConfig->saveIni(); - } + $url = Url::fromPath($urlString, [], new Request()); - if (! $oldConfig->hasSection($section)) { - $newConfig->setSection($section, $configObject); - } + try { + $urlString = UrlMigrator::transformUrl($url)->getRelativeUrl(); + $configObject->url = $urlString; + $updated = true; + } catch (\InvalidArgumentException $err) { + // Do nothing } } + + /** @var ?string $legacyFilter */ + $legacyFilter = $configObject->get('filter'); + if ($migrateFilter && $legacyFilter) { + $updated = true; + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformFilter($filter); + if ($filter !== false) { + $configObject->filter = QueryString::render($filter); + } else { + unset($configObject->filter); + } + } + + $section = $config->key(); + if (! $newConfig->hasSection($section) || $newConfig === $config || $override) { + $newConfig->setSection($section, $configObject); + } } - try { - if (! $newConfig->isEmpty()) { + if ($updated) { + try { $newConfig->saveIni(); // Remove the legacy file only if explicitly requested - if ($path !== null && $deleteLegacyFiles) { + if ($deleteLegacyFiles && $newConfig !== $config) { unlink($config->getConfigFile()); } + } catch (NotWritableError $error) { + Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + $rc = 256; + + return false; } - } catch (NotWritableError $error) { - Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); - $rc = 256; } + + return $updated; } /** @@ -691,6 +750,28 @@ private function readFromIni($path, &$rc) return $config; } + private function createBackupIni(string $path, Config $config = null): void + { + $counter = 0; + while (true) { + $filepath = $counter > 0 + ? "$path.backup$counter.ini" + : "$path.backup.ini"; + + if (! file_exists($filepath)) { + if ($config) { + $config->saveIni($filepath); + } else { + copy("$path.ini", $filepath); + } + + break; + } else { + $counter++; + } + } + } + /** * Checks if the given role should be updated * diff --git a/test/php/application/clicommands/MigrateCommandTest.php b/test/php/application/clicommands/MigrateCommandTest.php index 8e60726da..e14d1b427 100644 --- a/test/php/application/clicommands/MigrateCommandTest.php +++ b/test/php/application/clicommands/MigrateCommandTest.php @@ -160,6 +160,60 @@ class MigrateCommandTest extends TestCase ] ] ], + 'shared-menu-items' => [ + 'initial' => [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo', + 'owner' => 'test' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1', + 'owner' => 'test' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name=%2Afoo%2A', + 'owner' => 'test' + ], + 'other-monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1', + 'owner' => 'not-test' + ] + ], + 'expected' => [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo', + 'owner' => 'test' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.state.is_problem=y', + 'owner' => 'test' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A', + 'owner' => 'test' + ], + 'other-monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1', + 'owner' => 'not-test' + ] + ] + ], 'host-actions' => [ 'initial' => [ 'hosts' => [ @@ -222,7 +276,7 @@ class MigrateCommandTest extends TestCase 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' ], 'services_encoded_params' => [ - 'type' => 'icingadb-service-action', + 'type' => 'icingadb-host-action', 'url' => 'icingadb/services?host.name=%28foo%29&sort=host.vars.%28foo%29', 'filter' => 'host.vars.%28foo%29=bar' ] @@ -717,26 +771,46 @@ public function testDashboardMigrationExpectsUserSwitch() /** * Checks the following: * - Whether only a single user is handled - * - Whether shared host actions are migrated, depending on the owner - * - Whether old configs are kept - * - Whether a second run changes nothing + * - Whether shared items are migrated, depending on the owner + * - Whether old configs are kept/or backups are created + * - Whether a second run changes nothing, if nothing changed + * - Whether a second run keeps the backup, if nothing changed + * - Whether a new backup isn't created, if nothing changed */ public function testNavigationMigrationBehavesAsExpectedByDefault() { + [$initialMenuConfig, $expectedMenu] = $this->getConfig('menu-items'); [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions'); [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions'); + $this->createConfig('preferences/test/menu.ini', $initialMenuConfig); $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); + $this->createConfig('preferences/test2/menu.ini', $initialMenuConfig); $this->createConfig('preferences/test2/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test2/service-actions.ini', $initialServiceConfig); + [$initialSharedMenuConfig, $expectedSharedMenu] = $this->getConfig('shared-menu-items'); + $this->createConfig('navigation/menu.ini', $initialSharedMenuConfig); + [$initialSharedConfig, $expectedShared] = $this->getConfig('shared-host-actions'); $this->createConfig('navigation/host-actions.ini', $initialSharedConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); + $menuConfig = $this->loadConfig('preferences/test/menu.ini'); + $this->assertSame($expectedMenu, $menuConfig); + + $sharedMenuConfig = $this->loadConfig('navigation/menu.ini'); + $this->assertSame($expectedSharedMenu, $sharedMenuConfig); + + $menuConfig2 = $this->loadConfig('preferences/test2/menu.ini'); + $this->assertSame($initialMenuConfig, $menuConfig2); + + $menuBackup = $this->loadConfig('preferences/test/menu.backup.ini'); + $this->assertSame($initialMenuConfig, $menuBackup); + $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($expectedHosts, $hosts); @@ -758,12 +832,105 @@ public function testNavigationMigrationBehavesAsExpectedByDefault() $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); + $menuConfigAfterSecondRun = $this->loadConfig('preferences/test/menu.ini'); + $this->assertSame($menuConfig, $menuConfigAfterSecondRun); + + $menuBackupAfterSecondRun = $this->loadConfig('preferences/test/menu.backup.ini'); + $this->assertSame($menuBackup, $menuBackupAfterSecondRun); + + $menuBackup1AfterSecondRun = $this->loadConfig('preferences/test/menu.backup1.ini'); + $this->assertEmpty($menuBackup1AfterSecondRun); + $hostsAfterSecondRun = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $servicesAfterSecondRun = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($hosts, $hostsAfterSecondRun); $this->assertSame($services, $servicesAfterSecondRun); } + /** + * Checks the following: + * - Whether a second run creates a new backup, if something changed + * + * @depends testNavigationMigrationBehavesAsExpectedByDefault + */ + public function testNavigationMigrationCreatesMultipleBackups() + { + $initialOldConfig = [ + 'hosts' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ] + ]; + $initialNewConfig = [ + 'hosts' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ], + 'group_members' => [ + 'title' => 'Group Members', + 'url' => 'monitoring/list/hosts?hostgroup_name=group1|hostgroup_name=group2' + ] + ]; + $expectedNewConfig = [ + 'hosts' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ] + ]; + $expectedFinalConfig = [ + 'hosts' => [ + 'title' => 'Host Problems', + 'url' => 'icingadb/hosts?host.state.is_problem=y' + ], + 'group_members' => [ + 'title' => 'Group Members', + 'url' => 'icingadb/hosts?hostgroup.name=group1|hostgroup.name=group2' + ] + ]; + + $this->createConfig('preferences/test/menu.ini', $initialOldConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $newConfig = $this->loadConfig('preferences/test/menu.ini'); + $this->assertSame($expectedNewConfig, $newConfig); + $oldBackup = $this->loadConfig('preferences/test/menu.backup.ini'); + $this->assertSame($initialOldConfig, $oldBackup); + + $this->createConfig('preferences/test/menu.ini', $initialNewConfig); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + + $finalConfig = $this->loadConfig('preferences/test/menu.ini'); + $this->assertSame($expectedFinalConfig, $finalConfig); + $newBackup = $this->loadConfig('preferences/test/menu.backup1.ini'); + $this->assertSame($initialNewConfig, $newBackup); + } + + /** + * Checks the following: + * - Whether backups are skipped + * + * @depends testNavigationMigrationBehavesAsExpectedByDefault + */ + public function testNavigationMigrationSkipsBackupIfRequested() + { + [$initialConfig, $expected] = $this->getConfig('menu-items'); + + $this->createConfig('preferences/test/menu.ini', $initialConfig); + + $command = $this->createCommandInstance('--user', 'test', '--no-backup'); + $command->navigationAction(); + + $config = $this->loadConfig('preferences/test/menu.ini'); + $this->assertSame($expected, $config); + + $backup = $this->loadConfig('preferences/test/menu.backup.ini'); + $this->assertEmpty($backup); + } + /** * Checks the following: * - Whether existing Icinga DB Actions are transformed regarding wildcard filters @@ -826,7 +993,7 @@ public function testNavigationMigrationDeletesOldConfigsIfRequested() $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); - $command = $this->createCommandInstance('--user', 'test', '--delete'); + $command = $this->createCommandInstance('--user', 'test', '--no-backup'); $command->navigationAction(); $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); From 98c43732f7f2281c1de5d4a4c6390a445c5ce6be Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 10 Nov 2023 15:32:00 +0100 Subject: [PATCH 12/17] migrate: Don't crash just because a directory does not exist --- application/clicommands/MigrateCommand.php | 2 +- .../clicommands/MigrateCommandTest.php | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 5e55d8621..f3a35ab4e 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -61,7 +61,7 @@ public function navigationAction(): void } $rc = 0; - $directories = new DirectoryIterator($preferencesPath); + $directories = file_exists($preferencesPath) ? new DirectoryIterator($preferencesPath) : []; /** @var string $directory */ foreach ($directories as $directory) { diff --git a/test/php/application/clicommands/MigrateCommandTest.php b/test/php/application/clicommands/MigrateCommandTest.php index e14d1b427..199486f37 100644 --- a/test/php/application/clicommands/MigrateCommandTest.php +++ b/test/php/application/clicommands/MigrateCommandTest.php @@ -1429,4 +1429,32 @@ public function testFilterMigrationWorksAsExpected() $this->assertSame($expectedDashboardConfig, $dashboardConfig); $this->assertSame($expectedRoleConfig, $roleConfig); } + + public function testNavigationMigrationWorksEvenIfOnlySharedItemsExist() + { + $this->expectNotToPerformAssertions(); + + $this->createConfig('navigation/menu.ini', []); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + } + + public function testNavigationMigrationWorksEvenIfOnlyUserItemsExist() + { + $this->expectNotToPerformAssertions(); + + $this->createConfig('preferences/test/menu.ini', []); + + $command = $this->createCommandInstance('--user', 'test'); + $command->navigationAction(); + } + + public function testDashboardMigrationWorksEvenIfNoDashboardsExist() + { + $this->expectNotToPerformAssertions(); + + $command = $this->createCommandInstance('--user', 'test'); + $command->dashboardAction(); + } } From 687f3ce28178ec9bd65ae41f5fc62508b3b6d1fe Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 10 Nov 2023 15:46:19 +0100 Subject: [PATCH 13/17] migrate: Simplify error logging --- application/clicommands/MigrateCommand.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index f3a35ab4e..e084a954c 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -397,7 +397,7 @@ public function roleAction(): void try { $rolesConfig->saveIni(); } catch (NotWritableError $error) { - Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + Logger::error($error); if ($this->skipMigration) { Logger::error('Failed to transform icingadb restrictions'); } else { @@ -508,7 +508,7 @@ public function dashboardAction(): void try { $dashboardsConfig->saveIni(); } catch (NotWritableError $error) { - Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + Logger::error($error); $rc = 256; } } @@ -616,7 +616,7 @@ private function transformNavigationItems(Config $config, string $owner, int &$r try { $config->saveIni(); } catch (NotWritableError $error) { - Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + Logger::error($error); $rc = 256; return false; @@ -714,7 +714,7 @@ private function migrateNavigationItems(Config $config, string $owner, string $p unlink($config->getConfigFile()); } } catch (NotWritableError $error) { - Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); + Logger::error($error); $rc = 256; return false; @@ -737,11 +737,7 @@ private function readFromIni($path, &$rc) try { $config = Config::fromIni($path); } catch (NotReadableError $error) { - if ($error->getPrevious() !== null) { - Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage()); - } else { - Logger::error($error->getMessage()); - } + Logger::error($error); $config = new Config(); $rc = 128; From 89fb9de4fdca1bbde07edf8c30cbdbc6b1fbff97 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 10 Nov 2023 16:22:21 +0100 Subject: [PATCH 14/17] migrate: Enhance logging --- application/clicommands/MigrateCommand.php | 40 +++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index e084a954c..5d2280044 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -63,6 +63,8 @@ public function navigationAction(): void $rc = 0; $directories = file_exists($preferencesPath) ? new DirectoryIterator($preferencesPath) : []; + $anythingChanged = false; + /** @var string $directory */ foreach ($directories as $directory) { /** @var string $username */ @@ -87,14 +89,15 @@ public function navigationAction(): void if (! $menuItems->isEmpty()) { $menuUpdated = $this->transformNavigationItems($menuItems, $username, $rc); + $anythingChanged |= $menuUpdated; } if (! $icingadbHostActions->isEmpty()) { - $this->transformNavigationItems($icingadbHostActions, $username, $rc); + $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $username, $rc); } if (! $icingadbServiceActions->isEmpty()) { - $this->transformNavigationItems( + $anythingChanged |= $this->transformNavigationItems( $icingadbServiceActions, $username, $rc @@ -106,10 +109,11 @@ public function navigationAction(): void if (! $menuItems->isEmpty()) { $menuUpdated = $this->migrateNavigationItems($menuItems, $username, $directory . '/menu.ini', $rc); + $anythingChanged |= $menuUpdated; } if (! $hostActions->isEmpty()) { - $this->migrateNavigationItems( + $anythingChanged |= $this->migrateNavigationItems( $hostActions, $username, $directory . '/icingadb-host-actions.ini', @@ -118,7 +122,7 @@ public function navigationAction(): void } if (! $serviceActions->isEmpty()) { - $this->migrateNavigationItems( + $anythingChanged |= $this->migrateNavigationItems( $serviceActions, $username, $directory . '/icingadb-service-actions.ini', @@ -142,18 +146,19 @@ public function navigationAction(): void $menuUpdated = false; $originalMenuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); - Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web'); + Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web navigation items'); if (! $menuItems->isEmpty()) { $menuUpdated = $this->transformNavigationItems($menuItems, $user, $rc); + $anythingChanged |= $menuUpdated; } if (! $icingadbHostActions->isEmpty()) { - $this->transformNavigationItems($icingadbHostActions, $user, $rc); + $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $user, $rc); } if (! $icingadbServiceActions->isEmpty()) { - $this->transformNavigationItems( + $anythingChanged |= $this->transformNavigationItems( $icingadbServiceActions, $user, $rc @@ -161,14 +166,15 @@ public function navigationAction(): void } if (! $this->skipMigration) { - Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions'); + Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web items'); if (! $menuItems->isEmpty()) { $menuUpdated = $this->migrateNavigationItems($menuItems, $user, $sharedNavigation . '/menu.ini', $rc); + $anythingChanged |= $menuUpdated; } if (! $hostActions->isEmpty()) { - $this->migrateNavigationItems( + $anythingChanged |= $this->migrateNavigationItems( $hostActions, $user, $sharedNavigation . '/icingadb-host-actions.ini', @@ -177,7 +183,7 @@ public function navigationAction(): void } if (! $serviceActions->isEmpty()) { - $this->migrateNavigationItems( + $anythingChanged |= $this->migrateNavigationItems( $serviceActions, $user, $sharedNavigation . '/icingadb-service-actions.ini', @@ -200,7 +206,9 @@ public function navigationAction(): void exit($rc); } - if ($this->skipMigration) { + if (! $anythingChanged) { + Logger::info('Nothing to do'); + } elseif ($this->skipMigration) { Logger::info('Successfully transformed all icingadb navigation item filters'); } else { Logger::info('Successfully migrated all monitoring navigation items'); @@ -446,6 +454,8 @@ public function dashboardAction(): void $rc = 0; $directories = new DirectoryIterator($dashboardsPath); + $anythingChanged = false; + /** @var string $directory */ foreach ($directories as $directory) { /** @var string $userName */ @@ -505,6 +515,10 @@ public function dashboardAction(): void $this->createBackupIni("$directory/dashboard", $backupConfig); } + if ($changed) { + $anythingChanged = true; + } + try { $dashboardsConfig->saveIni(); } catch (NotWritableError $error) { @@ -523,7 +537,9 @@ public function dashboardAction(): void exit($rc); } - if ($this->skipMigration) { + if (! $anythingChanged) { + Logger::info('Nothing to do'); + } elseif ($this->skipMigration) { Logger::info('Successfully transformed all icingadb dashboards'); } else { Logger::info('Successfully migrated dashboards for all the matched users'); From f1af2c27656a1f3f3bab8da75938e9ef96a2dda3 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 13 Nov 2023 14:52:42 +0100 Subject: [PATCH 15/17] migrate: Improve documentation --- application/clicommands/MigrateCommand.php | 52 +++++++++++----------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 5d2280044..51470747f 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -28,7 +28,7 @@ public function init(): void } /** - * Migrate monitoring navigation items to the Icinga DB Web actions + * Migrate monitoring navigation items to Icinga DB Web * * USAGE * @@ -36,16 +36,16 @@ public function init(): void * * REQUIRED OPTIONS: * - * --user= Migrate monitoring navigation items only for - * the given user or all similar users if a - * wildcard is used. (* matches all users) + * --user= Migrate navigation items whose owner matches the given + * name or owners matching the given pattern. Wildcard + * matching by `*` possible. * * OPTIONS: * - * --override Override the existing Icinga DB navigation items + * --override Replace existing or already migrated items + * (Attention: Actions are not backed up) * - * --no-backup Remove the legacy files after successfully - * migrated the navigation items. + * --no-backup Remove monitoring actions and don't back up menu items */ public function navigationAction(): void { @@ -217,26 +217,28 @@ public function navigationAction(): void /** - * Migrate monitoring restrictions and permissions in a role to Icinga DB Web restrictions and permissions + * Migrate monitoring restrictions and permissions to Icinga DB Web + * + * Migrated roles do not grant general or full access to users afterward. + * It is recommended to review any changes made by this command, before + * manually granting access. * * USAGE * * icingacli icingadb migrate role [options] * - * OPTIONS: + * REQUIRED OPTIONS: (Use either, not both) * - * --group= Migrate monitoring restrictions and permissions for all roles, - * the given group or the groups matching the given - * group belongs to. - * (wildcard * migrates monitoring restrictions and permissions - * for all roles) + * --group= Update roles that are assigned to the given group or to + * groups matching the pattern. Wildcard matching by `*` + * possible. * - * --role= Migrate monitoring restrictions and permissions for the - * given role or all the matching roles. - * (wildcard * migrates monitoring restrictions and permissions - * for all roles) + * --role= Update role with the given name or roles whose names + * match the pattern. Wildcard matching by `*` possible. + * + * OPTIONS: * - * --override Override the existing Icinga DB restrictions and permissions + * --override Reset any existing Icinga DB Web rules */ public function roleAction(): void { @@ -423,21 +425,21 @@ public function roleAction(): void } /** - * Migrate the monitoring dashboards to Icinga DB Web dashboards for all the matched users + * Migrate monitoring dashboards to Icinga DB Web * * USAGE * - * icingacli icingadb migrate dasboard [options] + * icingacli icingadb migrate dashboard [options] * * REQUIRED OPTIONS: * - * --user= Migrate monitoring dashboards for all the - * users that are matched. (* all users) + * --user= Migrate dashboards whose owner matches the given + * name or owners matching the given pattern. Wildcard + * matching by `*` possible. * * OPTIONS: * - * --no-backup Migrate without creating a backup. (By Default - * a backup for monitoring dashboards is created) + * --no-backup Don't back up dashboards */ public function dashboardAction(): void { From 4c7cbfa67db168c7fdf63fff94f8ae28208fb480 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 13 Nov 2023 14:53:19 +0100 Subject: [PATCH 16/17] migrate: Also backup roles Logging is useful, but a backup even more. --- application/clicommands/MigrateCommand.php | 47 ++++++++++++++----- .../clicommands/MigrateCommandTest.php | 41 ++++++++++++++++ 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 51470747f..9881b4b09 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -239,11 +239,14 @@ public function navigationAction(): void * OPTIONS: * * --override Reset any existing Icinga DB Web rules + * + * --no-backup Don't back up roles */ public function roleAction(): void { /** @var ?bool $override */ $override = $this->params->get('override'); + $noBackup = $this->params->get('no-backup'); /** @var ?string $groupName */ $groupName = $this->params->get('group'); @@ -257,6 +260,8 @@ public function roleAction(): void } $rc = 0; + $changed = false; + $restrictions = Config::$configDir . '/roles.ini'; $rolesConfig = $this->readFromIni($restrictions, $rc); $monitoringRestriction = 'monitoring/filter/objects'; @@ -301,6 +306,7 @@ public function roleAction(): void if ($transformedFilter) { $role[$icingadbRestrictions['objects']] = QueryString::render($transformedFilter); + $changed = true; } } @@ -320,6 +326,8 @@ public function roleAction(): void '*', implode(',', array_unique($icingadbProperties)) ); + + $changed = true; } if (isset($role['permissions'])) { @@ -335,6 +343,7 @@ public function roleAction(): void if ($monitoringProtection !== null) { $role['icingadb/protect/variables'] = $monitoringProtection; + $changed = true; } } @@ -342,9 +351,11 @@ public function roleAction(): void if (Str::startsWith($permission, 'icingadb/') || $permission === 'module/icingadb') { continue; } elseif (Str::startsWith($permission, 'monitoring/command/')) { + $changed = true; $updatedPermissions[] = $permission; $updatedPermissions[] = str_replace('monitoring/', 'icingadb/', $permission); } elseif ($permission === 'no-monitoring/contacts') { + $changed = true; $updatedPermissions[] = $permission; $role['icingadb/denylist/routes'] = 'users,usergroups'; } else { @@ -366,6 +377,7 @@ public function roleAction(): void if (Str::startsWith($refusal, 'icingadb/') || $refusal === 'module/icingadb') { continue; } elseif (Str::startsWith($refusal, 'monitoring/command/')) { + $changed = true; $updatedRefusals[] = $refusal; $updatedRefusals[] = str_replace('monitoring/', 'icingadb/', $refusal); } else { @@ -395,6 +407,7 @@ public function roleAction(): void ); $role[$icingadbRestriction] = $filter; + $changed = true; } } } @@ -404,23 +417,31 @@ public function roleAction(): void $rolesConfig->setSection($name, $role); } - try { - $rolesConfig->saveIni(); - } catch (NotWritableError $error) { - Logger::error($error); - if ($this->skipMigration) { - Logger::error('Failed to transform icingadb restrictions'); - } else { - Logger::error('Failed to migrate monitoring restrictions'); + if ($changed) { + if (! $noBackup) { + $this->createBackupIni(Config::$configDir . '/roles'); } - exit(256); - } + try { + $rolesConfig->saveIni(); + } catch (NotWritableError $error) { + Logger::error($error); + if ($this->skipMigration) { + Logger::error('Failed to transform icingadb restrictions'); + } else { + Logger::error('Failed to migrate monitoring restrictions'); + } - if ($this->skipMigration) { - Logger::info('Successfully transformed all icingadb restrictions'); + exit(256); + } + + if ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb restrictions'); + } else { + Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); + } } else { - Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); + Logger::info('Nothing to do'); } } diff --git a/test/php/application/clicommands/MigrateCommandTest.php b/test/php/application/clicommands/MigrateCommandTest.php index 199486f37..013d92a24 100644 --- a/test/php/application/clicommands/MigrateCommandTest.php +++ b/test/php/application/clicommands/MigrateCommandTest.php @@ -1135,6 +1135,10 @@ public function testRoleMigrationHandlesARoleWithMatchingGroups() * - Whether refusals are properly migrated * - Whether restrictions are properly migrated * - Whether blacklists are properly migrated + * - Whether backups are created + * - Whether a second run changes nothing, if nothing changed + * - Whether a second run keeps the backup, if nothing changed + * - Whether a new backup isn't created, if nothing changed */ public function testRoleMigrationMigratesAllRoles() { @@ -1147,6 +1151,43 @@ public function testRoleMigrationMigratesAllRoles() $config = $this->loadConfig('roles.ini'); $this->assertSame($expected, $config); + + $backup = $this->loadConfig('roles.backup.ini'); + $this->assertSame($initialConfig, $backup); + + $command = $this->createCommandInstance('--role', '*'); + $command->roleAction(); + + $configAfterSecondRun = $this->loadConfig('roles.ini'); + $this->assertSame($config, $configAfterSecondRun); + + $backupAfterSecondRun = $this->loadConfig('roles.backup.ini'); + $this->assertSame($backup, $backupAfterSecondRun); + + $backup2 = $this->loadConfig('roles.backup1.ini'); + $this->assertEmpty($backup2); + } + + /** + * Checks the following: + * - Whether backups are skipped + * + * @depends testRoleMigrationMigratesAllRoles + */ + public function testRoleMigrationSkipsBackupIfRequested() + { + [$initialConfig, $expected] = $this->getConfig('all-roles'); + + $this->createConfig('roles.ini', $initialConfig); + + $command = $this->createCommandInstance('--role', '*', '--no-backup'); + $command->roleAction(); + + $config = $this->loadConfig('roles.ini'); + $this->assertSame($expected, $config); + + $backup = $this->loadConfig('roles.backup.ini'); + $this->assertEmpty($backup); } /** From ce750587a05ee6d82c618282e773e0e48cbe2a4a Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Mon, 13 Nov 2023 14:57:29 +0100 Subject: [PATCH 17/17] migrate: Document the `--no-backup` switch for the `filter` subcommand --- application/clicommands/MigrateCommand.php | 4 + .../clicommands/MigrateCommandTest.php | 226 ++++++++++++++++++ 2 files changed, 230 insertions(+) diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 9881b4b09..6d034ee39 100644 --- a/application/clicommands/MigrateCommand.php +++ b/application/clicommands/MigrateCommand.php @@ -575,6 +575,10 @@ public function dashboardAction(): void * USAGE * * icingacli icingadb migrate filter + * + * OPTIONS: + * + * --no-backup Don't back up menu items, dashboards and roles */ public function filterAction(): void { diff --git a/test/php/application/clicommands/MigrateCommandTest.php b/test/php/application/clicommands/MigrateCommandTest.php index 013d92a24..2f591acac 100644 --- a/test/php/application/clicommands/MigrateCommandTest.php +++ b/test/php/application/clicommands/MigrateCommandTest.php @@ -1388,6 +1388,41 @@ public function testFilterMigrationWorksAsExpected() ] ]; + $initialMenuConfig = [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' + ] + ]; + $expectedMenuConfig = [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' + ] + ]; + $initialDashboardConfig = [ 'hosts' => [ 'title' => 'Hosts' @@ -1451,6 +1486,7 @@ public function testFilterMigrationWorksAsExpected() $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialIcingadbHostActionConfig); $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialIcingadbServiceActionConfig); $this->createConfig('dashboards/test/dashboard.ini', $initialDashboardConfig); + $this->createConfig('preferences/test/menu.ini', $initialMenuConfig); $this->createConfig('roles.ini', $initialRoleConfig); $command = $this->createCommandInstance(); @@ -1460,14 +1496,204 @@ public function testFilterMigrationWorksAsExpected() $serviceActionConfig = $this->loadConfig('preferences/test/service-actions.ini'); $icingadbHostActionConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $icingadbServiceActionConfig = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $dashboardBackup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); + $dashboardConfig = $this->loadConfig('dashboards/test/dashboard.ini'); + $menuBackup = $this->loadConfig('preferences/test/menu.backup.ini'); + $menuConfig = $this->loadConfig('preferences/test/menu.ini'); + $roleBackup = $this->loadConfig('roles.backup.ini'); + $roleConfig = $this->loadConfig('roles.ini'); + + $this->assertSame($expectedHostActionConfig, $hostActionConfig); + $this->assertSame($expectedServiceActionConfig, $serviceActionConfig); + $this->assertSame($initialDashboardConfig, $dashboardBackup); + $this->assertSame($initialMenuConfig, $menuBackup); + $this->assertSame($initialRoleConfig, $roleBackup); + + $this->assertSame($expectedIcingadbHostActionConfig, $icingadbHostActionConfig); + $this->assertSame($expectedIcingadbServiceActionConfig, $icingadbServiceActionConfig); + $this->assertSame($expectedDashboardConfig, $dashboardConfig); + $this->assertSame($expectedMenuConfig, $menuConfig); + $this->assertSame($expectedRoleConfig, $roleConfig); + } + + /** + * @depends testFilterMigrationWorksAsExpected + */ + public function testFilterMigrationSkipsBackupsIfRequested() + { + $initialHostActionConfig = [ + 'hosts' => [ + 'type' => 'host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host_name=%2Afoo%2A' + ] + ]; + $expectedHostActionConfig = $initialHostActionConfig; + + $initialIcingadbHostActionConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name=%2Afoo%2A' + ] + ]; + $expectedIcingadbHostActionConfig = [ + 'hosts' => [ + 'type' => 'icingadb-host-action', + 'url' => 'example.com/search?q=$host.name$', + 'filter' => 'host.name~%2Afoo%2A' + ] + ]; + + $initialServiceActionConfig = [ + 'services' => [ + 'type' => 'service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' + ] + ]; + $expectedServiceActionConfig = $initialServiceActionConfig; + + $initialIcingadbServiceActionConfig = [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=bar&service.vars.bar=%2Afoo%2A' + ] + ]; + $expectedIcingadbServiceActionConfig = [ + 'services' => [ + 'type' => 'icingadb-service-action', + 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', + 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' + ] + ]; + + $initialMenuConfig = [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' + ] + ]; + $expectedMenuConfig = [ + 'foreign-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'example.com?q=foo' + ], + 'monitoring-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb-url' => [ + 'type' => 'menu-item', + 'target' => '_blank', + 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' + ] + ]; + + $initialDashboardConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb' => [ + 'title' => 'Icinga DB' + ], + 'icingadb.wildcards' => [ + 'title' => 'Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=%2Alinux%2A' + ] + ]; + $expectedDashboardConfig = [ + 'hosts' => [ + 'title' => 'Hosts' + ], + 'hosts.problems' => [ + 'title' => 'Host Problems', + 'url' => 'monitoring/list/hosts?host_problem=1' + ], + 'icingadb' => [ + 'title' => 'Icinga DB' + ], + 'icingadb.wildcards' => [ + 'title' => 'Wildcards', + 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name~%2Alinux%2A' + ] + ]; + + $initialRoleConfig = [ + 'one' => [ + 'groups' => 'support,helpdesk', + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'two' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'three' => [ + 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' + ] + ]; + $expectedRoleConfig = [ + 'one' => [ + 'groups' => 'support,helpdesk', + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'two' => [ + 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' + ], + 'three' => [ + 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' + ] + ]; + + $this->createConfig('preferences/test/host-actions.ini', $initialHostActionConfig); + $this->createConfig('preferences/test/service-actions.ini', $initialServiceActionConfig); + $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialIcingadbHostActionConfig); + $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialIcingadbServiceActionConfig); + $this->createConfig('dashboards/test/dashboard.ini', $initialDashboardConfig); + $this->createConfig('preferences/test/menu.ini', $initialMenuConfig); + $this->createConfig('roles.ini', $initialRoleConfig); + + $command = $this->createCommandInstance('--no-backup'); + $command->filterAction(); + + $hostActionConfig = $this->loadConfig('preferences/test/host-actions.ini'); + $serviceActionConfig = $this->loadConfig('preferences/test/service-actions.ini'); + $icingadbHostActionConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); + $icingadbServiceActionConfig = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); + $dashboardBackup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $dashboardConfig = $this->loadConfig('dashboards/test/dashboard.ini'); + $menuBackup = $this->loadConfig('preferences/test/menu.backup.ini'); + $menuConfig = $this->loadConfig('preferences/test/menu.ini'); + $roleBackup = $this->loadConfig('roles.backup.ini'); $roleConfig = $this->loadConfig('roles.ini'); $this->assertSame($expectedHostActionConfig, $hostActionConfig); $this->assertSame($expectedServiceActionConfig, $serviceActionConfig); + $this->assertEmpty($dashboardBackup); + $this->assertEmpty($menuBackup); + $this->assertEmpty($roleBackup); + $this->assertSame($expectedIcingadbHostActionConfig, $icingadbHostActionConfig); $this->assertSame($expectedIcingadbServiceActionConfig, $icingadbServiceActionConfig); $this->assertSame($expectedDashboardConfig, $dashboardConfig); + $this->assertSame($expectedMenuConfig, $menuConfig); $this->assertSame($expectedRoleConfig, $roleConfig); }