diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php index 776f41a2f..75e533584 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'); @@ -69,67 +70,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 +162,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'); @@ -466,19 +499,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 +546,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, string $owner, &$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 +570,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 +581,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'; + $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 + * @param bool $updated + * + * @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 +747,28 @@ private function readFromIni($path, &$rc) return $config; } + private function createBackupIni(string $path, Config $config = null) + { + $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 d08db04c9..1c1d4f038 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' ] @@ -707,26 +761,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); @@ -748,12 +822,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 @@ -816,7 +983,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');