From f66ab8dae2f3d0418759f81a3a64dc9517a62449 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 15:17:31 +0200 Subject: Adding upstream version 1.10.2. Signed-off-by: Daniel Baumann --- application/clicommands/BasketCommand.php | 127 +++++++++++++++ application/clicommands/BenchmarkCommand.php | 152 ++++++++++++++++++ application/clicommands/CommandCommand.php | 15 ++ application/clicommands/CommandsCommand.php | 14 ++ application/clicommands/ConfigCommand.php | 178 +++++++++++++++++++++ application/clicommands/CoreCommand.php | 16 ++ application/clicommands/DaemonCommand.php | 26 ++++ application/clicommands/DependencyCommand.php | 15 ++ application/clicommands/EndpointCommand.php | 19 +++ application/clicommands/ExportCommand.php | 180 ++++++++++++++++++++++ application/clicommands/HealthCommand.php | 80 ++++++++++ application/clicommands/HostCommand.php | 15 ++ application/clicommands/HostgroupCommand.php | 15 ++ application/clicommands/HostgroupsCommand.php | 14 ++ application/clicommands/HostsCommand.php | 14 ++ application/clicommands/HousekeepingCommand.php | 74 +++++++++ application/clicommands/ImportCommand.php | 62 ++++++++ application/clicommands/ImportsourceCommand.php | 168 ++++++++++++++++++++ application/clicommands/JobsCommand.php | 74 +++++++++ application/clicommands/KickstartCommand.php | 88 +++++++++++ application/clicommands/MigrationCommand.php | 66 ++++++++ application/clicommands/NotificationCommand.php | 15 ++ application/clicommands/ServiceCommand.php | 92 +++++++++++ application/clicommands/ServicegroupCommand.php | 15 ++ application/clicommands/ServicesetCommand.php | 14 ++ application/clicommands/ServicesetsCommand.php | 15 ++ application/clicommands/SyncruleCommand.php | 195 ++++++++++++++++++++++++ application/clicommands/TimeperiodCommand.php | 15 ++ application/clicommands/UserCommand.php | 15 ++ application/clicommands/UsergroupCommand.php | 15 ++ application/clicommands/ZoneCommand.php | 15 ++ 31 files changed, 1818 insertions(+) create mode 100644 application/clicommands/BasketCommand.php create mode 100644 application/clicommands/BenchmarkCommand.php create mode 100644 application/clicommands/CommandCommand.php create mode 100644 application/clicommands/CommandsCommand.php create mode 100644 application/clicommands/ConfigCommand.php create mode 100644 application/clicommands/CoreCommand.php create mode 100644 application/clicommands/DaemonCommand.php create mode 100644 application/clicommands/DependencyCommand.php create mode 100644 application/clicommands/EndpointCommand.php create mode 100644 application/clicommands/ExportCommand.php create mode 100644 application/clicommands/HealthCommand.php create mode 100644 application/clicommands/HostCommand.php create mode 100644 application/clicommands/HostgroupCommand.php create mode 100644 application/clicommands/HostgroupsCommand.php create mode 100644 application/clicommands/HostsCommand.php create mode 100644 application/clicommands/HousekeepingCommand.php create mode 100644 application/clicommands/ImportCommand.php create mode 100644 application/clicommands/ImportsourceCommand.php create mode 100644 application/clicommands/JobsCommand.php create mode 100644 application/clicommands/KickstartCommand.php create mode 100644 application/clicommands/MigrationCommand.php create mode 100644 application/clicommands/NotificationCommand.php create mode 100644 application/clicommands/ServiceCommand.php create mode 100644 application/clicommands/ServicegroupCommand.php create mode 100644 application/clicommands/ServicesetCommand.php create mode 100644 application/clicommands/ServicesetsCommand.php create mode 100644 application/clicommands/SyncruleCommand.php create mode 100644 application/clicommands/TimeperiodCommand.php create mode 100644 application/clicommands/UserCommand.php create mode 100644 application/clicommands/UsergroupCommand.php create mode 100644 application/clicommands/ZoneCommand.php (limited to 'application/clicommands') diff --git a/application/clicommands/BasketCommand.php b/application/clicommands/BasketCommand.php new file mode 100644 index 0000000..dd2434f --- /dev/null +++ b/application/clicommands/BasketCommand.php @@ -0,0 +1,127 @@ +db()->getDbAdapter(); + $query = $db->select() + ->from('director_basket', 'basket_name') + ->order('basket_name'); + foreach ($db->fetchCol($query) as $name) { + echo "$name\n"; + } + } + + /** + * JSON-dump for objects related to the given Basket + * + * USAGE + * + * icingacli director basket dump --name + * + * OPTIONS + */ + public function dumpAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::createForBasket($basket, $this->db()); + echo $snapshot->getJsonDump() . "\n"; + } + + /** + * Take a snapshot for the given Basket + * + * USAGE + * + * icingacli director basket snapshot --name + * + * OPTIONS + */ + public function snapshotAction() + { + $basket = $this->requireBasket(); + $snapshot = BasketSnapshot::createForBasket($basket, $this->db()); + $snapshot->store(); + $hexSum = bin2hex($snapshot->get('content_checksum')); + printf( + "Snapshot '%s' taken for Basket '%s' at %s\n", + substr($hexSum, 0, 7), + $basket->get('basket_name'), + DateFormatter::formatDateTime($snapshot->get('ts_create') / 1000) + ); + } + + /** + * Restore a Basket from JSON dump provided on STDIN + * + * USAGE + * + * icingacli director basket restore < basket-dump.json + * + * OPTIONS + * --purge [,params->get('purge')) { + $purge = explode(',', $purge); + ObjectPurgeHelper::assertObjectTypesAreEligibleForPurge($purge); + } + $json = file_get_contents('php://stdin'); + BasketSnapshot::restoreJson($json, $this->db()); + if ($purge) { + $this->purgeObjectTypes(Json::decode($json), $purge, $this->params->get('force')); + } + echo "Objects from Basket Snapshot have been restored\n"; + } + + protected function purgeObjectTypes($objects, array $types, $force = false) + { + $helper = new ObjectPurgeHelper($this->db()); + if ($force) { + $helper->force(); + } + foreach ($types as $type) { + list($className, $typeFilter) = BasketSnapshot::getClassAndObjectTypeForType($type); + $helper->purge( + isset($objects->$type) ? (array) $objects->$type : [], + $className, + $typeFilter + ); + } + } + + /** + */ + protected function requireBasket() + { + return Basket::load($this->params->getRequired('name'), $this->db()); + } +} diff --git a/application/clicommands/BenchmarkCommand.php b/application/clicommands/BenchmarkCommand.php new file mode 100644 index 0000000..6ccd8c8 --- /dev/null +++ b/application/clicommands/BenchmarkCommand.php @@ -0,0 +1,152 @@ +db()); + $filter = Filter::fromQueryString( + // 'host.vars.snmp_community="*ub*"&(host.vars.location="London"|host.vars.location="Berlin")' + // 'host.vars.snmp_community="*ub*"&(host.vars.location="FRA DC"|host.vars.location="NBG DC")' + 'host.vars.priority="*igh"&(host.vars.location="FRA DC"|host.vars.location="NBG DC")' + ); + IcingaObjectFilterRenderer::apply($filter, $q); + echo $q->getSql() . "\n"; + + print_r($q->listNames()); + } + + public function rerendervarsAction() + { + $conn = $this->db(); + $db = $conn->getDbAdapter(); + $db->beginTransaction(); + $query = $db->select()->from( + array('v' => 'icinga_var'), + array( + 'v.varname', + 'v.varvalue', + 'v.checksum', + 'v.rendered_checksum', + 'v.rendered', + 'format' => "('json')", + ) + ); + Benchmark::measure('Ready to fetch all vars'); + $rows = $db->fetchAll($query); + Benchmark::measure('Got vars, storing flat'); + foreach ($rows as $row) { + $var = CustomVariable::fromDbRow($row); + $rendered = $var->render(); + $checksum = sha1($rendered, true); + if ($checksum === $row->rendered_checksum) { + continue; + } + + $where = $db->quoteInto('checksum = ?', $row->checksum); + $db->update( + 'icinga_var', + array( + 'rendered' => $rendered, + 'rendered_checksum' => $checksum + ), + $where + ); + } + + $db->commit(); + } + + public function flattenvarsAction() + { + $conn = $this->db(); + $db = $conn->getDbAdapter(); + $db->beginTransaction(); + $query = $db->select()->from(['v' => 'icinga_host_var'], [ + 'v.host_id', + 'v.varname', + 'v.varvalue', + 'v.format', + 'v.checksum' + ]); + Benchmark::measure('Ready to fetch all vars'); + $rows = $db->fetchAll($query); + Benchmark::measure('Got vars, storing flat'); + + foreach ($rows as $row) { + $var = CustomVariable::fromDbRow($row); + $checksum = $var->checksum(); + if (! IcingaVar::exists($checksum, $conn)) { + IcingaVar::generateForCustomVar($var, $conn); + } + + if ($row->checksum === null) { + $where = $db->quoteInto('host_id = ?', $row->host_id) + . $db->quoteInto(' AND varname = ?', $row->varname); + $db->update('icinga_host_var', ['checksum' => $checksum], $where); + } + } + + $db->commit(); + } + + public function resolvehostgroupsAction() + { + $resolver = new HostGroupMembershipResolver($this->db()); + $resolver->refreshDb(); + } + + public function filterAction() + { + $flat = []; + + /** @var FilterChain|FilterExpression $filter */ + $filter = Filter::fromQueryString( + // 'object_name=*ic*2*&object_type=object' + 'vars.bpconfig=*' + ); + Benchmark::measure('ready'); + $objs = IcingaHost::loadAll($this->db()); + Benchmark::measure('db done'); + + foreach ($objs as $host) { + $flat[$host->get('id')] = (object) []; + foreach ($host->getProperties() as $k => $v) { + $flat[$host->get('id')]->$k = $v; + } + } + Benchmark::measure('objects ready'); + + $vars = IcingaHostVar::loadAll($this->db()); + Benchmark::measure('vars loaded'); + foreach ($vars as $var) { + if (! array_key_exists($var->get('host_id'), $flat)) { + // Templates? + continue; + } + $flat[$var->get('host_id')]->{'vars.' . $var->get('varname')} = $var->get('varvalue'); + } + Benchmark::measure('vars done'); + + foreach ($flat as $host) { + if ($filter->matches($host)) { + echo $host->object_name . "\n"; + } + } + } +} diff --git a/application/clicommands/CommandCommand.php b/application/clicommands/CommandCommand.php new file mode 100644 index 0000000..5c96442 --- /dev/null +++ b/application/clicommands/CommandCommand.php @@ -0,0 +1,15 @@ +params->shift('profile'); + if ($profile) { + $this->enableDbProfiler(); + } + + $config = new IcingaConfig($this->db()); + Benchmark::measure('Rendering config'); + if ($config->hasBeenModified()) { + Benchmark::measure('Config rendered, storing to db'); + $config->store(); + Benchmark::measure('All done'); + $checksum = $config->getHexChecksum(); + printf( + "New config with checksum %s has been generated\n", + $checksum + ); + } else { + $checksum = $config->getHexChecksum(); + printf( + "Config with checksum %s already exists\n", + $checksum + ); + } + + if ($profile) { + $this->dumpDbProfile(); + } + } + + protected function dumpDbProfile() + { + $profiler = $this->getDbProfiler(); + + $totalTime = $profiler->getTotalElapsedSecs(); + $queryCount = $profiler->getTotalNumQueries(); + $longestTime = 0; + $longestQuery = null; + + /** @var \Zend_Db_Profiler_Query $query */ + foreach ($profiler->getQueryProfiles() as $query) { + echo $query->getQuery() . "\n"; + if ($query->getElapsedSecs() > $longestTime) { + $longestTime = $query->getElapsedSecs(); + $longestQuery = $query->getQuery(); + } + } + + echo 'Executed ' . $queryCount . ' queries in ' . $totalTime . ' seconds' . "\n"; + echo 'Average query length: ' . $totalTime / $queryCount . ' seconds' . "\n"; + echo 'Queries per second: ' . $queryCount / $totalTime . "\n"; + echo 'Longest query length: ' . $longestTime . "\n"; + echo "Longest query: \n" . $longestQuery . "\n"; + } + + protected function getDbProfiler() + { + return $this->db()->getDbAdapter()->getProfiler(); + } + + protected function enableDbProfiler() + { + return $this->getDbProfiler()->setEnabled(true); + } + + /** + * Deploy the current configuration + * + * USAGE + * + * icingacli director config deploy [--checksum ] [--force] [--wait ] + * [--grace-period ] + * + * OPTIONS + * + * --checksum Optionally deploy a specific configuration + * --force Force a deployment, even when the configuration + * hasn't changed + * --wait Optionally wait until Icinga completed it's + * restart + * --grace-period Do not deploy if a deployment took place + * less than ago + */ + public function deployAction() + { + $db = $this->db(); + + $checksum = $this->params->get('checksum'); + if ($checksum) { + $config = IcingaConfig::load(hex2bin($checksum), $db); + } else { + $config = IcingaConfig::generate($db); + $checksum = $config->getHexChecksum(); + } + + $deployer = new ConditionalDeployment($db, $this->api()); + $deployer->force((bool) $this->params->get('force')); + if ($graceTime = $this->params->get('grace-period')) { + $deployer->setGracePeriod(new DeploymentGracePeriod((int) $graceTime, $db)); + if ($this->params->get('force')) { + fwrite(STDERR, "WARNING: force overrides Grace period\n"); + } + } + $deployer->refresh(); + + if ($deployment = $deployer->deploy($config)) { + if ($deployer->hasBeenForced()) { + echo $deployer->getNoDeploymentReason() . ", deploying anyway\n"; + } + printf("Config '%s' has been deployed\n", $checksum); + } else { + echo $deployer->getNoDeploymentReason() . "\n"; + return; + } + + if ($timeout = $this->getWaitTime()) { + $deployed = $deployer->waitForStartupAfterDeploy($deployment, $timeout); + if ($deployed !== true) { + $this->fail("Waiting for Icinga restart failed '%s': %s\n", $checksum, $deployed); + } + } + } + + /** + * Checks the deployments status + */ + public function deploymentstatusAction() + { + $db = $this->db(); + $api = $this->api(); + $status = new DeploymentStatus($db, $api); + $result = $status->getDeploymentStatus($this->params->get('configs'), $this->params->get('activities')); + if ($key = $this->params->get('key')) { + $result = SyncUtils::getSpecificValue($result, $key); + } + + if (is_string($result)) { + echo "$result\n"; + } else { + echo Json::encode($result, JSON_PRETTY_PRINT) . "\n"; + } + } + + protected function getWaitTime() + { + if ($timeout = $this->params->get('wait')) { + if (!ctype_digit($timeout)) { + $this->fail("--wait must be the number of seconds to wait'"); + } + + return (int) $timeout; + } + + return null; + } +} diff --git a/application/clicommands/CoreCommand.php b/application/clicommands/CoreCommand.php new file mode 100644 index 0000000..4927aa5 --- /dev/null +++ b/application/clicommands/CoreCommand.php @@ -0,0 +1,16 @@ +api()->getConstants() as $name => $value) { + printf("const %s = %s\n", $name, PlainObjectRenderer::render($value)); + } + } +} diff --git a/application/clicommands/DaemonCommand.php b/application/clicommands/DaemonCommand.php new file mode 100644 index 0000000..e89e1da --- /dev/null +++ b/application/clicommands/DaemonCommand.php @@ -0,0 +1,26 @@ +] + */ + public function runAction() + { + $this->app->getModuleManager()->loadEnabledModules(); + $daemon = new BackgroundDaemon(); + if ($dbResource = $this->params->get('db-resource')) { + $daemon->setDbResourceName($dbResource); + } + $daemon->run(); + } +} diff --git a/application/clicommands/DependencyCommand.php b/application/clicommands/DependencyCommand.php new file mode 100644 index 0000000..ff5cbdc --- /dev/null +++ b/application/clicommands/DependencyCommand.php @@ -0,0 +1,15 @@ +api()->getStatus()); + } +} diff --git a/application/clicommands/ExportCommand.php b/application/clicommands/ExportCommand.php new file mode 100644 index 0000000..2b2119d --- /dev/null +++ b/application/clicommands/ExportCommand.php @@ -0,0 +1,180 @@ +db()); + echo $this->renderJson( + $export->serializeAllImportSources(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all SyncRule definitions + * + * USAGE + * + * icingacli director export syncrules [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function syncrulesAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllSyncRules(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all Job definitions + * + * USAGE + * + * icingacli director export jobs [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function jobsAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllJobs(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all DataField definitions + * + * USAGE + * + * icingacli director export datafields [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function datafieldsAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllDataFields(), + !$this->params->shift('no-pretty') + ); + } + + /** + * Export all DataList definitions + * + * USAGE + * + * icingacli director export datalists [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function datalistsAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllDataLists(), + !$this->params->shift('no-pretty') + ); + } + + // /** + // * Export all IcingaHostGroup definitions + // * + // * USAGE + // * + // * icingacli director export hostgroup [options] + // * + // * OPTIONS + // * + // * --no-pretty JSON is pretty-printed per default + // * Use this flag to enforce unformatted JSON + // */ + // public function hostgroupAction() + // { + // $export = new ImportExport($this->db()); + // echo $this->renderJson( + // $export->serializeAllHostGroups(), + // !$this->params->shift('no-pretty') + // ); + // } + // + // /** + // * Export all IcingaServiceGroup definitions + // * + // * USAGE + // * + // * icingacli director export servicegroup [options] + // * + // * OPTIONS + // * + // * --no-pretty JSON is pretty-printed per default + // * Use this flag to enforce unformatted JSON + // */ + // public function servicegroupAction() + // { + // $export = new ImportExport($this->db()); + // echo $this->renderJson( + // $export->serializeAllServiceGroups(), + // !$this->params->shift('no-pretty') + // ); + // } + + /** + * Export all IcingaTemplateChoiceHost definitions + * + * USAGE + * + * icingacli director export hosttemplatechoices [options] + * + * OPTIONS + * + * --no-pretty JSON is pretty-printed per default + * Use this flag to enforce unformatted JSON + */ + public function hosttemplatechoicesAction() + { + $export = new ImportExport($this->db()); + echo $this->renderJson( + $export->serializeAllHostTemplateChoices(), + !$this->params->shift('no-pretty') + ); + } +} diff --git a/application/clicommands/HealthCommand.php b/application/clicommands/HealthCommand.php new file mode 100644 index 0000000..1635c50 --- /dev/null +++ b/application/clicommands/HealthCommand.php @@ -0,0 +1,80 @@ + Run only a specific set of checks + * valid names: config, sync, import, jobs, deployment + * --db Use a specific Icinga Web DB resource + * --watch Refresh every . For interactive use only + */ + public function checkAction() + { + $health = new Health(); + if ($name = $this->params->get('db')) { + $health->setDbResourceName($name); + } + + if ($name = $this->params->get('check')) { + $check = $health->getCheck($name); + echo PluginOutputBeautifier::beautify($check->getOutput(), $this->screen); + + exit($check->getState()->getNumeric()); + } else { + $state = new PluginState('OK'); + $checks = $health->getAllChecks(); + + $output = []; + foreach ($checks as $check) { + $state->raise($check->getState()); + $output[] = $check->getOutput(); + } + + if ($state->getNumeric() === 0) { + echo "Icinga Director: everything is fine\n\n"; + } else { + echo "Icinga Director: there are problems\n\n"; + } + + $out = PluginOutputBeautifier::beautify(implode("\n", $output), $this->screen); + echo $out; + + if (! $this->isBeingWatched()) { + exit($state->getNumeric()); + } + } + } + + /** + * Cli should provide this information, as it shifts the parameter + * + * @return bool + */ + protected function isBeingWatched() + { + global $argv; + return in_array('--watch', $argv); + } +} diff --git a/application/clicommands/HostCommand.php b/application/clicommands/HostCommand.php new file mode 100644 index 0000000..21ec5eb --- /dev/null +++ b/application/clicommands/HostCommand.php @@ -0,0 +1,15 @@ +params->shift('pending')) { + $tasks = $this->housekeeping()->getPendingTaskSummary(); + } else { + $tasks = $this->housekeeping()->getTaskSummary(); + } + + $len = array_reduce( + $tasks, + function ($max, $task) { + return max( + $max, + strlen($task->title) + strlen($task->name) + 3 + ); + } + ); + + if (count($tasks)) { + print "\n"; + printf(" %-" . $len . "s | %s\n", 'Housekeeping task (name)', 'Count'); + printf("-%-" . $len . "s-|-------\n", str_repeat('-', $len)); + } + + foreach ($tasks as $task) { + printf( + " %-" . $len . "s | %5d\n", + sprintf('%s (%s)', $task->title, $task->name), + $task->count + ); + } + + if (count($tasks)) { + print "\n"; + } + } + + public function runAction() + { + if (!$job = $this->params->shift()) { + throw new MissingParameterException( + 'Job is required, say ALL to run all pending jobs' + ); + } + + if ($job === 'ALL') { + $this->housekeeping()->runAllTasks(); + } else { + $this->housekeeping()->runTask($job); + } + } + + protected function housekeeping() + { + if ($this->housekeeping === null) { + $this->housekeeping = new Housekeeping($this->db()); + } + + return $this->housekeeping; + } +} diff --git a/application/clicommands/ImportCommand.php b/application/clicommands/ImportCommand.php new file mode 100644 index 0000000..3edfff2 --- /dev/null +++ b/application/clicommands/ImportCommand.php @@ -0,0 +1,62 @@ +db()); + $count = $import->unserializeImportSources(json_decode($json)); + echo "$count Import Sources have been imported\n"; + } + + // /** + // * Import an ImportSource definition + // * + // * USAGE + // * + // * icingacli director import importsource < importsource.json + // * + // * OPTIONS + // */ + // public function importsourcection() + // { + // $json = file_get_contents('php://stdin'); + // $object = ImportSource::import(json_decode($json), $this->db()); + // $object->store(); + // printf("Import Source '%s' has been imported\n", $object->getObjectName()); + // } + + /** + * Import SyncRule definitions + * + * USAGE + * + * icingacli director import syncrules < syncrules.json + */ + public function syncrulesAction() + { + $json = file_get_contents('php://stdin'); + $import = new ImportExport($this->db()); + $count = $import->unserializeSyncRules(json_decode($json)); + echo "$count Sync Rules have been imported\n"; + } +} diff --git a/application/clicommands/ImportsourceCommand.php b/application/clicommands/ImportsourceCommand.php new file mode 100644 index 0000000..477fdf5 --- /dev/null +++ b/application/clicommands/ImportsourceCommand.php @@ -0,0 +1,168 @@ +db()); + if (empty($sources)) { + echo "No Import Source has been defined\n"; + + return; + } + + printf("%4s | %s\n", 'ID', 'Import Source name'); + printf("-----+%s\n", str_repeat('-', 64)); + + foreach ($sources as $source) { + $state = $source->get('import_state'); + printf("%4d | %s\n", $source->get('id'), $source->get('source_name')); + printf(" | -> %s%s\n", $state, $state === 'failing' ? ': ' . $source->get('last_error_message') : ''); + } + } + + /** + * Check a given Import Source for changes + * + * This command fetches data from the given Import Source and compares it + * to the most recently imported data. + * + * USAGE + * + * icingacli director importsource check --id + * + * OPTIONS + * + * --id An Import Source ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function checkAction() + { + $source = $this->getImportSource(); + $source->checkForChanges(); + $this->showImportStateDetails($source); + } + + /** + * Fetch current data from a given Import Source + * + * This command fetches data from the given Import Source and outputs + * them as plain JSON + * + * USAGE + * + * icingacli director importsource fetch --id + * + * OPTIONS + * + * --id An Import Source ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function fetchAction() + { + $source = $this->getImportSource(); + $source->checkForChanges(); + $hook = ImportSourceHook::forImportSource($source); + Benchmark::measure('Ready to fetch data'); + $data = $hook->fetchData(); + $source->applyModifiers($data); + Benchmark::measure(sprintf('Got %d rows, ready to dump JSON', count($data))); + echo Json::encode($data, JSON_PRETTY_PRINT); + } + + /** + * Trigger an Import Run for a given Import Source + * + * This command fetches data from the given Import Source and stores it to + * the Director DB, so that the next related Sync Rule run can work with + * fresh data. In case data didn't change, nothing is going to be stored. + * + * USAGE + * + * icingacli director importsource run --id + * + * OPTIONS + * + * --id An Import Source ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function runAction() + { + $source = $this->getImportSource(); + + if ($source->runImport()) { + print "New data has been imported\n"; + $this->showImportStateDetails($source); + } else { + print "Nothing has been changed, imported data is still up to date\n"; + } + } + + /** + * @return ImportSource + */ + protected function getImportSource() + { + return ImportSource::loadWithAutoIncId( + (int) $this->params->getRequired('id'), + $this->db() + ); + } + + /** + * @param ImportSource $source + * @throws \Icinga\Exception\IcingaException + */ + protected function showImportStateDetails(ImportSource $source) + { + echo $this->getImportStateDescription($source) . "\n"; + } + + /** + * @param ImportSource $source + * @return string + * @throws \Icinga\Exception\IcingaException + */ + protected function getImportStateDescription(ImportSource $source) + { + switch ($source->get('import_state')) { + case 'unknown': + return "It's currently unknown whether we are in sync with this" + . ' Import Source. You should either check for changes or' + . ' trigger a new Import Run.'; + case 'in-sync': + return 'This Import Source is in sync'; + case 'pending-changes': + return 'There are pending changes for this Import Source. You' + . ' should trigger a new Import Run.'; + case 'failing': + return 'This Import Source failed: ' . $source->get('last_error_message'); + default: + return 'This Import Source has an invalid state: ' . $source->get('import_state'); + } + } +} diff --git a/application/clicommands/JobsCommand.php b/application/clicommands/JobsCommand.php new file mode 100644 index 0000000..1c6297f --- /dev/null +++ b/application/clicommands/JobsCommand.php @@ -0,0 +1,74 @@ +app->getModuleManager()->loadEnabledModules(); + $loop = Loop::create(); + if ($this->params->get('rpc')) { + $this->enableRpc($loop); + } + if ($this->params->get('rpc') && $jobId = $this->params->get('id')) { + $exitCode = 1; + $jobId = (int) $jobId; + $loop->futureTick(function () use ($jobId, $loop, &$exitCode) { + Process::setTitle('icinga::director::job'); + try { + $this->raiseLimits(); + $job = DirectorJob::loadWithAutoIncId($jobId, $this->db()); + Process::setTitle('icinga::director::job (' . $job->get('job_name') . ')'); + if ($job->run()) { + $exitCode = 0; + } else { + $exitCode = 1; + } + } catch (Exception $e) { + Logger::error($e->getMessage()); + $exitCode = 1; + } + $loop->futureTick(function () use ($loop) { + $loop->stop(); + }); + }); + } else { + Logger::error('This command is no longer available. Please check our Upgrading documentation'); + $exitCode = 1; + } + + $loop->run(); + exit($exitCode); + } + + protected function enableRpc(LoopInterface $loop) + { + // stream_set_blocking(STDIN, 0); + // stream_set_blocking(STDOUT, 0); + // print_r(stream_get_meta_data(STDIN)); + // stream_set_write_buffer(STDOUT, 0); + // ini_set('implicit_flush', 1); + $netString = new StreamWrapper( + new ReadableResourceStream(STDIN, $loop), + new WritableResourceStream(STDOUT, $loop) + ); + $jsonRpc = new Connection(); + $jsonRpc->handle($netString); + + Logger::replaceRunningInstance(new JsonRpcLogWriterAlias($jsonRpc)); + } +} diff --git a/application/clicommands/KickstartCommand.php b/application/clicommands/KickstartCommand.php new file mode 100644 index 0000000..80aa183 --- /dev/null +++ b/application/clicommands/KickstartCommand.php @@ -0,0 +1,88 @@ + '/usr/local/bin:/usr/bin:/bin', + * command => 'icingacli director kickstart run', + * onlyif => 'icingacli director kickstart required', + * require => Exec['Icinga Director DB migration'], + * } + * + * Exit code 0 means that a kickstart run is required, code 2 that it is + * not. + */ + public function requiredAction() + { + if ($this->kickstart()->isConfigured()) { + if ($this->kickstart()->isRequired()) { + if ($this->isVerbose) { + echo "Kickstart has been configured and should be triggered\n"; + } + + exit(0); + } else { + echo "Kickstart configured, execution is not required\n"; + exit(1); + } + } else { + echo "Kickstart has not been configured\n"; + exit(2); + } + } + + /** + * Trigger the kickstart helper + * + * This will connect to the endpoint configured in your kickstart.ini, + * store the given API user and import existing objects like zones, + * endpoints and commands. + * + * /etc/icingaweb2/modules/director/kickstart.ini could look as follows: + * + * [config] + * endpoint = "master-node.example.com" + * + * ; Host can be an IP address or a hostname. Equals to endpoint name + * ; if not set: + * host = "127.0.0.1" + * + * ; Port is 5665 if none given + * port = 5665 + * + * username = "director" + * password = "***" + * + */ + public function runAction() + { + $this->raiseLimits(); + $this->kickstart()->loadConfigFromFile()->run(); + exit(0); + } + + protected function kickstart() + { + return new KickstartHelper($this->db()); + } +} diff --git a/application/clicommands/MigrationCommand.php b/application/clicommands/MigrationCommand.php new file mode 100644 index 0000000..6a4d002 --- /dev/null +++ b/application/clicommands/MigrationCommand.php @@ -0,0 +1,66 @@ + 'icingacli director migration run', + * onlyif => 'icingacli director migration pending', + * } + * + * Exit code 0 means that there are pending migrations, code 1 that there + * are no such. Use --verbose for human readable output + */ + public function pendingAction() + { + if ($count = $this->migrations()->countPendingMigrations()) { + if ($this->isVerbose) { + if ($count === 1) { + echo "There is 1 pending migration\n"; + } else { + printf("There are %d pending migrations\n", $count); + } + } + + exit(0); + } else { + if ($this->isVerbose) { + echo "There are no pending migrations\n"; + } + + exit(1); + } + } + + /** + * Run any pending migrations + * + * All pending migrations will be silently applied + */ + public function runAction() + { + $this->migrations()->applyPendingMigrations(); + exit(0); + } + + protected function migrations() + { + return new Migrations($this->db()); + } +} diff --git a/application/clicommands/NotificationCommand.php b/application/clicommands/NotificationCommand.php new file mode 100644 index 0000000..bb5402a --- /dev/null +++ b/application/clicommands/NotificationCommand.php @@ -0,0 +1,15 @@ +params->get('host')) && $this->params->shift('allow-overrides')) { + if ($this->setServiceProperties($host)) { + return; + } + } + + parent::setAction(); + } + + protected function setServiceProperties($hostname) + { + $serviceName = $this->getName(); + $host = IcingaHost::load($hostname, $this->db()); + $service = ServiceFinder::find($host, $serviceName); + if ($service->requiresOverrides()) { + self::checkForOverrideSafety($this->params); + $properties = $this->remainingParams(); + unset($properties['host']); + OverrideHelper::applyOverriddenVars($host, $serviceName, $properties); + $this->persistChanges($host, 'Host', $hostname . " (Overrides for $serviceName)", 'modified'); + return true; + } + + return false; + } + + protected static function checkForOverrideSafety(Params $params) + { + if ($params->shift('replace')) { + throw new InvalidArgumentException('--replace is not available for Variable Overrides'); + } + $appends = self::stripPrefixedProperties($params, 'append-'); + $remove = self::stripPrefixedProperties($params, 'remove-'); + OverrideHelper::assertVarsForOverrides($appends); + OverrideHelper::assertVarsForOverrides($remove); + if (!empty($appends)) { + throw new InvalidArgumentException('--append- is not available for Variable Overrides'); + } + if (!empty($remove)) { + throw new InvalidArgumentException('--remove- is not available for Variable Overrides'); + } + // Alternative, untested: + // $this->appendToArrayProperties($object, $appends); + // $this->removeProperties($object, $remove); + } + + protected function load($name) + { + return parent::load($this->makeServiceKey($name)); + } + + protected function exists($name) + { + return parent::exists($this->makeServiceKey($name)); + } + + protected function makeServiceKey($name) + { + if ($host = $this->params->get('host')) { + return [ + 'object_name' => $name, + 'host_id' => IcingaHost::load($host, $this->db())->get('id'), + ]; + } else { + return [ + 'object_name' => $name, + 'object_type' => 'template', + ]; + } + } +} diff --git a/application/clicommands/ServicegroupCommand.php b/application/clicommands/ServicegroupCommand.php new file mode 100644 index 0000000..1c732d4 --- /dev/null +++ b/application/clicommands/ServicegroupCommand.php @@ -0,0 +1,15 @@ +db()); + if (empty($rules)) { + echo "No Sync Rule has been defined\n"; + + return; + } + + printf("%4s | %s\n", 'ID', 'Sync Rule name'); + printf("-----+%s\n", str_repeat('-', 64)); + + foreach ($rules as $rule) { + $state = $rule->get('sync_state'); + printf("%4d | %s\n", $rule->get('id'), $rule->get('rule_name')); + printf(" | -> %s%s\n", $state, $state === 'failing' ? ': ' . $rule->get('last_error_message') : ''); + } + } + + /** + * Check a given Sync Rule for changes + * + * This command runs a complete Sync in memory but doesn't persist eventual changes. + * + * USAGE + * + * icingacli director syncrule check --id + * + * OPTIONS + * + * --id A Sync Rule ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function checkAction() + { + $rule = $this->getSyncRule(); + $hasChanges = $rule->checkForChanges(); + $this->showSyncStateDetails($rule); + if ($hasChanges) { + $mods = $this->getExpectedModificationCounts($rule); + printf( + "Expected modifications: %dx create, %dx modify, %dx delete\n", + $mods->modify, + $mods->create, + $mods->delete + ); + } + + exit($this->getSyncStateExitCode($rule)); + } + + protected function getExpectedModificationCounts(SyncRule $rule) + { + $modifications = $rule->getExpectedModifications(); + + $create = 0; + $modify = 0; + $delete = 0; + + /** @var IcingaObject $object */ + foreach ($modifications as $object) { + if ($object->hasBeenLoadedFromDb()) { + if ($object->shouldBeRemoved()) { + $delete++; + } else { + $modify++; + } + } else { + $create++; + } + } + + return (object) [ + DirectorActivityLog::ACTION_CREATE => $create, + DirectorActivityLog::ACTION_MODIFY => $modify, + DirectorActivityLog::ACTION_DELETE => $delete, + ]; + } + + /** + * Trigger a Sync Run for a given Sync Rule + * + * This command builds new objects according your Sync Rule, compares them + * with existing ones and persists eventual changes. + * + * USAGE + * + * icingacli director syncrule run --id + * + * OPTIONS + * + * --id A Sync Rule ID. Use the list command to figure out + * --benchmark Show timing and memory usage details + */ + public function runAction() + { + $rule = $this->getSyncRule(); + + if ($rule->applyChanges()) { + print "New data has been imported\n"; + $this->showSyncStateDetails($rule); + } else { + print "Nothing has been changed, imported data is still up to date\n"; + } + } + + /** + * @return SyncRule + */ + protected function getSyncRule() + { + return SyncRule::loadWithAutoIncId( + (int) $this->params->getRequired('id'), + $this->db() + ); + } + + /** + * @param SyncRule $rule + */ + protected function showSyncStateDetails(SyncRule $rule) + { + echo $this->getSyncStateDescription($rule) . "\n"; + } + + /** + * @param SyncRule $rule + * @return string + */ + protected function getSyncStateDescription(SyncRule $rule) + { + switch ($rule->get('sync_state')) { + case 'unknown': + return "It's currently unknown whether we are in sync with this rule." + . ' You should either check for changes or trigger a new Sync Run.'; + case 'in-sync': + return 'This Sync Rule is in sync'; + case 'pending-changes': + return 'There are pending changes for this Sync Rule. You should' + . ' trigger a new Sync Run.'; + case 'failing': + return 'This Sync Rule failed: '. $rule->get('last_error_message'); + default: + throw new RuntimeException('Invalid sync state: ' . $rule->get('sync_state')); + } + } + + /** + * @param SyncRule $rule + * @return string + */ + protected function getSyncStateExitCode(SyncRule $rule) + { + switch ($rule->get('sync_state')) { + case 'unknown': + return 3; + case 'in-sync': + return 0; + case 'pending-changes': + return 1; + case 'failing': + return 2; + default: + throw new RuntimeException('Invalid sync state: ' . $rule->get('sync_state')); + } + } +} diff --git a/application/clicommands/TimeperiodCommand.php b/application/clicommands/TimeperiodCommand.php new file mode 100644 index 0000000..352289a --- /dev/null +++ b/application/clicommands/TimeperiodCommand.php @@ -0,0 +1,15 @@ +