diff options
Diffstat (limited to 'library/Director/Cli')
-rw-r--r-- | library/Director/Cli/Command.php | 115 | ||||
-rw-r--r-- | library/Director/Cli/ObjectCommand.php | 517 | ||||
-rw-r--r-- | library/Director/Cli/ObjectsCommand.php | 115 | ||||
-rw-r--r-- | library/Director/Cli/PluginOutputBeautifier.php | 75 |
4 files changed, 822 insertions, 0 deletions
diff --git a/library/Director/Cli/Command.php b/library/Director/Cli/Command.php new file mode 100644 index 0000000..69d61b1 --- /dev/null +++ b/library/Director/Cli/Command.php @@ -0,0 +1,115 @@ +<?php + +namespace Icinga\Module\Director\Cli; + +use gipfl\Json\JsonDecodeException; +use gipfl\Json\JsonString; +use Icinga\Cli\Command as CliCommand; +use Icinga\Module\Director\Application\MemoryLimit; +use Icinga\Module\Director\Core\CoreApi; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaEndpoint; +use Icinga\Application\Config; +use RuntimeException; + +class Command extends CliCommand +{ + /** @var Db */ + protected $db; + + /** @var CoreApi */ + private $api; + + protected function renderJson($object, $pretty = true) + { + return JsonString::encode($object, $pretty ? JSON_PRETTY_PRINT : null) . "\n"; + } + + /** + * @param $json + * @return mixed + */ + protected function parseJson($json) + { + try { + return JsonString::decode($json); + } catch (JsonDecodeException $e) { + $this->fail('Invalid JSON: %s', $e->getMessage()); + } + } + + /** + * @param string $msg + * @return never-return + */ + public function fail($msg) + { + $args = func_get_args(); + array_shift($args); + if (count($args)) { + $msg = vsprintf($msg, $args); + } + echo $this->screen->colorize("ERROR", 'red') . ": $msg\n"; + exit(1); + } + + /** + * @param null $endpointName + * @return CoreApi|\Icinga\Module\Director\Core\LegacyDeploymentApi + * @throws \Icinga\Exception\NotFoundError + */ + protected function api($endpointName = null) + { + if ($this->api === null) { + if ($endpointName === null) { + $endpoint = $this->db()->getDeploymentEndpoint(); + } else { + $endpoint = IcingaEndpoint::load($endpointName, $this->db()); + } + + $this->api = $endpoint->api(); + } + + return $this->api; + } + + /** + * Raise PHP resource limits + * + * @return self; + */ + protected function raiseLimits() + { + MemoryLimit::raiseTo('1024M'); + + ini_set('max_execution_time', 0); + if (version_compare(PHP_VERSION, '7.0.0') < 0) { + ini_set('zend.enable_gc', 0); + } + + return $this; + } + + /** + * @return Db + */ + protected function db() + { + if ($this->db === null) { + $resourceName = $this->params->get('dbResourceName'); + + if ($resourceName === null) { + // Hint: not using $this->Config() intentionally. This allows + // CLI commands in other modules to use this as a base class. + $resourceName = Config::module('director')->get('db', 'resource'); + } + if ($resourceName) { + $this->db = Db::fromResourceName($resourceName); + } else { + throw new RuntimeException('Director is not configured correctly'); + } + } + + return $this->db; + } +} diff --git a/library/Director/Cli/ObjectCommand.php b/library/Director/Cli/ObjectCommand.php new file mode 100644 index 0000000..ca68213 --- /dev/null +++ b/library/Director/Cli/ObjectCommand.php @@ -0,0 +1,517 @@ +<?php + +namespace Icinga\Module\Director\Cli; + +use Icinga\Cli\Params; +use Icinga\Exception\MissingParameterException; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\PropertyMangler; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaObject; +use InvalidArgumentException; + +class ObjectCommand extends Command +{ + protected $type; + + private $name; + + private $object; + + private $experimentalFlags = array(); + + public function init() + { + $this->shiftExperimentalFlags(); + } + + /** + * Show a specific object + * + * Use this command to show single objects rendered as Icinga 2 + * config or in JSON format. + * + * USAGE + * + * icingacli director <type> show <name> [options] + * + * OPTIONS + * + * --resolved Resolve all inherited properties and show a flat + * object + * --json Use JSON format + * --no-pretty JSON is pretty-printed per default. Use this flag + * to enforce un-formatted JSON + * --no-defaults Per default JSON output ships null or default + * values. This flag skips those properties + * --with-services For hosts only, also shows attached services + * --all-services For hosts only, show applied and inherited services + * too + */ + public function showAction() + { + $db = $this->db(); + $object = $this->getObject(); + $exporter = new Exporter($db); + $resolve = (bool) $this->params->shift('resolved'); + $withServices = (bool) $this->params->get('with-services'); + $allServices = (bool) $this->params->get('all-services'); + if ($withServices) { + if (!$object instanceof IcingaHost) { + $this->fail('--with-services is available for Hosts only'); + } + $exporter->enableHostServices(); + } + if ($allServices) { + if (!$object instanceof IcingaHost) { + $this->fail('--all-services is available for Hosts only'); + } + $exporter->serviceLoader()->resolveHostServices(); + } + + $exporter->resolveObjects($resolve); + $exporter->showDefaults($this->params->shift('no-defaults', false)); + + if ($this->params->shift('json')) { + echo $this->renderJson($exporter->export($object), !$this->params->shift('no-pretty')); + } else { + $config = new IcingaConfig($db); + if ($resolve) { + $object = $object::fromPlainObject($object->toPlainObject(true, false, null, false), $db); + } + $object->renderToConfig($config); + if ($withServices) { + foreach ($exporter->serviceLoader()->fetchServicesForHost($object) as $service) { + $service->renderToConfig($config); + } + } + foreach ($config->getFiles() as $filename => $content) { + printf("/** %s **/\n\n", $filename); + echo $content; + } + } + } + + /** + * Create a new object + * + * Use this command to create a new Icinga object + * + * USAGE + * + * icingacli director <type> create [<name>] [options] + * + * OPTIONS + * + * --<key> <value> Provide all properties as single command line + * options + * --json Otherwise provide all options as a JSON string + * + * EXAMPLES + * + * icingacli director host create localhost \ + * --imports generic-host \ + * --address 127.0.0.1 \ + * --vars.location 'My datacenter' + * + * icingacli director host create localhost \ + * --json '{ "address": "127.0.0.1" }' + */ + public function createAction() + { + $type = $this->getType(); + $props = $this->getObjectProperties(); + $name = $props['object_name']; + $object = IcingaObject::createByType($type, $props, $this->db()); + + if ($object->store()) { + printf("%s '%s' has been created\n", $type, $name); + if ($this->hasExperimental('live-creation')) { + if ($this->api()->createObjectAtRuntime($object)) { + echo "Live creation for '$name' succeeded\n"; + } else { + echo "Live creation for '$name' succeeded\n"; + exit(1); + } + + if ($type === 'Host' && $this->hasExperimental('immediate-check')) { + echo "Waiting for check result..."; + flush(); + if ($res = $this->api()->checkHostAndWaitForResult($name)) { + echo " done\n" . $res->output . "\n"; + } else { + echo "TIMEOUT\n"; + } + } + } + + exit(0); + } else { + printf("%s '%s' has not been created\n", $type, $name); + exit(1); + } + } + + /** + * Modify an existing objects properties + * + * Use this command to modify specific properties of an existing + * Icinga object + * + * USAGE + * + * icingacli director <type> set <name> [options] + * + * OPTIONS + * + * --<key> <value> Provide all properties as single command line + * options + * --append-<key> <value> Appends to array values, like `imports`, + * `groups` or `vars.system_owners` + * --remove-<key> [<value>] Remove a specific property, eventually only + * when matching `value`. In case the property is an + * array it will remove just `value` when given + * --json Otherwise provide all options as a JSON string + * --replace Replace all object properties with the given ones + * --auto-create Create the object in case it does not exist + * + * EXAMPLES + * + * icingacli director host set localhost \ + * --address 127.0.0.2 \ + * --vars.location 'Somewhere else' + * + * icingacli director host set localhost \ + * --json '{ "address": "127.0.0.2" }' + */ + public function setAction() + { + $name = $this->getName(); + $type = $this->getType(); + + if ($this->params->shift('auto-create') && ! $this->exists($name)) { + $action = 'created'; + $object = $this->create($type, $name); + } else { + $action = 'modified'; + $object = $this->getObject(); + } + + $appends = self::stripPrefixedProperties($this->params, 'append-'); + $remove = self::stripPrefixedProperties($this->params, 'remove-'); + + if ($this->params->shift('replace')) { + $object->replaceWith($this->create($type, $name, $this->remainingParams())); + } else { + $object->setProperties($this->remainingParams()); + } + + PropertyMangler::appendToArrayProperties($object, $appends); + PropertyMangler::removeProperties($object, $remove); + $this->persistChanges($object, $type, $name, $action); + } + + protected function persistChanges(DbObject $object, $type, $name, $action) + { + if ($object->hasBeenModified() && $object->store()) { + printf("%s '%s' has been %s\n", $type, $name, $action); + exit(0); + } + + printf("%s '%s' has not been modified\n", $type, $name); + exit(0); + } + + /** + * Delete a specific object + * + * Use this command to delete a single Icinga object + * + * USAGE + * + * icingacli director <type> delete <name> + * + * EXAMPLES + * + * icingacli director host delete localhost2 + * + * icingacli director host delete localhost{3..8} + */ + public function deleteAction() + { + $type = $this->getType(); + + foreach ($this->shiftOneOrMoreNames() as $name) { + if ($this->load($name)->delete()) { + printf("%s '%s' has been deleted\n", $type, $name); + } else { + printf("Something went wrong while deleting %s '%s'\n", $type, $name); + exit(1); + } + + $this->object = null; + } + exit(0); + } + + /** + * Whether a specific object exists + * + * Use this command to find out whether a single Icinga object exists + * + * USAGE + * + * icingacli director <type> exists <name> + */ + public function existsAction() + { + $name = $this->getName(); + $type = $this->getType(); + if ($this->exists($name)) { + printf("%s '%s' exists\n", $type, $name); + exit(0); + } else { + printf("%s '%s' does not exist\n", $type, $name); + exit(1); + } + } + + /** + * Clone an existing object + * + * Use this command to clone a specific object + * + * USAGE + * + * icingacli director <type> clone <name> --from <original> [options] + * + * OPTIONS + * --from <original> The name of the object you want to clone + * --<key> <value> Override specific properties while cloning + * --replace In case an object <name> already exists replace + * it with the clone + * --flat Do no keep inherited properties but create a flat + * object with all resolved/inherited properties + * + * EXAMPLES + * + * icingacli director host clone localhost2 --from localhost + * + * icingacli director host clone localhost{3..8} --from localhost2 + * + * icingacli director host clone localhost3 --from localhost \ + * --address 127.0.0.3 + */ + public function cloneAction() + { + $fromName = $this->params->shiftRequired('from'); + $from = $this->load($fromName); + + // $name = $this->getName(); + $type = $this->getType(); + + $resolve = $this->params->shift('flat'); + $replace = $this->params->shift('replace'); + + $from->setProperties($this->remainingParams()); + + foreach ($this->shiftOneOrMoreNames() as $name) { + $object = $from::fromPlainObject( + $from->toPlainObject($resolve), + $from->getConnection() + ); + + $object->set('object_name', $name); + + if ($replace && $this->exists($name)) { + $object = $this->load($name)->replaceWith($object); + } + + if ($object->hasBeenModified() && $object->store()) { + printf("%s '%s' has been cloned from %s\n", $type, $name, $fromName); + } else { + printf("%s '%s' has not been modified\n", $this->getType(), $name); + } + } + + exit(0); + } + + protected static function stripPrefixedProperties(Params $params, $prefix = 'append-') + { + $appends = []; + $len = strlen($prefix); + + foreach ($params->getParams() as $key => $value) { + if (substr($key, 0, $len) === $prefix) { + $appends[substr($key, $len)] = $value; + } + } + + foreach ($appends as $key => $value) { + $params->shift("$prefix$key"); + } + + return $appends; + } + + protected function getObjectProperties() + { + $name = $this->params->shift(); + + $props = $this->remainingParams(); + if (! array_key_exists('object_type', $props)) { + $props['object_type'] = 'object'; + } + + // Normalize object_name, compare to given name + if ($name) { + if (array_key_exists('object_name', $props)) { + if ($name !== $props['object_name']) { + $this->fail(sprintf( + "Name '%s' conflicts with object_name '%s'\n", + $name, + $props['object_name'] + )); + } + } else { + $props['object_name'] = $name; + } + } else { + if (! array_key_exists('object_name', $props)) { + $this->fail('Cannot create an object with at least an object name'); + } + } + + return $props; + } + + protected function shiftOneOrMoreNames() + { + $names = array(); + while ($name = $this->params->shift()) { + $names[] = $name; + } + + if (empty($names)) { + throw new MissingParameterException('Required object name is missing'); + } + + return $names; + } + + protected function remainingParams() + { + if ($json = $this->params->shift('json')) { + if ($json === true) { + $json = $this->readFromStdin(); + if ($json === null) { + $this->fail('Please pass JSON either via STDIN or via --json'); + } + } + return (array) $this->parseJson($json); + } else { + return $this->params->getParams(); + } + } + + protected function readFromStdin() + { + if (!defined('STDIN')) { + define('STDIN', fopen('php://stdin', 'r')); + } + $inputIsTty = function_exists('posix_isatty') && posix_isatty(STDIN); + if ($inputIsTty) { + return null; + } + + $stdin = file_get_contents('php://stdin'); + if (strlen($stdin) === 0) { + return null; + } + + return $stdin; + } + + protected function exists($name) + { + return IcingaObject::existsByType( + $this->getType(), + $name, + $this->db() + ); + } + + protected function load($name) + { + return IcingaObject::loadByType( + $this->getType(), + $name, + $this->db() + ); + } + + protected function create($type, $name, $properties = []) + { + return IcingaObject::createByType($type, $properties + [ + 'object_type' => 'object', + 'object_name' => $name + ], $this->db()); + } + + /** + * @return IcingaObject + */ + protected function getObject() + { + if ($this->object === null) { + $this->object = $this->load($this->getName()); + } + + return $this->object; + } + + protected function getType() + { + if ($this->type === null) { + // Extract the command class name... + $className = substr(strrchr(get_class($this), '\\'), 1); + // ...and strip the Command extension + $this->type = substr($className, 0, -7); + } + + return $this->type; + } + + protected function getName() + { + if ($this->name === null) { + $name = $this->params->shift(); + if (! $name) { + throw new InvalidArgumentException('Object name parameter is required'); + } + + $this->name = $name; + } + + return $this->name; + } + + protected function hasExperimental($flag) + { + return array_key_exists($flag, $this->experimentalFlags); + } + + protected function shiftExperimentalFlags() + { + if ($flags = $this->params->shift('experimental')) { + foreach (preg_split('/,/', $flags) as $flag) { + $this->experimentalFlags[$flag] = true; + } + } + + return $this; + } +} diff --git a/library/Director/Cli/ObjectsCommand.php b/library/Director/Cli/ObjectsCommand.php new file mode 100644 index 0000000..3e0844a --- /dev/null +++ b/library/Director/Cli/ObjectsCommand.php @@ -0,0 +1,115 @@ +<?php + +namespace Icinga\Module\Director\Cli; + +use Icinga\Module\Director\Objects\IcingaObject; + +class ObjectsCommand extends Command +{ + protected $type; + + private $objects; + + /** + * List all objects of this type + * + * Use this command to get a list of all matching objects + * + * USAGE + * + * icingacli director <types> list [options] + * + * OPTIONS + * + * --json Use JSON format + * --no-pretty JSON is pretty-printed per default (for PHP >= 5.4) + * Use this flag to enforce unformatted JSON + */ + public function listAction() + { + $db = $this->db(); + $result = array(); + foreach ($this->getObjects() as $o) { + $result[] = $o->getObjectName(); + } + + sort($result); + + if ($this->params->shift('json')) { + echo $this->renderJson($result, !$this->params->shift('no-pretty')); + } else { + foreach ($result as $name) { + echo $name . "\n"; + } + } + } + + /** + * Fetch all objects of this type + * + * Use this command to fetch all matching objects + * + * USAGE + * + * icingacli director <types> fetch [options] + * + * OPTIONS + * + * --resolved Resolve all inherited properties and show a flat + * object + * --json Use JSON format + * --no-pretty JSON is pretty-printed per default (for PHP >= 5.4) + * Use this flag to enforce unformatted JSON + * --no-defaults Per default JSON output ships null or default values + * With this flag you will skip those properties + */ + public function fetchAction() + { + $resolved = $this->params->shift('resolved'); + + if ($this->params->shift('json')) { + $noDefaults = $this->params->shift('no-defaults', false); + } else { + $this->fail('Currently only json is supported when fetching objects'); + } + + $db = $this->db(); + $res = array(); + foreach ($this->getObjects() as $object) { + if ($resolved) { + $object = $object::fromPlainObject($object->toPlainObject(true), $db); + } + + $res[$object->getObjectName()] = $object->toPlainObject(false, $noDefaults); + } + + echo $this->renderJson($res, !$this->params->shift('no-pretty')); + } + + /** + * @return IcingaObject[] + */ + protected function getObjects() + { + if ($this->objects === null) { + $this->objects = IcingaObject::loadAllByType( + $this->getType(), + $this->db() + ); + } + + return $this->objects; + } + + protected function getType() + { + if ($this->type === null) { + // Extract the command class name... + $className = substr(strrchr(get_class($this), '\\'), 1); + // ...and strip the Command extension + $this->type = rtrim(substr($className, 0, -7), 's'); + } + + return $this->type; + } +} diff --git a/library/Director/Cli/PluginOutputBeautifier.php b/library/Director/Cli/PluginOutputBeautifier.php new file mode 100644 index 0000000..18a18f2 --- /dev/null +++ b/library/Director/Cli/PluginOutputBeautifier.php @@ -0,0 +1,75 @@ +<?php + +namespace Icinga\Module\Director\Cli; + +use Icinga\Cli\Screen; + +class PluginOutputBeautifier +{ + /** @var Screen */ + protected $screen; + + protected $isTty; + + protected $colorized; + + public function __construct(Screen $screen) + { + $this->screen = $screen; + } + + public static function beautify($string, Screen $screen) + { + $self = new static($screen); + if ($self->isTty()) { + return $self->colorizeStates($string); + } else { + return $string; + } + } + + protected function colorizeStates($string) + { + $string = preg_replace_callback( + "/'([^']+)'/", + [$this, 'highlightNames'], + $string + ); + + $string = preg_replace_callback( + '/(OK|WARNING|CRITICAL|UNKNOWN)/', + [$this, 'getColorized'], + $string + ); + + return $string; + } + + protected function isTty() + { + if ($this->isTty === null) { + $this->isTty = function_exists('posix_isatty') && posix_isatty(STDOUT); + } + + return $this->isTty; + } + + protected function highlightNames($match) + { + return "'" . $this->screen->colorize($match[1], 'darkgray') . "'"; + } + + protected function getColorized($match) + { + if ($this->colorized === null) { + $this->colorized = [ + 'OK' => $this->screen->colorize('OK', 'lightgreen'), + 'WARNING' => $this->screen->colorize('WARNING', 'yellow'), + 'CRITICAL' => $this->screen->colorize('CRITICAL', 'lightred'), + 'UNKNOWN' => $this->screen->colorize('UNKNOWN', 'lightpurple'), + ]; + } + + return $this->colorized[$match[1]]; + } +} |