diff --git a/application/clicommands/ConfigCommand.php b/application/clicommands/ConfigCommand.php index dbf92a626..e313aa4b5 100644 --- a/application/clicommands/ConfigCommand.php +++ b/application/clicommands/ConfigCommand.php @@ -132,12 +132,13 @@ public function deployAction() 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("Failed to deploy config '%s': %s\n", $checksum, $deployed); + $this->fail("Waiting for Icinga restart failed '%s': %s\n", $checksum, $deployed); } } } diff --git a/application/clicommands/ServiceCommand.php b/application/clicommands/ServiceCommand.php index c947064e2..1bd21e766 100644 --- a/application/clicommands/ServiceCommand.php +++ b/application/clicommands/ServiceCommand.php @@ -2,8 +2,12 @@ namespace Icinga\Module\Director\Clicommands; +use Icinga\Cli\Params; use Icinga\Module\Director\Cli\ObjectCommand; +use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder; use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Resolver\OverrideHelper; +use InvalidArgumentException; /** * Manage Icinga Services @@ -13,6 +17,53 @@ */ class ServiceCommand extends ObjectCommand { + public function setAction() + { + if (($host = $this->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) { diff --git a/application/clicommands/SyncruleCommand.php b/application/clicommands/SyncruleCommand.php index 961683df3..37a3f0e93 100644 --- a/application/clicommands/SyncruleCommand.php +++ b/application/clicommands/SyncruleCommand.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Clicommands; use Icinga\Module\Director\Cli\Command; +use Icinga\Module\Director\Objects\DirectorActivityLog; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Objects\SyncRule; use RuntimeException; @@ -98,9 +99,9 @@ protected function getExpectedModificationCounts(SyncRule $rule) } return (object) [ - 'create' => $create, - 'modify' => $modify, - 'delete' => $delete, + DirectorActivityLog::ACTION_CREATE => $create, + DirectorActivityLog::ACTION_MODIFY => $modify, + DirectorActivityLog::ACTION_DELETE => $delete, ]; } diff --git a/application/controllers/BasketController.php b/application/controllers/BasketController.php index b1ff2f8a4..8733d1649 100644 --- a/application/controllers/BasketController.php +++ b/application/controllers/BasketController.php @@ -10,6 +10,7 @@ use gipfl\Web\Widget\Hint; use Icinga\Date\DateFormatter; use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Data\Exporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\Basket; use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot; @@ -247,6 +248,7 @@ public function snapshotAction() $json = $snapshot->getJsonDump(); $this->addSingleTab($this->translate('Snapshot')); $all = Json::decode($json); + $exporter = new Exporter($this->db()); $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); foreach ($all as $type => $objects) { if ($type === 'Datafield') { @@ -284,7 +286,7 @@ public function snapshotAction() ); continue; } - $currentExport = $current->export(); + $currentExport = $exporter->export($current); $fieldResolver->tweakTargetIds($currentExport); // Ignore originalId @@ -366,7 +368,7 @@ public function snapshotobjectAction() ) */ ]); - + $exporter = new Exporter($this->db()); $json = $snapshot->getJsonDump(); $this->addSingleTab($this->translate('Snapshot')); $objects = Json::decode($json); @@ -385,7 +387,7 @@ public function snapshotobjectAction() if ($current === null) { $current = ''; } else { - $exported = $current->export(); + $exported = $exporter->export($current); $fieldResolver->tweakTargetIds($exported); unset($exported->originalId); CompareBasketObject::normalize($exported); diff --git a/application/controllers/ImportsourceController.php b/application/controllers/ImportsourceController.php index 1d70eb8e5..cbddb9e9e 100644 --- a/application/controllers/ImportsourceController.php +++ b/application/controllers/ImportsourceController.php @@ -4,6 +4,7 @@ use Exception; use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Data\Exporter; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Forms\ImportRowModifierForm; use Icinga\Module\Director\Forms\ImportSourceForm; @@ -84,7 +85,7 @@ public function indexAction() $this->addMainActions(); $source = $this->getImportSource(); if ($this->params->get('format') === 'json') { - $this->sendJson($this->getResponse(), $source->export()); + $this->sendJson($this->getResponse(), (new Exporter($this->db()))->export($source)); return; } $this->addTitle( diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php index ec9545ef3..2438852e7 100644 --- a/application/controllers/ServiceController.php +++ b/application/controllers/ServiceController.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Controllers; use Exception; +use Icinga\Exception\NotFoundError; use Icinga\Module\Director\Data\Db\DbObjectStore; use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db\Branch\UuidLookup; @@ -250,9 +251,16 @@ protected function loadObject() } $key = $this->getLegacyKey(); - $uuid = UuidLookup::findServiceUuid($this->db(), $this->getBranch(), 'object', $key, $this->host, $this->set); - $this->params->set('uuid', $uuid->toString()); - parent::loadObject(); + // Hint: not passing 'object' as type, we still have name-based links in previews and similar + $uuid = UuidLookup::findServiceUuid($this->db(), $this->getBranch(), null, $key, $this->host, $this->set); + if ($uuid === null) { + if (! $this->params->get('allowOverrides')) { + throw new NotFoundError('Not found'); + } + } else { + $this->params->set('uuid', $uuid->toString()); + parent::loadObject(); + } } protected function addOptionalHostTabs() diff --git a/application/forms/DeploymentLinkForm.php b/application/forms/DeploymentLinkForm.php index 044dd0868..f42a627f1 100644 --- a/application/forms/DeploymentLinkForm.php +++ b/application/forms/DeploymentLinkForm.php @@ -82,7 +82,7 @@ public function setup() ); } - $this->setAttrib('class', 'inline'); + $this->setAttrib('class', 'gipfl-inline-form'); $this->addHtml(Icon::create('wrench')); try { // As this is shown for single objects, ignore errors caused by an diff --git a/application/forms/ImportRowModifierForm.php b/application/forms/ImportRowModifierForm.php index 5626a45f8..9e53bd94a 100644 --- a/application/forms/ImportRowModifierForm.php +++ b/application/forms/ImportRowModifierForm.php @@ -163,10 +163,10 @@ protected function addSettings($class = null) if ($class !== null) { if (! class_exists($class)) { - throw new RuntimeException( + throw new RuntimeException(sprintf( 'The hooked class "%s" for this property modifier does no longer exist', $class - ); + )); } $class::addSettingsFormFields($this); diff --git a/application/forms/KickstartForm.php b/application/forms/KickstartForm.php index b7a6b0840..0079cfbc2 100644 --- a/application/forms/KickstartForm.php +++ b/application/forms/KickstartForm.php @@ -320,7 +320,7 @@ protected function storeResourceConfig() ) ); $this->addHtmlHint( - Html::tag('pre', null, $config), + Html::tag('pre', null, (string) $config), array('name' => 'HINT_config_store') ); diff --git a/application/forms/SyncCheckForm.php b/application/forms/SyncCheckForm.php index 28ab5e275..8fb3bd0ca 100644 --- a/application/forms/SyncCheckForm.php +++ b/application/forms/SyncCheckForm.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Forms; +use Icinga\Module\Director\Objects\DirectorActivityLog; use Icinga\Module\Director\Objects\SyncRule; use Icinga\Module\Director\Web\Form\DirectorForm; @@ -31,16 +32,20 @@ public function onSuccess() $this->notifySuccess( $this->translate(('This Sync Rule would apply new changes')) ); - $sum = array('create' => 0, 'modify' => 0, 'delete' => 0); + $sum = [ + DirectorActivityLog::ACTION_CREATE => 0, + DirectorActivityLog::ACTION_MODIFY => 0, + DirectorActivityLog::ACTION_DELETE => 0 + ]; // TODO: Preview them? Like "hosta, hostb and 4 more would be... foreach ($this->rule->getExpectedModifications() as $object) { if ($object->shouldBeRemoved()) { - $sum['delete']++; + $sum[DirectorActivityLog::ACTION_DELETE]++; } elseif (! $object->hasBeenLoadedFromDb()) { - $sum['create']++; + $sum[DirectorActivityLog::ACTION_CREATE]++; } elseif ($object->hasBeenModified()) { - $sum['modify']++; + $sum[DirectorActivityLog::ACTION_MODIFY]++; } } diff --git a/doc/60-CLI.md b/doc/60-CLI.md index 284ee2e53..5d3524460 100644 --- a/doc/60-CLI.md +++ b/doc/60-CLI.md @@ -75,6 +75,12 @@ icingacli director host create localhost \ --json '{ "address": "127.0.0.1", "vars": { "test": [ "one", "two" ] } }' ``` +Passing JSON via STDIN is also possible: + +```shell +icingacli director host create localhost --json < my-host.json +``` + ### Delete a specific object @@ -137,6 +143,7 @@ Use this command to modify specific properties of an existing Icinga object. | `--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 | +| `--allow-overrides` | Set variable overrides for virtual Services | #### Examples @@ -184,16 +191,16 @@ in JSON format. #### Options -| Option | Description | -|-----------------|---------------------------------------------------------| -| `--resolved` | Resolve all inherited properties and show a flat object | -| | 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 skips null or default values | -| | With this flag you will get all properties | - +| Option | Description | +|-------------------|------------------------------------------------------| +| `--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 skips null or default values | +| | With this flag you will get all properties | +| `--with-services` | For hosts only, also shows attached services | ### Clone an existing object @@ -464,7 +471,7 @@ Config with checksum b330febd0820493fb12921ad8f5ea42102a5c871 already exists | Option | Description | |----------------------------|------------------------------------------------------------------| -| `checksum ` | Optionally deploy a specific configuration | +| `--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 | diff --git a/doc/70-REST-API.md b/doc/70-REST-API.md index 9a7ad7c8b..dd5d26697 100644 --- a/doc/70-REST-API.md +++ b/doc/70-REST-API.md @@ -39,12 +39,15 @@ URL scheme and supported methods We support GET, POST, PUT and DELETE. -| Method | Meaning -| ------ | ------------------------------------------------------------ -| GET | Read / fetch data. Not allowed to run operations with the potential to cause any harm -| POST | Trigger actions, create or modify objects. Can also be used to partially modify objects -| PUT | Creates or replaces objects, cannot be used to modify single object properties -| DELETE | Remove a specific object +| Method | Meaning | +|--------|---------------------------------------------------------------------| +| GET | Read / fetch data. Not allowed to run operations with the potential | +| | to cause any harm | +| POST | Trigger actions, create or modify objects. Can also be used to | +| | partially modify objects | +| PUT | Creates or replaces objects, cannot be used to modify single object | +| | properties | +| DELETE | Remove a specific object | TODO: more examples showing the difference between POST and PUT @@ -113,6 +116,15 @@ Icinga Objects ### Special parameters +| Parameter | Description | +|----------------|-------------------------------------------------------------| +| resolved | Resolve all inherited properties and show a flat object | +| withNull | Retrieve default (null) properties also | +| withServices | Show services attached to a host. `resolved` and `withNull` | +| | are applied for services too | +| allowOverrides | Set variable overrides for virtual Services | +| showStacktrace | Returns the related stack trace, in case an error occurs | + #### Resolve object properties In case you add the `resolved` parameter to your URL, all inherited object @@ -121,15 +133,20 @@ properties will be resolved. Such a URL could look as follows: director/host?name=hostname.example.com&resolved -#### Retrieve all properties - -TODO: adjust the code to fix this, current implementation has `withNull` +#### Retrieve default (null) properties also Per default properties with `null` value are skipped when shipping a result. -You can influence this behavior with the properties parameter. Just append -`properties=ALL` to your URL: +You can influence this behavior with the `properties` parameter. Just append +`&withNull` to your URL: + + director/host?name=hostname.example.com&withNull + - director/host?name=hostname.example.com&properties=all +#### Fetch host with it's services + +This is what the `withServices` parameter exists: + + director/host?name=hostname.example.com&withServices #### Retrieve only specific properties @@ -141,6 +158,23 @@ when they have no (`null`) value: director/host?name=hostname.example.com&properties=object_name,address,vars +#### Override vars for inherited/applied Services + +Enabling `allowOverrides` allows you to let Director figure out, whether your +modified Custom Variables need to be applied to a specific individual Service, +or whether setting Overrides at Host level is the way to go. + + POST director/service?name=Uptime&host=hostname.example.com&allowOverrices + +```json +{ "vars.uptime_warning": 300 } +``` + +In case `Uptime` is an Apply Rule, calling this without `allowOverrides` will +trigger a 404 response. Please note that when modifying the Host object, the +body for response 200 will show the Host object, as that's the one that has +been modified. + ### Example GET director/host?name=pe2015.example.com diff --git a/doc/82-Changelog.md b/doc/82-Changelog.md index cfe922bc9..9cf35591b 100644 --- a/doc/82-Changelog.md +++ b/doc/82-Changelog.md @@ -4,6 +4,35 @@ Please make sure to always read our [Upgrading](05-Upgrading.md) documentation before switching to a new version. +v1.10.0 (unreleased) +-------------------- + +### Fixed issues +* You can find issues and feature requests related to this release on our + [roadmap](https://github.com/Icinga/icingaweb2-module-director/milestone/27?closed=1) + +### User Interface +* FIX: links from Service Previews (Icinga DSL) to templates (#2554) + +### Import and Sync +* FEATURE: clone a row for nested Dictionary/Hash entries (#2555) +* FEATURE: Sync in "override" mode now preserves Self Service API keys (#2590) + +### Configuration Baskets +* BREAKING: configuration baskets no longer contain originalId (#2549) +* FEATURE: exporting/snapshot-logic has been centralized (#2549) + +### REST API +* FIX: addressing service templates by name has been fixed (#2487) +* FEATURE: Stack traces can now be requested (#2570) + +### CLI +* FIX: config deploy doesn't try to wait in case of no deployment (#2522) +* FEATURE: improved wording for deployment error messages (#2523) +* FEATURE: JSON can now be shipped via STDIN (#1570) +* FEATURE: improved readability for some error messages (#2567) +* FEATURE: allows showing hosts with their services (#2565) + 1.9.1 ----- diff --git a/library/Director/Cli/Command.php b/library/Director/Cli/Command.php index fc878d4b6..69d61b1d8 100644 --- a/library/Director/Cli/Command.php +++ b/library/Director/Cli/Command.php @@ -2,12 +2,12 @@ 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\Exception\JsonException; use Icinga\Module\Director\Objects\IcingaEndpoint; use Icinga\Application\Config; use RuntimeException; @@ -31,15 +31,17 @@ protected function renderJson($object, $pretty = true) */ protected function parseJson($json) { - $res = json_decode($json); - - if ($res === null) { - $this->fail('Invalid JSON: %s', $this->getLastJsonError()); + try { + return JsonString::decode($json); + } catch (JsonDecodeException $e) { + $this->fail('Invalid JSON: %s', $e->getMessage()); } - - return $res; } + /** + * @param string $msg + * @return never-return + */ public function fail($msg) { $args = func_get_args(); @@ -47,16 +49,8 @@ public function fail($msg) if (count($args)) { $msg = vsprintf($msg, $args); } - - throw new RuntimeException($msg); - } - - /** - * @return string - */ - protected function getLastJsonError() - { - return JsonException::getJsonErrorMessage(json_last_error()); + echo $this->screen->colorize("ERROR", 'red') . ": $msg\n"; + exit(1); } /** diff --git a/library/Director/Cli/ObjectCommand.php b/library/Director/Cli/ObjectCommand.php index c8a876d8c..ca68213d3 100644 --- a/library/Director/Cli/ObjectCommand.php +++ b/library/Director/Cli/ObjectCommand.php @@ -4,6 +4,11 @@ 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; @@ -34,28 +39,58 @@ public function init() * * 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 + * --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(); - if ($this->params->shift('resolved')) { - $object = $object::fromPlainObject($object->toPlainObject(true), $db); + $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')) { - $noDefaults = $this->params->shift('no-defaults', false); - $data = $object->toPlainObject(false, $noDefaults); - echo $this->renderJson($data, !$this->params->shift('no-pretty')); + echo $this->renderJson($exporter->export($object), !$this->params->shift('no-pretty')); } else { - echo $object; + $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; + } } } @@ -87,36 +122,9 @@ public function showAction() public function createAction() { $type = $this->getType(); - $name = $this->params->shift(); - - $props = $this->remainingParams(); - if (! array_key_exists('object_type', $props)) { - $props['object_type'] = 'object'; - } - - 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; - } - } - - if (! array_key_exists('object_name', $props)) { - $this->fail('Cannot create an object with at least an object name'); - } - - $object = IcingaObject::createByType( - $type, - $props, - $this->db() - ); + $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); @@ -181,33 +189,38 @@ public function createAction() public function setAction() { $name = $this->getName(); + $type = $this->getType(); if ($this->params->shift('auto-create') && ! $this->exists($name)) { $action = 'created'; - $object = $this->create($name); + $object = $this->create($type, $name); } else { $action = 'modified'; $object = $this->getObject(); } - $appends = $this->stripPrefixedProperties($this->params, 'append-'); - $remove = $this->stripPrefixedProperties($this->params, 'remove-'); + $appends = self::stripPrefixedProperties($this->params, 'append-'); + $remove = self::stripPrefixedProperties($this->params, 'remove-'); if ($this->params->shift('replace')) { - $new = $this->create($name)->setProperties($this->remainingParams()); - $object->replaceWith($new); + $object->replaceWith($this->create($type, $name, $this->remainingParams())); } else { $object->setProperties($this->remainingParams()); } - $this->appendToArrayProperties($object, $appends); - $this->removeProperties($object, $remove); + 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", $this->getType(), $this->name, $action); + printf("%s '%s' has been %s\n", $type, $name, $action); exit(0); } - printf("%s '%s' has not been modified\n", $this->getType(), $name); + printf("%s '%s' has not been modified\n", $type, $name); exit(0); } @@ -326,58 +339,7 @@ public function cloneAction() exit(0); } - protected function appendToArrayProperties(IcingaObject $object, $properties) - { - foreach ($properties as $key => $value) { - $current = $object->$key; - if ($current === null) { - $current = [$value]; - } elseif (is_array($current)) { - $current[] = $value; - } else { - throw new InvalidArgumentException(sprintf( - 'I can only append to arrays, %s is %s', - $key, - var_export($current, 1) - )); - } - - $object->$key = $current; - } - } - - protected function removeProperties(IcingaObject $object, $properties) - { - foreach ($properties as $key => $value) { - if ($value === true) { - $object->$key = null; - } - $current = $object->$key; - if ($current === null) { - continue; - } elseif (is_array($current)) { - $new = []; - foreach ($current as $item) { - if ($item !== $value) { - $new[] = $item; - } - } - $object->$key = $new; - } elseif (is_string($current)) { - if ($current === $value) { - $object->$key = null; - } - } else { - throw new InvalidArgumentException(sprintf( - 'I can only remove strings or from arrays, %s is %s', - $key, - var_export($current, 1) - )); - } - } - } - - protected function stripPrefixedProperties(Params $params, $prefix = 'append-') + protected static function stripPrefixedProperties(Params $params, $prefix = 'append-') { $appends = []; $len = strlen($prefix); @@ -395,6 +357,37 @@ protected function stripPrefixedProperties(Params $params, $prefix = 'append-') 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(); @@ -412,12 +405,36 @@ protected function shiftOneOrMoreNames() 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( @@ -436,16 +453,12 @@ protected function load($name) ); } - protected function create($name) + protected function create($type, $name, $properties = []) { - return IcingaObject::createByType( - $this->getType(), - array( - 'object_type' => 'object', - 'object_name' => $name - ), - $this->db() - ); + return IcingaObject::createByType($type, $properties + [ + 'object_type' => 'object', + 'object_name' => $name + ], $this->db()); } /** diff --git a/library/Director/Data/Db/DbConnection.php b/library/Director/Data/Db/DbConnection.php index 27db59c76..d85b9f02b 100644 --- a/library/Director/Data/Db/DbConnection.php +++ b/library/Director/Data/Db/DbConnection.php @@ -20,15 +20,19 @@ public function isPgsql() public function quoteBinary($binary) { - if ($binary instanceof Zend_Db_Expr) { - throw new RuntimeException('Trying to escape binary twice'); + if ($binary === '') { + return ''; + } + + if (is_array($binary)) { + return array_map([$this, 'quoteBinary'], $binary); } if ($this->isPgsql()) { return new Zend_Db_Expr("'\\x" . bin2hex($binary) . "'"); } - return $binary; + return new Zend_Db_Expr('0x' . bin2hex($binary)); } public function binaryDbResult($value) diff --git a/library/Director/Data/Exporter.php b/library/Director/Data/Exporter.php new file mode 100644 index 000000000..79814a725 --- /dev/null +++ b/library/Director/Data/Exporter.php @@ -0,0 +1,302 @@ +connection = $connection; + $this->db = $connection->getDbAdapter(); + $this->fieldReferenceLoader = new FieldReferenceLoader($connection); + } + + public function export(DbObject $object) + { + $props = $object instanceof IcingaObject + ? $this->exportIcingaObject($object) + : $this->exportDbObject($object); + + ImportExportDeniedProperties::strip($props, $object, $this->showIds); + $this->appendTypeSpecificRelations($props, $object); + + if ($this->chosenProperties !== null) { + $chosen = []; + foreach ($this->chosenProperties as $k) { + if (array_key_exists($k, $props)) { + $chosen[$k] = $props[$k]; + } + } + + $props = $chosen; + } + + ksort($props); + return (object) $props; + } + + public function enableHostServices($enable = true) + { + $this->exportHostServices = $enable; + return $this; + } + + public function showDefaults($show = true) + { + $this->showDefaults = $show; + return $this; + } + + public function showIds($show = true) + { + $this->showIds = $show; + return $this; + } + + public function resolveObjects($resolve = true) + { + $this->resolveObjects = $resolve; + if ($this->serviceLoader) { + $this->serviceLoader->resolveObjects($resolve); + } + + return $this; + } + + public function filterProperties(array $properties) + { + $this->chosenProperties = $properties; + return $this; + } + + protected function appendTypeSpecificRelations(array &$props, DbObject $object) + { + if ($object instanceof DirectorDatalist) { + $props['entries'] = $this->exportDatalistEntries($object); + } elseif ($object instanceof DirectorDatafield) { + if (isset($props['settings']->datalist_id)) { + $props['settings']->datalist = $this->getDatalistNameForId($props['settings']->datalist_id); + unset($props['settings']->datalist_id); + } + + $props['category'] = isset($props['category_id']) + ? $this->getDatafieldCategoryNameForId($props['category_id']) + : null; + unset($props['category_id']); + } elseif ($object instanceof ImportSource) { + $props['modifiers'] = $this->exportRowModifiers($object); + } elseif ($object instanceof SyncRule) { + $props['properties'] = $this->exportSyncProperties($object); + } elseif ($object instanceof IcingaCommand) { + if (isset($props['arguments'])) { + foreach ($props['arguments'] as $key => $argument) { + if (property_exists($argument, 'command_id')) { + unset($props['arguments'][$key]->command_id); + } + } + } + } elseif ($object instanceof DirectorJob) { + if ($object->hasTimeperiod()) { + $props['timeperiod'] = $object->timeperiod()->getObjectName(); + } + unset($props['timeperiod_id']); + } elseif ($object instanceof IcingaTemplateChoice) { + if (isset($props['required_template_id'])) { + $requiredId = $props['required_template_id']; + unset($props['required_template_id']); + $props = $this->loadTemplateName($object->getObjectTableName(), $requiredId); + } + + $props['members'] = array_values($object->getMembers()); + } elseif ($object instanceof IcingaServiceSet) { + if ($object->get('host_id')) { + // Sets on Host + throw new RuntimeException('Not yet'); + } + $props['services'] = []; + foreach ($object->getServiceObjects() as $serviceObject) { + $props['services'][$serviceObject->getObjectName()] = $this->export($serviceObject); + } + ksort($props['services']); + } elseif ($object instanceof IcingaHost) { + if ($this->exportHostServices) { + $services = []; + foreach ($this->serviceLoader()->fetchServicesForHost($object) as $service) { + $services[] = $this->export($service); + } + + $props['services'] = $services; + } + } + } + + public function serviceLoader() + { + if ($this->serviceLoader === null) { + $this->serviceLoader = new HostServiceLoader($this->connection); + $this->serviceLoader->resolveObjects($this->resolveObjects); + } + + return $this->serviceLoader; + } + + protected function loadTemplateName($table, $id) + { + $db = $this->db; + $query = $db->select() + ->from(['o' => $table], 'o.object_name')->where("o.object_type = 'template'") + ->where('o.id = ?', $id); + + return $db->fetchOne($query); + } + + protected function getDatalistNameForId($id) + { + $db = $this->db; + $query = $db->select()->from('director_datalist', 'list_name')->where('id = ?', (int) $id); + return $db->fetchOne($query); + } + + protected function getDatafieldCategoryNameForId($id) + { + $db = $this->db; + $query = $db->select()->from('director_datafield_category', 'category_name')->where('id = ?', (int) $id); + return $db->fetchOne($query); + } + + protected function exportRowModifiers(ImportSource $object) + { + $modifiers = []; + // Hint: they're sorted by priority + foreach ($object->fetchRowModifiers() as $modifier) { + $modifiers[] = $this->export($modifier); + } + + return $modifiers; + } + + public function exportSyncProperties(SyncRule $object) + { + $all = []; + $db = $this->db; + $sourceNames = $db->fetchPairs( + $db->select()->from('import_source', ['id', 'source_name']) + ); + + foreach ($object->getSyncProperties() as $property) { + $properties = $property->getProperties(); + $properties['source'] = $sourceNames[$properties['source_id']]; + unset($properties['id']); + unset($properties['rule_id']); + unset($properties['source_id']); + ksort($properties); + $all[] = (object) $properties; + } + + return $all; + } + + /** + * @param DbObject $object + * @return array + */ + protected function exportDbObject(DbObject $object) + { + $props = $object->getProperties(); + if ($object instanceof DbObjectWithSettings) { + if ($object instanceof InstantiatedViaHook) { + $props['settings'] = (object) $object->getInstance()->exportSettings(); + } else { + $props['settings'] = (object) $object->getSettings(); // Already sorted + } + } + if (! $this->showDefaults) { + foreach ($props as $key => $value) { + // We assume NULL as a default value for all non-IcingaObject properties + if ($value === null) { + unset($props[$key]); + } + } + } + + return $props; + } + + /** + * @param IcingaObject $object + * @return array + * @throws \Icinga\Exception\NotFoundError + */ + protected function exportIcingaObject(IcingaObject $object) + { + $props = (array) $object->toPlainObject($this->resolveObjects, !$this->showDefaults); + if ($object->supportsFields()) { + $props['fields'] = $this->fieldReferenceLoader->loadFor($object); + } + + return $props; + } + + protected function exportDatalistEntries(DirectorDatalist $list) + { + $entries = []; + $id = $list->get('id'); + if ($id === null) { + return $entries; + } + + $dbEntries = DirectorDatalistEntry::loadAllForList($list); + // Hint: they are loaded with entry_name key + ksort($dbEntries); + + foreach ($dbEntries as $entry) { + if ($entry->shouldBeRemoved()) { + continue; + } + $plainEntry = $entry->getProperties(); + unset($plainEntry['list_id']); + + $entries[] = $plainEntry; + } + + return $entries; + } +} diff --git a/library/Director/Data/FieldReferenceLoader.php b/library/Director/Data/FieldReferenceLoader.php new file mode 100644 index 000000000..1e3d92e16 --- /dev/null +++ b/library/Director/Data/FieldReferenceLoader.php @@ -0,0 +1,51 @@ +db = $connection->getDbAdapter(); + } + + /** + * @param int $id + * @return array + */ + public function loadFor(IcingaObject $object) + { + $db = $this->db; + $id = $object->get('id'); + if ($id === null) { + return []; + } + $type = $object->getShortTableName(); + $res = $db->fetchAll( + $db->select()->from(['f' => "icinga_${type}_field"], [ + 'f.datafield_id', + 'f.is_required', + 'f.var_filter', + ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', []) + ->where("${type}_id = ?", (int) $id) + ->order('varname ASC') + ); + + if (empty($res)) { + return []; + } + + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + + return $res; + } +} diff --git a/library/Director/Data/HostServiceLoader.php b/library/Director/Data/HostServiceLoader.php new file mode 100644 index 000000000..4cc4b961f --- /dev/null +++ b/library/Director/Data/HostServiceLoader.php @@ -0,0 +1,170 @@ +connection = $connection; + $this->db = $connection->getDbAdapter(); + } + + public function fetchServicesForHost(IcingaHost $host) + { + $table = (new ObjectsTableService($this->connection))->setHost($host); + $services = $this->fetchServicesForTable($table); + if ($this->resolveHostServices) { + foreach ($this->fetchAllServicesForHost($host) as $service) { + $services[] = $service; + } + } + + return $services; + } + + public function resolveHostServices($enable = true) + { + $this->resolveHostServices = $enable; + return $this; + } + + public function resolveObjects($resolve = true) + { + $this->resolveObjects = $resolve; + return $this; + } + + protected function fetchAllServicesForHost(IcingaHost $host) + { + $services = []; + /** @var IcingaHost[] $parents */ + $parents = IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true); + foreach ($parents as $parent) { + $table = (new ObjectsTableService($this->connection)) + ->setHost($parent) + ->setInheritedBy($host); + foreach ($this->fetchServicesForTable($table) as $service) { + $services[] = $service; + } + } + + foreach ($this->getHostServiceSetTables($host) as $table) { + foreach ($this->fetchServicesForTable($table) as $service) { + $services[] = $service; + } + } + foreach ($parents as $parent) { + foreach ($this->getHostServiceSetTables($parent, $host) as $table) { + foreach ($this->fetchServicesForTable($table) as $service) { + $services[] = $service; + } + } + } + + $appliedSets = AppliedServiceSetLoader::fetchForHost($host); + foreach ($appliedSets as $set) { + $table = IcingaServiceSetServiceTable::load($set) + // ->setHost($host) + ->setAffectedHost($host); + foreach ($this->fetchServicesForTable($table) as $service) { + $services[] = $service; + } + } + + $table = IcingaHostAppliedServicesTable::load($host); + foreach ($this->fetchServicesForTable($table) as $service) { + $services[] = $service; + } + + return $services; + } + + /** + * Duplicates Logic in HostController + * + * @param IcingaHost $host + * @param IcingaHost|null $affectedHost + * @return IcingaServiceSetServiceTable[] + */ + protected function getHostServiceSetTables(IcingaHost $host, IcingaHost $affectedHost = null) + { + $tables = []; + $db = $this->connection; + if ($affectedHost === null) { + $affectedHost = $host; + } + if ($host->get('id') === null) { + return $tables; + } + + $query = $db->getDbAdapter()->select() + ->from(['ss' => 'icinga_service_set'], 'ss.*') + ->join(['hsi' => 'icinga_service_set_inheritance'], 'hsi.parent_service_set_id = ss.id', []) + ->join(['hs' => 'icinga_service_set'], 'hs.id = hsi.service_set_id', []) + ->where('hs.host_id = ?', $host->get('id')); + + $sets = IcingaServiceSet::loadAll($db, $query, 'object_name'); + /** @var IcingaServiceSet $set*/ + foreach ($sets as $name => $set) { + $tables[] = IcingaServiceSetServiceTable::load($set) + ->setHost($host) + ->setAffectedHost($affectedHost); + } + + return $tables; + } + + protected function fetchServicesForTable(QueryBasedTable $table) + { + $query = $table->getQuery(); + if ($query instanceof Select || $query instanceof Zend_Db_Select) { + // What about SimpleQuery? IcingaHostAppliedServicesTable with branch in place? + $query->reset(Select::LIMIT_COUNT); + $query->reset(Select::LIMIT_OFFSET); + $rows = $this->db->fetchAll($query); + } elseif ($query instanceof SimpleQuery) { + $rows = $query->fetchAll(); + } else { + throw new RuntimeException('Table query needs to be either a Select or a SimpleQuery instance'); + } + $services = []; + foreach ($rows as $row) { + $service = IcingaService::loadWithUniqueId(Uuid::fromBytes($row->uuid), $this->connection); + if ($this->resolveObjects) { + $service = $service::fromPlainObject($service->toPlainObject(true), $this->connection); + } + $services[] = $service; + } + + return $services; + } +} diff --git a/library/Director/Data/ImportExportDeniedProperties.php b/library/Director/Data/ImportExportDeniedProperties.php new file mode 100644 index 000000000..747eb0fc8 --- /dev/null +++ b/library/Director/Data/ImportExportDeniedProperties.php @@ -0,0 +1,52 @@ + [ + 'last_attempt_succeeded', + 'last_error_message', + 'ts_last_attempt', + 'ts_last_error', + ], + ImportSource::class => [ + // No state export + 'import_state', + 'last_error_message', + 'last_attempt', + ], + ImportRowModifier::class => [ + // Not state, but to be removed: + 'source_id', + ], + SyncRule::class => [ + 'sync_state', + 'last_error_message', + 'last_attempt', + ], + ]; + + public static function strip(array &$props, DbObject $object, $showIds = false) + { + // TODO: this used to exist. Double-check all imports to verify it's not in use + // $originalId = $props['id']; + + if (! $showIds) { + unset($props['id']); + } + $class = get_class($object); + if (isset(self::$denyProperties[$class])) { + foreach (self::$denyProperties[$class] as $key) { + unset($props[$key]); + } + } + } +} diff --git a/library/Director/Data/PropertyMangler.php b/library/Director/Data/PropertyMangler.php new file mode 100644 index 000000000..a457f1dfd --- /dev/null +++ b/library/Director/Data/PropertyMangler.php @@ -0,0 +1,60 @@ + $value) { + $current = $object->$key; + if ($current === null) { + $current = [$value]; + } elseif (is_array($current)) { + $current[] = $value; + } else { + throw new InvalidArgumentException(sprintf( + 'I can only append to arrays, %s is %s', + $key, + var_export($current, 1) + )); + } + + $object->$key = $current; + } + } + + public static function removeProperties(IcingaObject $object, $properties) + { + foreach ($properties as $key => $value) { + if ($value === true) { + $object->$key = null; + } + $current = $object->$key; + if ($current === null) { + continue; + } elseif (is_array($current)) { + $new = []; + foreach ($current as $item) { + if ($item !== $value) { + $new[] = $item; + } + } + $object->$key = $new; + } elseif (is_string($current)) { + if ($current === $value) { + $object->$key = null; + } + } else { + throw new InvalidArgumentException(sprintf( + 'I can only remove strings or from arrays, %s is %s', + $key, + var_export($current, 1) + )); + } + } + } +} diff --git a/library/Director/DataType/DataTypeDatalist.php b/library/Director/DataType/DataTypeDatalist.php index 5c5d72dc3..354c7c377 100644 --- a/library/Director/DataType/DataTypeDatalist.php +++ b/library/Director/DataType/DataTypeDatalist.php @@ -59,7 +59,9 @@ public function getFormElement($name, QuickForm $form) $db = $form->getDb(); foreach ($value as $entry) { - $this->createEntryIfNotExists($db, $listId, $entry); + if ($entry !== '') { + $this->createEntryIfNotExists($db, $listId, $entry); + } } }); } diff --git a/library/Director/Db/Branch/BranchActivity.php b/library/Director/Db/Branch/BranchActivity.php index 19b8391c2..097af2682 100644 --- a/library/Director/Db/Branch/BranchActivity.php +++ b/library/Director/Db/Branch/BranchActivity.php @@ -9,6 +9,7 @@ use Icinga\Module\Director\Data\Json; use Icinga\Module\Director\Data\SerializableValue; use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\DirectorActivityLog; use Icinga\Module\Director\Objects\IcingaObject; use InvalidArgumentException; use Ramsey\Uuid\Uuid; @@ -19,9 +20,9 @@ class BranchActivity { const DB_TABLE = 'director_branch_activity'; - const ACTION_CREATE = 'create'; - const ACTION_MODIFY = 'modify'; - const ACTION_DELETE = 'delete'; + const ACTION_CREATE = DirectorActivityLog::ACTION_CREATE; + const ACTION_MODIFY = DirectorActivityLog::ACTION_MODIFY; + const ACTION_DELETE = DirectorActivityLog::ACTION_DELETE; /** @var int */ protected $timestampNs; diff --git a/library/Director/Db/Branch/UuidLookup.php b/library/Director/Db/Branch/UuidLookup.php index aa49d3f4d..c56996a79 100644 --- a/library/Director/Db/Branch/UuidLookup.php +++ b/library/Director/Db/Branch/UuidLookup.php @@ -8,7 +8,6 @@ use Icinga\Module\Director\Objects\IcingaServiceSet; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; -use RuntimeException; use function is_int; use function is_resource; use function is_string; @@ -22,17 +21,21 @@ class UuidLookup * @param int|string $key * @param IcingaHost|null $host * @param IcingaServiceSet $set + * @return ?UuidInterface */ public static function findServiceUuid( Db $connection, Branch $branch, - $objectType, + $objectType = null, $key = null, IcingaHost $host = null, IcingaServiceSet $set = null ) { $db = $connection->getDbAdapter(); - $query = $db->select()->from('icinga_service', 'uuid')->where('object_type = ?', $objectType); + $query = $db->select()->from('icinga_service', 'uuid'); + if ($objectType) { + $query->where('object_type = ?', $objectType); + } $query = self::addKeyToQuery($connection, $query, $key); if ($host) { $query->where('host_id = ?', $host->get('id')); diff --git a/library/Director/Db/DbUtil.php b/library/Director/Db/DbUtil.php index b80c61e59..f98e213e7 100644 --- a/library/Director/Db/DbUtil.php +++ b/library/Director/Db/DbUtil.php @@ -2,6 +2,14 @@ namespace Icinga\Module\Director\Db; +use gipfl\ZfDb\Adapter\Adapter; +use gipfl\ZfDb\Adapter\Pdo\Pgsql; +use gipfl\ZfDb\Expr; +use Zend_Db_Adapter_Abstract; +use Zend_Db_Adapter_Pdo_Pgsql; +use Zend_Db_Expr; +use function bin2hex; +use function is_array; use function is_resource; use function stream_get_contents; @@ -15,4 +23,74 @@ public static function binaryResult($value) return $value; } + + + /** + * @param string|array $binary + * @param Zend_Db_Adapter_Abstract $db + * @return Zend_Db_Expr|Zend_Db_Expr[] + */ + public static function quoteBinaryLegacy($binary, $db) + { + if (is_array($binary)) { + return static::quoteArray($binary, 'quoteBinaryLegacy', $db); + } + + if ($binary === null) { + return null; + } + + if ($db instanceof Zend_Db_Adapter_Pdo_Pgsql) { + return new Zend_Db_Expr("'\\x" . bin2hex($binary) . "'"); + } + + return new Zend_Db_Expr('0x' . bin2hex($binary)); + } + + /** + * @param string|array $binary + * @param Adapter $db + * @return Expr|Expr[] + */ + public static function quoteBinary($binary, $db) + { + if (is_array($binary)) { + return static::quoteArray($binary, 'quoteBinary', $db); + } + + if ($binary === null) { + return null; + } + + if ($db instanceof Pgsql) { + return new Expr("'\\x" . bin2hex($binary) . "'"); + } + + return new Expr('0x' . bin2hex($binary)); + } + + /** + * @param string|array $binary + * @param Adapter|Zend_Db_Adapter_Abstract $db + * @return Expr|Zend_Db_Expr|Expr[]|Zend_Db_Expr[] + */ + public static function quoteBinaryCompat($binary, $db) + { + if ($db instanceof Adapter) { + return static::quoteBinary($binary, $db); + } + + return static::quoteBinaryLegacy($binary, $db); + } + + protected static function quoteArray($array, $method, $db) + { + $result = []; + foreach ($array as $bin) { + $quoted = static::$method($bin, $db); + $result[] = $quoted; + } + + return $result; + } } diff --git a/library/Director/Deployment/ConditionalDeployment.php b/library/Director/Deployment/ConditionalDeployment.php index 4dbd0bce8..0f64028fe 100644 --- a/library/Director/Deployment/ConditionalDeployment.php +++ b/library/Director/Deployment/ConditionalDeployment.php @@ -95,14 +95,14 @@ public function waitForStartupAfterDeploy(DirectorDeploymentLog $deploymentLog, continue; } if ($stageCollected === 'n') { - return 'stage has not been collected'; + return 'stage has not been collected (Icinga "lost" the deployment)'; } if ($deploymentFromDB->get('startup_succeeded') === 'y') { return true; } - return 'deployment failed during startup'; + return 'deployment failed during startup (usually a Configuration Error)'; } - return 'deployment timed out'; + return 'deployment timed out (while waiting for an Icinga restart)'; } /** diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php index acf6cdf2a..5dcb2d0ed 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshot.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Data\Exporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Objects\DirectorDatafield; @@ -423,10 +424,11 @@ public function getJsonDump() protected function addAll($typeName) { list($class, $filter) = static::getClassAndObjectTypeForType($typeName); + $connection = $this->getConnection(); + assert($connection instanceof Db); /** @var IcingaObject $dummy */ $dummy = $class::create(); - /** @var ExportInterface $object */ if ($dummy instanceof IcingaObject && $dummy->supportsImports()) { $db = $this->getDb(); $select = $db->select()->from($dummy->getTableName()); @@ -441,12 +443,13 @@ protected function addAll($typeName) ) { $select->where('object_type = ?', 'template'); } - $all = $class::loadAll($this->getConnection(), $select); + $all = $class::loadAll($connection, $select); } else { - $all = $class::loadAll($this->getConnection()); + $all = $class::loadAll($connection); } + $exporter = new Exporter($connection); foreach ($all as $object) { - $this->objects[$typeName][$object->getUniqueIdentifier()] = $object->export(); + $this->objects[$typeName][$object->getUniqueIdentifier()] = $exporter->export($object); } } @@ -461,7 +464,7 @@ protected function addByIdentifiers($typeName, $identifiers) * @param $typeName * @param $identifier * @param Db $connection - * @return ExportInterface|null + * @return ExportInterface|DbObject|null */ public static function instanceByIdentifier($typeName, $identifier, Db $connection) { @@ -490,13 +493,14 @@ protected function addByIdentifier($typeName, $identifier) { /** @var Db $connection */ $connection = $this->getConnection(); + $exporter = new Exporter($connection); $object = static::instanceByIdentifier( $typeName, $identifier, $connection ); if ($object !== null) { - $this->objects[$typeName][$identifier] = $object->export(); + $this->objects[$typeName][$identifier] = $exporter->export($object); } } } diff --git a/library/Director/DirectorObject/Automation/ExportInterface.php b/library/Director/DirectorObject/Automation/ExportInterface.php index 17efde2f2..275dfed92 100644 --- a/library/Director/DirectorObject/Automation/ExportInterface.php +++ b/library/Director/DirectorObject/Automation/ExportInterface.php @@ -7,6 +7,7 @@ interface ExportInterface { /** + * @deprecated * @return \stdClass */ public function export(); diff --git a/library/Director/DirectorObject/Automation/ImportExport.php b/library/Director/DirectorObject/Automation/ImportExport.php index 422eaeadc..a5e72fae3 100644 --- a/library/Director/DirectorObject/Automation/ImportExport.php +++ b/library/Director/DirectorObject/Automation/ImportExport.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\DirectorObject\Automation; +use Icinga\Module\Director\Data\Exporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\DirectorDatafield; use Icinga\Module\Director\Objects\DirectorDatalist; @@ -15,23 +16,26 @@ class ImportExport { + /** @var Db */ protected $connection; + /** @var Exporter */ + protected $exporter; + public function __construct(Db $connection) { $this->connection = $connection; + $this->exporter = new Exporter($connection); } public function serializeAllServiceSets() { - // TODO: Export host templates in Inheritance order $res = []; - $related = []; foreach (IcingaServiceSet::loadAll($this->connection) as $object) { - $res[] = $object->export(); - foreach ($object->exportRelated() as $key => $relatedObject) { - $related[$key] = $relatedObject; + if ($object->get('host_id')) { + continue; } + $res[] = $this->exporter->export($object); } return $res; @@ -41,7 +45,7 @@ public function serializeAllHostTemplateChoices() { $res = []; foreach (IcingaTemplateChoiceHost::loadAll($this->connection) as $object) { - $res[] = $object->export(); + $res[] = $this->exporter->export($object); } return $res; @@ -71,7 +75,7 @@ public function serializeAllDataFields() { $res = []; foreach (DirectorDatafield::loadAll($this->connection) as $object) { - $res[] = $object->export(); + $res[] = $this->exporter->export($object); } return $res; @@ -81,7 +85,7 @@ public function serializeAllDataLists() { $res = []; foreach (DirectorDatalist::loadAll($this->connection) as $object) { - $res[] = $object->export(); + $res[] = $this->exporter->export($object); } return $res; @@ -91,7 +95,7 @@ public function serializeAllJobs() { $res = []; foreach (DirectorJob::loadAll($this->connection) as $object) { - $res[] = $object->export(); + $res[] = $this->exporter->export($object); } return $res; @@ -101,7 +105,7 @@ public function serializeAllImportSources() { $res = []; foreach (ImportSource::loadAll($this->connection) as $object) { - $res[] = $object->export(); + $res[] = $this->exporter->export($object); } return $res; @@ -111,7 +115,7 @@ public function serializeAllSyncRules() { $res = []; foreach (SyncRule::loadAll($this->connection) as $object) { - $res[] = $object->export(); + $res[] = $this->exporter->export($object); } return $res; diff --git a/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php b/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php index 5b0f4ff9a..70a014fa3 100644 --- a/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php +++ b/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php @@ -7,6 +7,7 @@ use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\HostApplyMatches; use Icinga\Module\Director\Objects\IcingaHost; +use Ramsey\Uuid\UuidInterface; /** * A Service Apply Rule matching this Host, generating a Service with the given @@ -23,11 +24,15 @@ class AppliedServiceInfo implements ServiceInfo /** @var int */ protected $serviceApplyRuleId; - public function __construct($hostName, $serviceName, $serviceApplyRuleId) + /** @var UuidInterface */ + protected $uuid; + + public function __construct($hostName, $serviceName, $serviceApplyRuleId, UuidInterface $uuid) { $this->hostName = $hostName; $this->serviceName= $serviceName; $this->serviceApplyRuleId = $serviceApplyRuleId; + $this->uuid = $uuid; } public static function find(IcingaHost $host, $serviceName) @@ -36,7 +41,7 @@ public static function find(IcingaHost $host, $serviceName) $connection = $host->getConnection(); foreach (static::fetchApplyRulesByServiceName($connection, $serviceName) as $rule) { if ($matcher->matchesFilter($rule->filter)) { - return new static($host->getObjectName(), $serviceName, (int) $rule->id); + return new static($host->getObjectName(), $serviceName, (int) $rule->id, $rule->uuid); } } @@ -61,6 +66,11 @@ public function getName() return $this->serviceName; } + public function getUuid() + { + return $this->uuid; + } + public function getUrl() { return Url::fromPath('director/host/appliedservice', [ @@ -80,6 +90,7 @@ protected static function fetchApplyRulesByServiceName(Db $connection, $serviceN $query = $db->select() ->from(['s' => 'icinga_service'], [ 'id' => 's.id', + 'uuid' => 's.uuid', 'name' => 's.object_name', 'assign_filter' => 's.assign_filter', ]) diff --git a/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php b/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php index 44f400186..9aba8503e 100644 --- a/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php +++ b/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php @@ -7,6 +7,7 @@ use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\HostApplyMatches; use Icinga\Module\Director\Objects\IcingaHost; +use Ramsey\Uuid\UuidInterface; /** * A Service that makes part of a Service Set Apply Rule matching this Host, @@ -23,11 +24,15 @@ class AppliedServiceSetServiceInfo implements ServiceInfo /** @var string */ protected $serviceSetName; - public function __construct($hostName, $serviceName, $serviceSetName) + /** @var UuidInterface */ + protected $uuid; + + public function __construct($hostName, $serviceName, $serviceSetName, UuidInterface $uuid) { $this->hostName = $hostName; $this->serviceName = $serviceName; $this->serviceSetName = $serviceSetName; + $this->uuid = $uuid; } public static function find(IcingaHost $host, $serviceName) @@ -39,7 +44,8 @@ public static function find(IcingaHost $host, $serviceName) return new static( $host->getObjectName(), $serviceName, - $rule->service_set_name + $rule->service_set_name, + $rule->uuid ); } } @@ -52,6 +58,11 @@ public function getHostName() return $this->hostName; } + public function getUuid() + { + return $this->uuid; + } + /** * @return string */ diff --git a/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php b/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php index a542c387f..875d5fb7e 100644 --- a/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php +++ b/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php @@ -6,6 +6,7 @@ use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaService; use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use Ramsey\Uuid\UuidInterface; /** * A Service attached to a parent Service Template. This is a shortcut for @@ -22,24 +23,31 @@ class InheritedServiceInfo implements ServiceInfo /** @var string */ protected $serviceName; - public function __construct($hostName, $hostTemplateName, $serviceName) + /** @var UuidInterface */ + protected $uuid; + + public function __construct($hostName, $hostTemplateName, $serviceName, UuidInterface $uuid) { $this->hostName = $hostName; $this->hostTemplateName = $hostTemplateName; $this->serviceName= $serviceName; + $this->uuid = $uuid; } public static function find(IcingaHost $host, $serviceName) { + $db = $host->getConnection(); foreach (IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true) as $parent) { - if (IcingaService::exists([ + $key = [ 'host_id' => $parent->get('id'), 'object_name' => $serviceName - ], $host->getConnection())) { + ]; + if (IcingaService::exists($key, $db)) { return new static( $host->getObjectName(), $parent->getObjectName(), - $serviceName + $serviceName, + IcingaService::load($key, $db)->getUniqueId() ); } } @@ -52,6 +60,11 @@ public function getHostName() return $this->hostName; } + public function getUuid() + { + return $this->uuid; + } + /** * @return string */ diff --git a/library/Director/DirectorObject/Lookup/ServiceFinder.php b/library/Director/DirectorObject/Lookup/ServiceFinder.php index ee8bc7010..fb8d74cdc 100644 --- a/library/Director/DirectorObject/Lookup/ServiceFinder.php +++ b/library/Director/DirectorObject/Lookup/ServiceFinder.php @@ -6,13 +6,14 @@ use Icinga\Authentication\Auth; use Icinga\Module\Director\Objects\HostApplyMatches; use Icinga\Module\Director\Objects\IcingaHost; +use RuntimeException; class ServiceFinder { /** @var IcingaHost */ protected $host; - /** @var Auth */ + /** @var ?Auth */ protected $auth; /** @var IcingaHost[] */ @@ -24,7 +25,7 @@ class ServiceFinder /** @var \Icinga\Module\Director\Db */ protected $db; - public function __construct(IcingaHost $host, Auth $auth) + public function __construct(IcingaHost $host, Auth $auth = null) { $this->host = $host; $this->auth = $auth; @@ -55,6 +56,9 @@ public static function find(IcingaHost $host, $serviceName) */ public function getRedirectionUrl($serviceName) { + if ($this->auth === null) { + throw new RuntimeException('Auth is required for ServiceFinder when dealing when asking for URLs'); + } if ($this->auth->hasPermission('director/host')) { if ($info = $this::find($this->host, $serviceName)) { return $info->getUrl(); diff --git a/library/Director/DirectorObject/Lookup/ServiceInfo.php b/library/Director/DirectorObject/Lookup/ServiceInfo.php index 4ff024dff..3c8c51b94 100644 --- a/library/Director/DirectorObject/Lookup/ServiceInfo.php +++ b/library/Director/DirectorObject/Lookup/ServiceInfo.php @@ -4,6 +4,7 @@ use gipfl\IcingaWeb2\Url; use Icinga\Module\Director\Objects\IcingaHost; +use Ramsey\Uuid\UuidInterface; interface ServiceInfo { @@ -26,6 +27,11 @@ public function getHostName(); */ public function getUrl(); + /** + * @return UuidInterface + */ + public function getUuid(); + /** * @return bool */ diff --git a/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php b/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php index 8e0ee613f..a980da887 100644 --- a/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php +++ b/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php @@ -5,6 +5,8 @@ use gipfl\IcingaWeb2\Url; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; /** * A service belonging to a Service Set, attached either directly to the given @@ -21,11 +23,15 @@ class ServiceSetServiceInfo implements ServiceInfo /** @var string */ protected $serviceSetName; - public function __construct($hostName, $serviceName, $serviceSetName) + /** @var UuidInterface */ + protected $uuid; + + public function __construct($hostName, $serviceName, $serviceSetName, UuidInterface $uuid) { $this->hostName = $hostName; $this->serviceName = $serviceName; $this->serviceSetName = $serviceSetName; + $this->uuid = $uuid; } public static function find(IcingaHost $host, $serviceName) @@ -40,7 +46,10 @@ public static function find(IcingaHost $host, $serviceName) $query = $db->select() ->from( ['s' => 'icinga_service'], - ['service_set_name' => 'ss.object_name',] + [ + 'service_set_name' => 'ss.object_name', + 'uuid' => 's.uuid', + ] )->join( ['ss' => 'icinga_service_set'], 's.service_set_id = ss.id', @@ -62,7 +71,12 @@ public static function find(IcingaHost $host, $serviceName) ); if ($row = $db->fetchRow($query)) { - return new static($host->getObjectName(), $serviceName, $row->service_set_name); + return new static( + $host->getObjectName(), + $serviceName, + $row->service_set_name, + Uuid::fromBytes($row->uuid) + ); } return null; @@ -78,6 +92,11 @@ public function getName() return $this->serviceName; } + public function getUuid() + { + return $this->uuid; + } + /** * @return string */ diff --git a/library/Director/DirectorObject/Lookup/SingleServiceInfo.php b/library/Director/DirectorObject/Lookup/SingleServiceInfo.php index 3f22043bb..af54fc763 100644 --- a/library/Director/DirectorObject/Lookup/SingleServiceInfo.php +++ b/library/Director/DirectorObject/Lookup/SingleServiceInfo.php @@ -5,6 +5,7 @@ use gipfl\IcingaWeb2\Url; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaService; +use Ramsey\Uuid\UuidInterface; /** * A single service, directly attached to a Host Object. Overrides might @@ -21,11 +22,15 @@ class SingleServiceInfo implements ServiceInfo /** @var bool */ protected $useOverrides; - public function __construct($hostName, $serviceName, $useOverrides) + /** @var UuidInterface */ + protected $uuid; + + public function __construct($hostName, $serviceName, UuidInterface $uuid, $useOverrides) { $this->hostName = $hostName; $this->serviceName = $serviceName; $this->useOverrides = $useOverrides; + $this->uuid = $uuid; } public static function find(IcingaHost $host, $serviceName) @@ -36,10 +41,10 @@ public static function find(IcingaHost $host, $serviceName) ]; $connection = $host->getConnection(); if (IcingaService::exists($keyParams, $connection)) { - $useOverrides = IcingaService::load($keyParams, $connection) - ->getResolvedVar('use_var_overrides') === 'y'; + $service = IcingaService::load($keyParams, $connection); + $useOverrides = $service->getResolvedVar('use_var_overrides') === 'y'; - return new static($host->getObjectName(), $serviceName, $useOverrides); + return new static($host->getObjectName(), $serviceName, $service->getUniqueId(), $useOverrides); } return false; @@ -55,6 +60,14 @@ public function getName() return $this->serviceName; } + /** + * @return UuidInterface + */ + public function getUuid() + { + return $this->uuid; + } + public function getUrl() { return Url::fromPath('director/service/edit', [ diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php index 23d267d82..5704d7565 100644 --- a/library/Director/Import/Sync.php +++ b/library/Director/Import/Sync.php @@ -704,7 +704,13 @@ protected function refreshObject($key, $object) switch ($policy) { case 'override': - $this->objects[$key]->replaceWith($object); + if ($object instanceof IcingaHost + && !in_array('api_key', $this->rule->getSyncProperties()) + ) { + $this->objects[$key]->replaceWith($object, ['api_key']); + } else { + $this->objects[$key]->replaceWith($object); + } break; case 'merge': diff --git a/library/Director/Job/SyncJob.php b/library/Director/Job/SyncJob.php index fb81d64f0..0a5aa37f9 100644 --- a/library/Director/Job/SyncJob.php +++ b/library/Director/Job/SyncJob.php @@ -103,7 +103,7 @@ public static function addSettingsFormFields(QuickForm $form) ) )); - if (! strlen($form->getSentOrObjectValue('job_name'))) { + if ((string) $form->getSentOrObjectValue('job_name') !== '') { if (($ruleId = $form->getSentValue('rule_id')) && array_key_exists($ruleId, $rules)) { $name = sprintf('Sync job: %s', $rules[$ruleId]); $form->getElement('job_name')->setValue($name); diff --git a/library/Director/Objects/DirectorActivityLog.php b/library/Director/Objects/DirectorActivityLog.php index 9e00b0eca..ffc0c92e6 100644 --- a/library/Director/Objects/DirectorActivityLog.php +++ b/library/Director/Objects/DirectorActivityLog.php @@ -4,13 +4,19 @@ use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Db; -use Icinga\Module\Director\Util; use Icinga\Authentication\Auth; use Icinga\Application\Icinga; use Icinga\Application\Logger; class DirectorActivityLog extends DbObject { + const ACTION_CREATE = 'create'; + const ACTION_DELETE = 'delete'; + const ACTION_MODIFY = 'modify'; + + /** @deprecated */ + const AUDIT_REMOVE = 'remove'; + protected $table = 'director_activity_log'; protected $keyName = 'id'; @@ -104,25 +110,25 @@ public static function logCreation(IcingaObject $object, Db $db) $type = $object->getTableName(); $newProps = $object->toJson(null, true); - $data = array( + $data = [ 'object_name' => $name, - 'action_name' => 'create', + 'action_name' => self::ACTION_CREATE, 'author' => static::username(), 'object_type' => $type, 'new_properties' => $newProps, 'change_time' => date('Y-m-d H:i:s'), 'parent_checksum' => $db->getLastActivityChecksum() - ); + ]; $data['checksum'] = sha1(json_encode($data), true); $data['parent_checksum'] = hex2bin($data['parent_checksum']); - static::audit($db, array( - 'action' => 'create', + static::audit($db, [ + 'action' => self::ACTION_CREATE, 'object_type' => $type, 'object_name' => $name, 'new_props' => $newProps, - )); + ]); return static::create($data)->store($db); } @@ -134,27 +140,27 @@ public static function logModification(IcingaObject $object, Db $db) $oldProps = json_encode($object->getPlainUnmodifiedObject()); $newProps = $object->toJson(null, true); - $data = array( + $data = [ 'object_name' => $name, - 'action_name' => 'modify', + 'action_name' => self::ACTION_MODIFY, 'author' => static::username(), 'object_type' => $type, 'old_properties' => $oldProps, 'new_properties' => $newProps, 'change_time' => date('Y-m-d H:i:s'), 'parent_checksum' => $db->getLastActivityChecksum() - ); + ]; $data['checksum'] = sha1(json_encode($data), true); $data['parent_checksum'] = hex2bin($data['parent_checksum']); - static::audit($db, array( - 'action' => 'modify', + static::audit($db, [ + 'action' => self::ACTION_MODIFY, 'object_type' => $type, 'object_name' => $name, 'old_props' => $oldProps, 'new_props' => $newProps, - )); + ]); return static::create($data)->store($db); } @@ -165,45 +171,42 @@ public static function logRemoval(IcingaObject $object, Db $db) $type = $object->getTableName(); $oldProps = json_encode($object->getPlainUnmodifiedObject()); - $data = array( + $data = [ 'object_name' => $name, - 'action_name' => 'delete', + 'action_name' => self::ACTION_DELETE, 'author' => static::username(), 'object_type' => $type, 'old_properties' => $oldProps, 'change_time' => date('Y-m-d H:i:s'), 'parent_checksum' => $db->getLastActivityChecksum() - ); + ]; $data['checksum'] = sha1(json_encode($data), true); $data['parent_checksum'] = hex2bin($data['parent_checksum']); - static::audit($db, array( - 'action' => 'remove', + static::audit($db, [ + 'action' => self::AUDIT_REMOVE, 'object_type' => $type, 'object_name' => $name, 'old_props' => $oldProps - )); + ]); return static::create($data)->store($db); } public static function audit(Db $db, $properties) { - if ($db->settings()->enable_audit_log !== 'y') { + if ($db->settings()->get('enable_audit_log') !== 'y') { return; } - $log = array(); - $properties = array_merge( - array( - 'username' => static::username(), - 'address' => static::ip(), - ), - $properties - ); + $log = []; + $properties = array_merge([ + 'username' => static::username(), + 'address' => static::ip(), + ], $properties); - foreach ($properties as $key => & $val) { + foreach ($properties as $key => $val) { $log[] = "$key=" . json_encode($val); } diff --git a/library/Director/Objects/DirectorDatafieldCategory.php b/library/Director/Objects/DirectorDatafieldCategory.php index 7bf874e15..6cb4fb477 100644 --- a/library/Director/Objects/DirectorDatafieldCategory.php +++ b/library/Director/Objects/DirectorDatafieldCategory.php @@ -21,6 +21,7 @@ class DirectorDatafieldCategory extends DbObject ]; /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @return object */ public function export() diff --git a/library/Director/Objects/DirectorDatalist.php b/library/Director/Objects/DirectorDatalist.php index 8baf4b31d..ae5c983bf 100644 --- a/library/Director/Objects/DirectorDatalist.php +++ b/library/Director/Objects/DirectorDatalist.php @@ -185,6 +185,10 @@ public function onStore() } } + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return object + */ public function export() { $plain = (object) $this->getProperties(); diff --git a/library/Director/Objects/DirectorJob.php b/library/Director/Objects/DirectorJob.php index 0a0555bb2..bde1ed968 100644 --- a/library/Director/Objects/DirectorJob.php +++ b/library/Director/Objects/DirectorJob.php @@ -12,7 +12,7 @@ use Exception; use InvalidArgumentException; -class DirectorJob extends DbObjectWithSettings implements ExportInterface +class DirectorJob extends DbObjectWithSettings implements ExportInterface, InstantiatedViaHook { /** @var JobHook */ protected $job; @@ -55,9 +55,18 @@ public function getUniqueIdentifier() } /** + * @deprecated please use JobHook::getInstance() * @return JobHook */ public function job() + { + return $this->getInstance(); + } + + /** + * @return JobHook + */ + public function getInstance() { if ($this->job === null) { $class = $this->get('job_class'); @@ -74,7 +83,7 @@ public function job() */ public function run() { - $job = $this->job(); + $job = $this->getInstance(); $this->set('ts_last_attempt', date('Y-m-d H:i:s')); try { @@ -186,6 +195,7 @@ public function setTimeperiod($timeperiod) /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() @@ -201,7 +211,7 @@ public function export() foreach ($this->stateProperties as $key) { unset($plain->$key); } - $plain->settings = $this->job()->exportSettings(); + $plain->settings = $this->getInstance()->exportSettings(); return $plain; } @@ -272,9 +282,10 @@ protected static function existsWithNameAndId($name, $id, Db $connection) } /** + * @api internal Exporter only * @return IcingaTimePeriod */ - protected function timeperiod() + public function timeperiod() { try { return IcingaTimePeriod::loadWithAutoIncId($this->get('timeperiod_id'), $this->connection); diff --git a/library/Director/Objects/IcingaArguments.php b/library/Director/Objects/IcingaArguments.php index d00fe6841..e788da8c6 100644 --- a/library/Director/Objects/IcingaArguments.php +++ b/library/Director/Objects/IcingaArguments.php @@ -11,6 +11,8 @@ class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer { + const COMMENT_DSL_UNSUPPORTED = '/* Icinga 2 does not export DSL function bodies via API */'; + /** @var IcingaCommandArgument[] */ protected $storedArguments = []; @@ -153,7 +155,7 @@ protected function mungeCommandArgument($key, $value) if (property_exists($value, 'type')) { // argument is directly set as function, no further properties if ($value->type === 'Function') { - $attrs['argument_value'] = '/* Unable to fetch function body through API */'; + $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED; $attrs['argument_format'] = 'expression'; } } elseif (property_exists($value, 'value')) { @@ -164,7 +166,7 @@ protected function mungeCommandArgument($key, $value) $attrs['argument_value'] = $value->value->body; $attrs['argument_format'] = 'expression'; } elseif ($value->value->type === 'Function') { - $attrs['argument_value'] = '/* Unable to fetch function body through API */'; + $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED; $attrs['argument_format'] = 'expression'; } else { die('Unable to resolve command argument'); @@ -192,7 +194,7 @@ protected function mungeCommandArgument($key, $value) if (array_key_exists('set_if', $attrs)) { if (is_object($attrs['set_if']) && $attrs['set_if']->type === 'Function') { - $attrs['set_if'] = '/* Unable to fetch function body through API */'; + $attrs['set_if'] = self::COMMENT_DSL_UNSUPPORTED; $attrs['set_if_format'] = 'expression'; } elseif (property_exists($value, 'set_if_format')) { if (in_array($value->set_if_format, ['string', 'expression', 'json'])) { diff --git a/library/Director/Objects/IcingaCommand.php b/library/Director/Objects/IcingaCommand.php index 2567e3e7b..ad9965135 100644 --- a/library/Director/Objects/IcingaCommand.php +++ b/library/Director/Objects/IcingaCommand.php @@ -215,6 +215,7 @@ public function getUniqueIdentifier() /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() @@ -264,6 +265,10 @@ public static function import($plain, Db $db, $replace = false) return $object; } + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ protected function loadFieldReferences() { $db = $this->getDb(); diff --git a/library/Director/Objects/IcingaDependency.php b/library/Director/Objects/IcingaDependency.php index 77e5e31ad..c9d9b8968 100644 --- a/library/Director/Objects/IcingaDependency.php +++ b/library/Director/Objects/IcingaDependency.php @@ -82,6 +82,7 @@ public function getUniqueIdentifier() /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() diff --git a/library/Director/Objects/IcingaEndpoint.php b/library/Director/Objects/IcingaEndpoint.php index 5cb17953f..030183b63 100644 --- a/library/Director/Objects/IcingaEndpoint.php +++ b/library/Director/Objects/IcingaEndpoint.php @@ -8,6 +8,7 @@ use Icinga\Module\Director\Exception\NestingError; use Icinga\Module\Director\IcingaConfig\IcingaConfig; use InvalidArgumentException; +use RuntimeException; class IcingaEndpoint extends IcingaObject { @@ -42,10 +43,12 @@ public function hasApiUser() public function getApiUser() { - return $this->getRelatedObject( - 'apiuser', - $this->getResolvedProperty('apiuser_id') - ); + $id = $this->getResolvedProperty('apiuser_id'); + if ($id === null) { + throw new RuntimeException('Trying to get API User for Endpoint without such: ' . $this->getObjectName()); + } + + return $this->getRelatedObject('apiuser', $id); } /** diff --git a/library/Director/Objects/IcingaHost.php b/library/Director/Objects/IcingaHost.php index df2826dd7..2731f4a11 100644 --- a/library/Director/Objects/IcingaHost.php +++ b/library/Director/Objects/IcingaHost.php @@ -312,6 +312,7 @@ public function getUniqueIdentifier() /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() @@ -363,6 +364,10 @@ public static function import($plain, Db $db, $replace = false) return $object; } + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ protected function loadFieldReferences() { $db = $this->getDb(); diff --git a/library/Director/Objects/IcingaNotification.php b/library/Director/Objects/IcingaNotification.php index ec88b57ed..19e2897fa 100644 --- a/library/Director/Objects/IcingaNotification.php +++ b/library/Director/Objects/IcingaNotification.php @@ -98,6 +98,7 @@ public function getUniqueIdentifier() /** * @return \stdClass + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() @@ -142,6 +143,10 @@ public static function import($plain, Db $db, $replace = false) return $object; } + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ protected function loadFieldReferences() { $db = $this->getDb(); diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php index fd3bc205b..a43e14511 100644 --- a/library/Director/Objects/IcingaObject.php +++ b/library/Director/Objects/IcingaObject.php @@ -1536,6 +1536,9 @@ public function setBeingLoadedFromDb() foreach ($this->loadedMultiRelations as $multiRelation) { $multiRelation->setBeingLoadedFromDb(); } + // This might trigger DB requests and 404's. We might want to defer this, but a call to + // hasBeenModified triggers anyway: + $this->resolveUnresolvedRelatedProperties(); parent::setBeingLoadedFromDb(); } diff --git a/library/Director/Objects/IcingaObjectGroup.php b/library/Director/Objects/IcingaObjectGroup.php index bfa712469..c0bec5452 100644 --- a/library/Director/Objects/IcingaObjectGroup.php +++ b/library/Director/Objects/IcingaObjectGroup.php @@ -31,6 +31,7 @@ public function getUniqueIdentifier() /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php index 5dddc7035..43cc53a16 100644 --- a/library/Director/Objects/IcingaService.php +++ b/library/Director/Objects/IcingaService.php @@ -171,6 +171,7 @@ public function getUniqueIdentifier() /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() @@ -225,6 +226,10 @@ public static function import($plain, Db $db, $replace = false) return $object; } + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ protected function loadFieldReferences() { $db = $this->getDb(); diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index 2a31b146a..6ebd570d8 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -135,6 +135,7 @@ public function getUniqueIdentifier() /** * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @throws \Icinga\Exception\NotFoundError */ public function export() @@ -157,6 +158,7 @@ protected function exportSetOnHost() /** * @return object + * @deprecated * @throws \Icinga\Exception\NotFoundError */ protected function exportTemplate() diff --git a/library/Director/Objects/IcingaTemplateChoice.php b/library/Director/Objects/IcingaTemplateChoice.php index 8e34697ca..1a1be903f 100644 --- a/library/Director/Objects/IcingaTemplateChoice.php +++ b/library/Director/Objects/IcingaTemplateChoice.php @@ -69,6 +69,10 @@ public static function import($plain, Db $db, $replace = false) return $object; } + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return array|object|\stdClass + */ public function export() { $plain = (object) $this->getProperties(); @@ -78,14 +82,10 @@ public function export() unset($plain->required_template_id); if ($requiredId) { $db = $this->getDb(); - $query = $db->select()->from( - ['o' => $this->getObjectTableName()], - ['o.id', 'o.object_name'] - )->where("o.object_type = 'template'") - ->where('o.template_choice_id = ?', $this->get('id')) - ->order('o.object_name'); - - return $db->fetchPairs($query); + $query = $db->select() + ->from(['o' => $this->getObjectTableName()], 'o.object_name')->where("o.object_type = 'template'") + ->where('o.id = ?', $this->get('id')); + $plain->required_template = $db->fetchOne($query); } $plain->members = array_values($this->getMembers()); diff --git a/library/Director/Objects/IcingaTimePeriod.php b/library/Director/Objects/IcingaTimePeriod.php index a3c65c418..123236656 100644 --- a/library/Director/Objects/IcingaTimePeriod.php +++ b/library/Director/Objects/IcingaTimePeriod.php @@ -56,6 +56,7 @@ public function getUniqueIdentifier() } /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @return object * @throws \Icinga\Exception\NotFoundError */ diff --git a/library/Director/Objects/ImportRowModifier.php b/library/Director/Objects/ImportRowModifier.php index f3022ed70..76982c2eb 100644 --- a/library/Director/Objects/ImportRowModifier.php +++ b/library/Director/Objects/ImportRowModifier.php @@ -7,7 +7,7 @@ use Icinga\Module\Director\Objects\Extension\PriorityColumn; use RuntimeException; -class ImportRowModifier extends DbObjectWithSettings +class ImportRowModifier extends DbObjectWithSettings implements InstantiatedViaHook { use PriorityColumn; @@ -56,6 +56,7 @@ public function getInstance() } /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter * @return \stdClass */ public function export() diff --git a/library/Director/Objects/ImportSource.php b/library/Director/Objects/ImportSource.php index 81aa38ddf..44323c33f 100644 --- a/library/Director/Objects/ImportSource.php +++ b/library/Director/Objects/ImportSource.php @@ -53,6 +53,7 @@ class ImportSource extends DbObjectWithSettings implements ExportInterface private $newRowModifiers; /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader * @return \stdClass */ public function export() @@ -203,6 +204,10 @@ protected static function existsWithNameAndId($name, $id, Db $connection) ); } + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ protected function exportRowModifiers() { $modifiers = []; diff --git a/library/Director/Objects/InstantiatedViaHook.php b/library/Director/Objects/InstantiatedViaHook.php new file mode 100644 index 000000000..79f34427f --- /dev/null +++ b/library/Director/Objects/InstantiatedViaHook.php @@ -0,0 +1,14 @@ +apply()) { Benchmark::measure('Successfully synced rule ' . $this->get('rule_name')); $this->set('sync_state', 'in-sync'); - $this->currentSyncRunId = $runId; } $hadChanges = true; @@ -202,11 +198,6 @@ public function applyChanges() return $this->checkForChanges(true); } - public function getCurrentSyncRunId() - { - return $this->currentSyncRunId; - } - public function getSourceKeyPattern() { if ($this->hasCombinedKey()) { @@ -265,6 +256,10 @@ protected function loadConfiguredPurgeStrategy() } } + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return object + */ public function export() { $plain = $this->getProperties(); @@ -356,7 +351,11 @@ protected function onStore() } } - public function exportSyncProperties() + /** + * @deprecated + * @return array + */ + protected function exportSyncProperties() { $all = []; $db = $this->getDb(); diff --git a/library/Director/Objects/SyncRun.php b/library/Director/Objects/SyncRun.php index 44c09ac90..62f7378de 100644 --- a/library/Director/Objects/SyncRun.php +++ b/library/Director/Objects/SyncRun.php @@ -36,4 +36,11 @@ public static function start(SyncRule $rule) $rule->getConnection() ); } + + public function countActivities() + { + return (int) $this->get('objects_deleted') + + (int) $this->get('objects_created') + + (int) $this->get('objects_modified'); + } } diff --git a/library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php b/library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php new file mode 100644 index 000000000..2a60ab3e2 --- /dev/null +++ b/library/Director/PropertyModifier/PropertyModifierDictionaryToRow.php @@ -0,0 +1,94 @@ +addElement('text', 'key_column', [ + 'label' => $form->translate('Key Property Name'), + 'description' => $form->translate( + 'Every Dictionary entry has a key, its value will be provided in this column' + ) + ]); + $form->addElement('select', 'on_empty', [ + 'label' => $form->translate('When empty'), + 'description' => $form->translate('What should we do in case the given value is empty?'), + 'multiOptions' => $form->optionalEnum([ + 'reject' => $form->translate('Drop the current row'), + 'fail' => $form->translate('Let the whole import run fail'), + 'keep' => $form->translate('Keep the row, set the column value to null'), + ]), + 'value' => 'reject', + 'required' => true, + ]); + } + + public function requiresRow() + { + return true; + } + + public function hasArraySupport() + { + return true; + } + + public function expandsRows() + { + return true; + } + + public function transform($value) + { + if (empty($value)) { + $onDuplicate = $this->getSetting('on_empty', 'reject'); + switch ($onDuplicate) { + case 'reject': + return []; + case 'keep': + return [null]; + case 'fail': + throw new InvalidArgumentException('Failed to clone row, value is empty'); + default: + throw new InvalidArgumentException( + "'$onDuplicate' is not a valid 'on_duplicate' setting" + ); + } + } + + $keyColumn = $this->getSetting('key_column'); + + if (! \is_object($value)) { + throw new InvalidArgumentException( + "Object required to clone this row, got " . Error::getPhpTypeName($value) + ); + } + $result = []; + foreach ($value as $key => $properties) { + if (! is_object($properties)) { + throw new InvalidDataException( + sprintf('Nested "%s" dictionary', $key), + $properties + ); + } + + $properties->$keyColumn = $key; + $result[] = $properties; + } + + return $result; + } +} diff --git a/library/Director/PropertyModifier/PropertyModifierReplaceNull.php b/library/Director/PropertyModifier/PropertyModifierReplaceNull.php new file mode 100644 index 000000000..d6f9fd34b --- /dev/null +++ b/library/Director/PropertyModifier/PropertyModifierReplaceNull.php @@ -0,0 +1,33 @@ +addElement('text', 'string', [ + 'label' => 'Replacement String', + 'description' => $form->translate('Your replacement string'), + 'required' => true, + ]); + } + + public function transform($value) + { + if ($value === null) { + return $this->getSetting('string'); + } else { + return $value; + } + } +} diff --git a/library/Director/Resolver/OverrideHelper.php b/library/Director/Resolver/OverrideHelper.php new file mode 100644 index 000000000..f911a4fe0 --- /dev/null +++ b/library/Director/Resolver/OverrideHelper.php @@ -0,0 +1,38 @@ +getOverriddenServiceVars($serviceName); + foreach ($properties as $key => $value) { + if ($key === 'vars') { + foreach ($value as $k => $v) { + $current->$k = $v; + } + } else { + $current->{substr($key, 5)} = $value; + } + } + $host->overrideServiceVars($serviceName, $current); + } + + public static function assertVarsForOverrides($properties) + { + if (empty($properties)) { + return; + } + + foreach ($properties as $key => $value) { + if ($key !== 'vars' && substr($key, 0, 5) !== 'vars.') { + throw new InvalidArgumentException("Only Custom Variables can be set based on Variable Overrides"); + } + } + } +} diff --git a/library/Director/RestApi/IcingaObjectHandler.php b/library/Director/RestApi/IcingaObjectHandler.php index 5a5c377cd..7329be33f 100644 --- a/library/Director/RestApi/IcingaObjectHandler.php +++ b/library/Director/RestApi/IcingaObjectHandler.php @@ -7,9 +7,14 @@ use Icinga\Exception\NotFoundError; use Icinga\Exception\ProgrammingError; use Icinga\Module\Director\Core\CoreApi; +use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\DirectorObject\Lookup\ServiceFinder; use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaObject; -use Icinga\Module\Director\Util; +use Icinga\Module\Director\Resolver\OverrideHelper; +use InvalidArgumentException; +use RuntimeException; class IcingaObjectHandler extends RequestHandler { @@ -47,7 +52,7 @@ protected function requireObject() /** * @return IcingaObject */ - protected function eventuallyLoadObject() + protected function loadOptionalObject() { return $this->object; } @@ -94,7 +99,6 @@ protected function processApiRequest() protected function handleApiRequest() { $request = $this->request; - $response = $this->response; $db = $this->db; // TODO: I hate doing this: @@ -122,8 +126,10 @@ protected function handleApiRequest() case 'POST': case 'PUT': $data = (array) $this->requireJsonBody(); + $params = $this->request->getUrl()->getParams(); + $allowsOverrides = $params->get('allowOverrides'); $type = $this->getType(); - if ($object = $this->eventuallyLoadObject()) { + if ($object = $this->loadOptionalObject()) { if ($request->getMethod() === 'POST') { $object->setProperties($data); } else { @@ -131,42 +137,27 @@ protected function handleApiRequest() 'object_type' => $object->get('object_type'), 'object_name' => $object->getObjectName() ], $data); - $object->replaceWith( - IcingaObject::createByType($type, $data, $db) - ); + $object->replaceWith(IcingaObject::createByType($type, $data, $db)); + } + $this->persistChanges($object); + $this->sendJson($object->toPlainObject(false, true)); + } elseif ($allowsOverrides && $type === 'service') { + if ($request->getMethod() === 'PUT') { + throw new InvalidArgumentException('Overrides are not (yet) available for HTTP PUT'); } + $this->setServiceProperties($params->getRequired('host'), $params->getRequired('name'), $data); } else { $object = IcingaObject::createByType($type, $data, $db); + $this->persistChanges($object); + $this->sendJson($object->toPlainObject(false, true)); } - - if ($object->hasBeenModified()) { - $status = $object->hasBeenLoadedFromDb() ? 200 : 201; - $object->store(); - $response->setHttpResponseCode($status); - } else { - $response->setHttpResponseCode(304); - } - - $this->sendJson($object->toPlainObject(false, true)); break; case 'GET': - $params = $this->request->getUrl()->getParams(); - $this->requireObject(); - $properties = $params->shift('properties'); - if (strlen($properties)) { - $properties = preg_split('/\s*,\s*/', $properties, -1, PREG_SPLIT_NO_EMPTY); - } else { - $properties = null; - } - - $this->sendJson( - $this->requireObject()->toPlainObject( - $params->shift('resolved'), - ! $params->shift('withNull'), - $properties - ) - ); + $object = $this->requireObject(); + $exporter = new Exporter($this->db); + RestApiParams::applyParamsToExporter($exporter, $this->request, $object->getShortTableName()); + $this->sendJson($exporter->export($object)); break; default: @@ -174,4 +165,32 @@ protected function handleApiRequest() throw new IcingaException('Unsupported method ' . $request->getMethod()); } } + + protected function persistChanges(IcingaObject $object) + { + if ($object->hasBeenModified()) { + $status = $object->hasBeenLoadedFromDb() ? 200 : 201; + $object->store(); + $this->response->setHttpResponseCode($status); + } else { + $this->response->setHttpResponseCode(304); + } + } + + protected function setServiceProperties($hostname, $serviceName, $properties) + { + $host = IcingaHost::load($hostname, $this->db); + $service = ServiceFinder::find($host, $serviceName); + if ($service === false) { + throw new NotFoundError('Not found'); + } + if ($service->requiresOverrides()) { + unset($properties['host']); + OverrideHelper::applyOverriddenVars($host, $serviceName, $properties); + $this->persistChanges($host); + $this->sendJson($host->toPlainObject(false, true)); + } else { + throw new RuntimeException('Found a single service, which should have been found (and dealt with) before'); + } + } } diff --git a/library/Director/RestApi/IcingaObjectsHandler.php b/library/Director/RestApi/IcingaObjectsHandler.php index 5af1c1ea3..471987adc 100644 --- a/library/Director/RestApi/IcingaObjectsHandler.php +++ b/library/Director/RestApi/IcingaObjectsHandler.php @@ -3,8 +3,10 @@ namespace Icinga\Module\Director\RestApi; use Exception; +use gipfl\Json\JsonString; use Icinga\Application\Benchmark; use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Data\Exporter; use Icinga\Module\Director\Db\Cache\PrefetchCache; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Web\Table\ApplyRulesTable; @@ -21,8 +23,7 @@ public function processApiRequest() try { $this->streamJsonResult(); } catch (Exception $e) { - // NONO - $this->sendJsonError($e->getTraceAsString()); + $this->sendJsonError($e); } } @@ -62,14 +63,18 @@ protected function streamJsonResult() Benchmark::measure('Ready to stream JSON result'); $db = $connection->getDbAdapter(); $table = $this->getTable(); + $exporter = new Exporter($connection); + $type = $table->getType(); + RestApiParams::applyParamsToExporter($exporter, $this->request, $type); $query = $table ->getQuery() ->reset(ZfSelect::COLUMNS) ->columns('*') ->reset(ZfSelect::LIMIT_COUNT) ->reset(ZfSelect::LIMIT_OFFSET); - $type = $table->getType(); - $serviceApply = $type === 'service' && $table instanceof ApplyRulesTable; + if ($type === 'service' && $table instanceof ApplyRulesTable) { + $exporter->showIds(); + } echo '{ "objects": [ '; $cnt = 0; $objects = []; @@ -85,15 +90,6 @@ protected function streamJsonResult() if (! ob_get_level()) { ob_start(); } - $params = $this->request->getUrl()->getParams(); - $resolved = (bool) $params->get('resolved', false); - $withNull = ! $params->shift('withNull'); - $properties = $params->shift('properties'); - if (strlen($properties)) { - $properties = preg_split('/\s*,\s*/', $properties, -1, PREG_SPLIT_NO_EMPTY); - } else { - $properties = null; - } $first = true; $flushes = 0; @@ -103,13 +99,7 @@ protected function streamJsonResult() Benchmark::measure('Fetching first row'); } $object = $dummy::fromDbRow($row, $connection); - $objects[] = json_encode($object->toPlainObject( - $resolved, - $withNull, - $properties, - true, - $serviceApply - ), JSON_PRETTY_PRINT); + $objects[] = JsonString::encode($exporter->export($object), JSON_PRETTY_PRINT); if ($first) { Benchmark::measure('Got first row'); $first = false; @@ -135,7 +125,7 @@ protected function streamJsonResult() echo implode(', ', $objects); } - if ($params->get('benchmark')) { + if ($this->request->getUrl()->getParams()->get('benchmark')) { echo "],\n"; Benchmark::measure('All done'); echo '"benchmark_string": ' . json_encode(Benchmark::renderToText()); diff --git a/library/Director/RestApi/RequestHandler.php b/library/Director/RestApi/RequestHandler.php index 0e63637c0..6f6688966 100644 --- a/library/Director/RestApi/RequestHandler.php +++ b/library/Director/RestApi/RequestHandler.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\RestApi; use Exception; +use gipfl\Json\JsonString; use Icinga\Module\Director\Db; use Icinga\Web\Request; use Icinga\Web\Response; @@ -36,7 +37,7 @@ public function sendJson($object) { $this->response->setHeader('Content-Type', 'application/json', true); $this->response->sendHeaders(); - echo json_encode($object, JSON_PRETTY_PRINT) . "\n"; + echo JsonString::encode($object, JSON_PRETTY_PRINT) . "\n"; } public function sendJsonError($error, $code = null) @@ -57,7 +58,11 @@ public function sendJsonError($error, $code = null) } $response->sendHeaders(); - $this->sendJson((object) ['error' => $message]); + $result = ['error' => $message]; + if ($this->request->getUrl()->getParam('showStacktrace')) { + $result['trace'] = $error->getTraceAsString(); + } + $this->sendJson((object) $result); } // TODO: just return json_last_error_msg() for PHP >= 5.5.0 diff --git a/library/Director/RestApi/RestApiParams.php b/library/Director/RestApi/RestApiParams.php new file mode 100644 index 000000000..c237ac512 --- /dev/null +++ b/library/Director/RestApi/RestApiParams.php @@ -0,0 +1,29 @@ +getUrl()->getParams(); + $resolved = (bool) $params->get('resolved', false); + $withNull = $params->shift('withNull'); + if ($params->get('withServices')) { + if ($shortObjectType !== 'host') { + throw new InvalidArgumentException('withServices is available for Hosts only'); + } + $exporter->enableHostServices(); + } + $properties = $params->shift('properties'); + if ($properties !== null && strlen($properties)) { + $exporter->filterProperties(preg_split('/\s*,\s*/', $properties, -1, PREG_SPLIT_NO_EMPTY)); + } + $exporter->resolveObjects($resolved); + $exporter->showDefaults($withNull); + } +} diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php index e7482efe9..a491024b4 100644 --- a/library/Director/Web/Controller/ObjectController.php +++ b/library/Director/Web/Controller/ObjectController.php @@ -63,47 +63,56 @@ public function init() { parent::init(); $this->enableStaticObjectLoader($this->getTableName()); - if ($this->getRequest()->isApiRequest()) { - $handler = new IcingaObjectHandler($this->getRequest(), $this->getResponse(), $this->db()); - try { - $this->loadOptionalObject(); - } catch (NotFoundError $e) { - // Silently ignore the error, the handler will complain - $handler->sendJsonError($e, 404); - // TODO: nice shutdown - exit; - } + $this->initializeRestApi(); + } else { + $this->initializeWebRequest(); + } + } - $handler->setApi($this->api()); - if ($this->object) { - $handler->setObject($this->object); - } - $handler->dispatch(); - // Hint: also here, hard exit. There is too much magic going on. - // Letting this bubble up smoothly would be "correct", but proved - // to be too fragile. Web 2, all kinds of pre/postDispatch magic, - // different view renderers - hard exit is the only safe bet right - // now. + protected function initializeRestApi() + { + $handler = new IcingaObjectHandler($this->getRequest(), $this->getResponse(), $this->db()); + try { + $this->loadOptionalObject(); + } catch (NotFoundError $e) { + // Silently ignore the error, the handler will complain + $handler->sendJsonError($e, 404); + // TODO: nice shutdown exit; + } + + $handler->setApi($this->api()); + if ($this->object) { + $handler->setObject($this->object); + } + $handler->dispatch(); + // Hint: also here, hard exit. There is too much magic going on. + // Letting this bubble up smoothly would be "correct", but proved + // to be too fragile. Web 2, all kinds of pre/postDispatch magic, + // different view renderers - hard exit is the only safe bet right + // now. + exit; + } + + protected function initializeWebRequest() + { + $this->loadOptionalObject(); + if ($this->getRequest()->getActionName() === 'add') { + $this->addSingleTab( + sprintf($this->translate('Add %s'), ucfirst($this->getType())), + null, + 'add' + ); } else { - $this->loadOptionalObject(); - if ($this->getRequest()->getActionName() === 'add') { - $this->addSingleTab( - sprintf($this->translate('Add %s'), ucfirst($this->getType())), - null, - 'add' - ); - } else { - $this->tabs(new ObjectTabs( - $this->getRequest()->getControllerName(), - $this->getAuth(), - $this->object - )); - } - if ($this->object !== null) { - $this->addDeploymentLink(); - } + $this->tabs(new ObjectTabs( + $this->getRequest()->getControllerName(), + $this->getAuth(), + $this->object + )); + } + if ($this->object !== null) { + $this->addDeploymentLink(); } } diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php index 07610834e..67927ebde 100644 --- a/library/Director/Web/Controller/ObjectsController.php +++ b/library/Director/Web/Controller/ObjectsController.php @@ -85,7 +85,7 @@ protected function apiRequestHandler() $table->filterObjectType('apply'); } $search = $this->params->get('q'); - if (\strlen($search) > 0) { + if ($search !== null && \strlen($search) > 0) { $table->search($search); } diff --git a/library/Director/Web/Form/CloneImportSourceForm.php b/library/Director/Web/Form/CloneImportSourceForm.php index bce920234..0849dd407 100644 --- a/library/Director/Web/Form/CloneImportSourceForm.php +++ b/library/Director/Web/Form/CloneImportSourceForm.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Form; +use Icinga\Module\Director\Data\Exporter; use ipl\Html\Form; use ipl\Html\FormDecorator\DdDtDecorator; use gipfl\Translation\TranslationHelper; @@ -48,14 +49,15 @@ protected function getTargetDb() */ public function onSuccess() { - $export = $this->source->export(); + $db = $this->getTargetDb(); + $export = (new Exporter($db))->export($this->source); $newName = $this->getElement('source_name')->getValue(); $export->source_name = $newName; unset($export->originalId); - if (ImportSource::existsWithName($newName, $this->source->getConnection())) { + if (ImportSource::existsWithName($newName, $db)) { $this->getElement('source_name')->addMessage('Name already exists'); } - $this->newSource = ImportSource::import($export, $this->getTargetDb()); + $this->newSource = ImportSource::import($export, $db); $this->newSource->store(); } diff --git a/library/Director/Web/Form/CloneSyncRuleForm.php b/library/Director/Web/Form/CloneSyncRuleForm.php index a3a5a3410..f90b593fa 100644 --- a/library/Director/Web/Form/CloneSyncRuleForm.php +++ b/library/Director/Web/Form/CloneSyncRuleForm.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Form; +use Icinga\Module\Director\Data\Exporter; use ipl\Html\Form; use ipl\Html\FormDecorator\DdDtDecorator; use gipfl\Translation\TranslationHelper; @@ -49,15 +50,18 @@ protected function getTargetDb() */ public function onSuccess() { - $export = $this->rule->export(); + $db = $this->getTargetDb(); + $exporter = new Exporter($db); + + $export = $exporter->export($this->rule); $newName = $this->getValue('rule_name'); $export->rule_name = $newName; unset($export->originalId); - if (SyncRule::existsWithName($newName, $this->getTargetDb())) { + if (SyncRule::existsWithName($newName, $db)) { $this->getElement('rule_name')->addMessage('Name already exists'); } - $this->newRule = SyncRule::import($export, $this->getTargetDb()); + $this->newRule = SyncRule::import($export, $db); $this->newRule->store(); } diff --git a/library/Director/Web/Form/Element/SimpleNote.php b/library/Director/Web/Form/Element/SimpleNote.php index 96e7f05eb..3097e117c 100644 --- a/library/Director/Web/Form/Element/SimpleNote.php +++ b/library/Director/Web/Form/Element/SimpleNote.php @@ -2,6 +2,9 @@ namespace Icinga\Module\Director\Web\Form\Element; +use Icinga\Module\Director\PlainObjectRenderer; +use ipl\Html\ValidHtml; + class SimpleNote extends FormElement { public $helper = 'formSimpleNote'; @@ -19,4 +22,13 @@ public function isValid($value, $context = null) { return true; } + + public function setValue($value) + { + if (is_object($value) && ! $value instanceof ValidHtml) { + $value = 'Unexpected object: ' . PlainObjectRenderer::render($value); + } + + return parent::setValue($value); + } } diff --git a/library/Director/Web/Form/QuickForm.php b/library/Director/Web/Form/QuickForm.php index 2fa3d0827..91c8f00bb 100644 --- a/library/Director/Web/Form/QuickForm.php +++ b/library/Director/Web/Form/QuickForm.php @@ -92,6 +92,7 @@ protected function getActionFromRequest() protected function setPreferredDecorators() { $current = $this->getAttrib('class'); + $current .= ' director-form'; if ($current) { $this->setAttrib('class', "$current autofocus"); } else { diff --git a/library/Director/Web/Table/BranchActivityTable.php b/library/Director/Web/Table/BranchActivityTable.php index 42dc3e679..d3a867a9f 100644 --- a/library/Director/Web/Table/BranchActivityTable.php +++ b/library/Director/Web/Table/BranchActivityTable.php @@ -83,7 +83,7 @@ protected function makeBranchLink(BranchActivity $activity) $activity->getAuthor(), $activity->getAction(), $type, - 'object name' + $activity->getObjectName() ); } } diff --git a/library/Director/Web/Table/DbHelper.php b/library/Director/Web/Table/DbHelper.php index 6e5d9b836..573f94610 100644 --- a/library/Director/Web/Table/DbHelper.php +++ b/library/Director/Web/Table/DbHelper.php @@ -17,11 +17,19 @@ public function dbHexFunc($column) public function quoteBinary($binary) { + if ($binary === '') { + return ''; + } + + if (is_array($binary)) { + return array_map([$this, 'quoteBinary'], $binary); + } + if ($this->isPgsql()) { return new Expr("'\\x" . bin2hex($binary) . "'"); } - return $binary; + return new Expr('0x' . bin2hex($binary)); } public function isPgsql() diff --git a/library/Director/Web/Table/IcingaHostAppliedServicesTable.php b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php index 10f07f453..415903b40 100644 --- a/library/Director/Web/Table/IcingaHostAppliedServicesTable.php +++ b/library/Director/Web/Table/IcingaHostAppliedServicesTable.php @@ -149,6 +149,7 @@ public function prepareQuery() $ds = new ArrayDatasource($services); return $ds->select()->columns([ 'id' => 'id', + 'uuid' => 'uuid', 'name' => 'name', 'filter' => 'filter', 'disabled' => 'disabled', @@ -184,6 +185,7 @@ protected function fetchAllApplyRules() ['s' => 'icinga_service'], [ 'id' => 's.id', + 'uuid' => 's.uuid', 'name' => 's.object_name', 'assign_filter' => 's.assign_filter', 'apply_for' => 's.apply_for', diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php index 864601756..41763c02b 100644 --- a/library/Director/Web/Table/IcingaServiceSetServiceTable.php +++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php @@ -234,6 +234,7 @@ public function prepareQuery() ['s' => 'icinga_service'], [ 'id' => 's.id', + 'uuid' => 's.uuid', 'service_set_id' => 's.service_set_id', 'host_id' => 'ss.host_id', 'service_set' => 'ss.object_name', diff --git a/library/Director/Web/Widget/ActivityLogInfo.php b/library/Director/Web/Widget/ActivityLogInfo.php index 4a8634873..8454b2632 100644 --- a/library/Director/Web/Widget/ActivityLogInfo.php +++ b/library/Director/Web/Widget/ActivityLogInfo.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Web\Widget; use gipfl\Json\JsonString; +use Icinga\Module\Director\Objects\DirectorActivityLog; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use Icinga\Date\DateFormatter; @@ -343,7 +344,6 @@ protected function objectKey() /** * @param Url|null $url * @return Tabs - * @throws ProgrammingError */ public function getTabs(Url $url = null) { @@ -357,13 +357,12 @@ public function getTabs(Url $url = null) /** * @param Url $url * @return Tabs - * @throws ProgrammingError */ public function createTabs(Url $url) { $entry = $this->entry; $tabs = new Tabs(); - if ($entry->action_name === 'modify') { + if ($entry->action_name === DirectorActivityLog::ACTION_MODIFY) { $tabs->add('diff', [ 'label' => $this->translate('Diff'), 'url' => $url->without('show')->with('id', $entry->id) @@ -372,7 +371,10 @@ public function createTabs(Url $url) $this->defaultTab = 'diff'; } - if (in_array($entry->action_name, ['create', 'modify'])) { + if (in_array($entry->action_name, [ + DirectorActivityLog::ACTION_CREATE, + DirectorActivityLog::ACTION_MODIFY, + ])) { $tabs->add('new', [ 'label' => $this->translate('New object'), 'url' => $url->with(['id' => $entry->id, 'show' => 'new']) @@ -383,7 +385,10 @@ public function createTabs(Url $url) } } - if (in_array($entry->action_name, ['delete', 'modify'])) { + if (in_array($entry->action_name, [ + DirectorActivityLog::ACTION_DELETE, + DirectorActivityLog::ACTION_MODIFY, + ])) { $tabs->add('old', [ 'label' => $this->translate('Former object'), 'url' => $url->with(['id' => $entry->id, 'show' => 'old']) @@ -572,13 +577,13 @@ public function hasBeenDisabled() public function getTitle() { switch ($this->entry->action_name) { - case 'create': + case DirectorActivityLog::ACTION_CREATE: $msg = $this->translate('%s "%s" has been created'); break; - case 'delete': + case DirectorActivityLog::ACTION_DELETE: $msg = $this->translate('%s "%s" has been deleted'); break; - case 'modify': + case DirectorActivityLog::ACTION_MODIFY: $msg = $this->translate('%s "%s" has been modified'); break; default: diff --git a/library/Director/Web/Widget/BackgroundDaemonDetails.php b/library/Director/Web/Widget/BackgroundDaemonDetails.php index 1eb95470d..b4c33dd87 100644 --- a/library/Director/Web/Widget/BackgroundDaemonDetails.php +++ b/library/Director/Web/Widget/BackgroundDaemonDetails.php @@ -114,7 +114,7 @@ protected function assemble() $pid ], Html::tag('pre', $process->command), - Format::bytes($process->memory->rss) + $process->memory === false ? 'n/a' : Format::bytes($process->memory->rss) ])); } $this->add($table); diff --git a/library/Director/Web/Widget/SyncRunDetails.php b/library/Director/Web/Widget/SyncRunDetails.php index 90eb1efe2..408e8f6b2 100644 --- a/library/Director/Web/Widget/SyncRunDetails.php +++ b/library/Director/Web/Widget/SyncRunDetails.php @@ -2,18 +2,21 @@ namespace Icinga\Module\Director\Web\Widget; +use Icinga\Module\Director\Objects\DirectorActivityLog; use ipl\Html\HtmlDocument; use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\SyncRun; -use ipl\Html\Html; use gipfl\IcingaWeb2\Link; use gipfl\Translation\TranslationHelper; use gipfl\IcingaWeb2\Widget\NameValueTable; +use function sprintf; class SyncRunDetails extends NameValueTable { use TranslationHelper; + const URL_ACTIVITIES = 'director/config/activities'; + /** @var SyncRun */ protected $run; @@ -22,22 +25,20 @@ public function __construct(SyncRun $run) $this->run = $run; $this->getAttributes()->add('data-base-target', '_next'); // eigentlich nur runSummary $this->addNameValuePairs([ - $this->translate('Start time') => $run->start_time, - $this->translate('Duration') => sprintf('%.2fs', $run->duration_ms / 1000), - $this->translate('Activity') => $this->runSummary($run) + $this->translate('Start time') => $run->get('start_time'), + $this->translate('Duration') => sprintf('%.2fs', $run->get('duration_ms') / 1000), + $this->translate('Activity') => $this->runSummary($run) ]); } /** * @param SyncRun $run * @return array - * @throws \Icinga\Exception\IcingaException - * @throws \Icinga\Exception\ProgrammingError */ protected function runSummary(SyncRun $run) { $html = []; - $total = $run->objects_deleted + $run->objects_created + $run->objects_modified; + $total = $run->countActivities(); if ($total === 0) { $html[] = $this->translate('No changes have been made'); } else { @@ -52,47 +53,49 @@ protected function runSummary(SyncRun $run) /** @var Db $db */ $db = $run->getConnection(); - if ($run->last_former_activity === null) { + $formerId = $db->fetchActivityLogIdByChecksum($run->get('last_former_activity')); + if ($formerId === null) { return $html; } - $formerId = $db->fetchActivityLogIdByChecksum($run->last_former_activity); - $lastId = $db->fetchActivityLogIdByChecksum($run->last_related_activity); + $lastId = $db->fetchActivityLogIdByChecksum($run->get('last_related_activity')); - $idRangeEx = sprintf( - 'id>%d&id<=%d', - $formerId, - $lastId - ); - $activityUrl = 'director/config/activities'; + if ($formerId !== $lastId) { + $idRangeEx = sprintf( + 'id>%d&id<=%d', + $formerId, + $lastId + ); + } else { + $idRangeEx = null; + } $links = new HtmlDocument(); $links->setSeparator(', '); - if ($run->objects_created > 0) { - $links->add(new Link( - sprintf('%d created', $run->objects_created), - $activityUrl, - ['action' => 'create', 'idRangeEx' => $idRangeEx] - )); - } - if ($run->objects_modified > 0) { - $links->add(new Link( - sprintf('%d modified', $run->objects_modified), - $activityUrl, - ['action' => 'modify', 'idRangeEx' => $idRangeEx] - )); - } - if ($run->objects_deleted > 0) { - $links->add(new Link( - sprintf('%d deleted', $run->objects_deleted), - $activityUrl, - ['action' => 'delete', 'idRangeEx' => $idRangeEx] - )); - } + $links->add([ + $this->activitiesLink( + 'objects_created', + $this->translate('%d created'), + DirectorActivityLog::ACTION_CREATE, + $idRangeEx + ), + $this->activitiesLink( + 'objects_modified', + $this->translate('%d modified'), + DirectorActivityLog::ACTION_MODIFY, + $idRangeEx + ), + $this->activitiesLink( + 'objects_deleted', + $this->translate('%d deleted'), + DirectorActivityLog::ACTION_DELETE, + $idRangeEx + ), + ]); - if (count($links) > 1) { + if ($idRangeEx && count($links) > 1) { $links->add(new Link( - 'Show all actions', - $activityUrl, + $this->translate('Show all actions'), + self::URL_ACTIVITIES, ['idRangeEx' => $idRangeEx] )); } @@ -105,4 +108,22 @@ protected function runSummary(SyncRun $run) return $html; } + + protected function activitiesLink($key, $label, $action, $rangeFilter) + { + $count = $this->run->get($key); + if ($count > 0) { + if ($rangeFilter) { + return new Link( + sprintf($label, $count), + self::URL_ACTIVITIES, + ['action' => $action, 'idRangeEx' => $rangeFilter] + ); + } + + return sprintf($label, $count); + } + + return null; + } } diff --git a/public/css/module.less b/public/css/module.less index 3a4cd4f90..438c397c4 100644 --- a/public/css/module.less +++ b/public/css/module.less @@ -33,11 +33,11 @@ a:before { text-decoration: none; } -form { +form.director-form { max-width: 68em; } -form:focus { +form.director-form:focus { outline: none; } @@ -59,7 +59,7 @@ div.action-bar { vertical-align: middle; } - form input { + form.director-form input { margin: 0; } input { @@ -198,14 +198,14 @@ span.disabled { color: @icinga-blue; margin-right: 1em; } - form { + form.director-form { display: inline; margin-right: 1em; } } .action-bar { - form { + form.director-form { display: inline; margin-right: 1em; } @@ -215,12 +215,12 @@ pre.disabled { color: @disabled-gray; } -form i.link-color::before { +form.director-form i.link-color::before { color: @icinga-blue; } /* TODO: remove this, but autosubmit looks ugly otherwise */ -input[disabled] { +form.director-form input[disabled] { background: inherit; } /* END OF TODO */ @@ -311,12 +311,12 @@ table.avp th { font-size: inherit; } -.content form { +.content form.director-form { margin-top: 0.5em; margin-bottom: 2em; } -.content form.inline { +.content form.director-form.inline { margin: 0; i.icon::before { @@ -329,12 +329,12 @@ table.avp th { left: -100%; } -form input[type=file] { +form.director-form input[type=file] { padding-right: 1em; } -form input[type=submit] { +form.director-form input[type=submit] { .button(); border-width: 1px; margin-top: 0.5em; @@ -346,11 +346,11 @@ form input[type=submit] { } } -form input[type=submit]:first-of-type { +form.director-form input[type=submit]:first-of-type { border-width: 2px; } -form input[type=submit].link-button { +form.director-form input[type=submit].link-button { color: @icinga-blue; background: none; border: none; @@ -365,19 +365,21 @@ form input[type=submit].link-button { } } -form p.description { +form.director-form p.description { padding: 1em 1em; margin: 0; font-style: italic; width: 100%; } -input[type=text], input[type=button], select, select option, textarea { +form.director-form { + input[type=text], input[type=button], select, select option, textarea { -webkit-appearance: none; -moz-appearance: none; + } } -form ul.form-errors { +form.director-form ul.form-errors { list-style-type: none; margin-bottom: 0.5em; padding: 0; @@ -395,17 +397,18 @@ form ul.form-errors { } } -select::-ms-expand, input::-ms-expand, textarea::-ms-expand { /* for IE 11 */ +form.director-form { + select::-ms-expand, input::-ms-expand, textarea::-ms-expand { /* for IE 11 */ display: none; -} + } -select { + select { border: 1px solid @gray-light; cursor: pointer; background: none; -} + } -input[type=text], input[type=password], textarea, select { + input[type=text], input[type=password], textarea, select { max-width: 36em; min-width: 20em; width: 100%; @@ -419,33 +422,34 @@ input[type=text], input[type=password], textarea, select { background-color: @low-sat-blue; &.search { - background: transparent url("../img/icons/search.png") no-repeat scroll 0.5em center / 1em 1em; - padding-left: 2em; + background: transparent url("../img/icons/search.png") no-repeat scroll 0.5em center / 1em 1em; + padding-left: 2em; } -} + } -textarea { - max-width: 100%; -} + textarea { + max-width: 100%; + } -select[multiple] { + select[multiple] { height: auto; -} + } -select option { + select option { height: 2em; padding-top: 0.3em; -} + } -select[multiple=multiple] { - height: auto; -} + select[multiple=multiple] { + height: auto; + } -label { + label { line-height: 2em; + } } -form dl { +form.director-form dl { margin: 0; padding: 0; } @@ -478,7 +482,7 @@ div.filter form.editor { } } -form.editor { +form.director-form.editor { select, input[type=text] { background: @low-sat-blue; max-width: unset; @@ -490,30 +494,6 @@ form.editor { } } -form.gipfl-quicksearch { - display: block; - input.search { - width: 8em; - min-width: unset; - border: none; - background-size: 0.75em auto; - background-position: 0.5em 0.9em; - padding-left: 1.5em; - margin-left: 1.5em; - font-size: 0.75em; - font-weight: normal; - &:focus { - width: 16em; - .transition(width 0.5s ease); - border: none; - } - } -} - -#layout.twocols form.quicksearch input.search { - float: right; -} - ul.extensible-set { margin: 0; padding: 0; @@ -567,14 +547,14 @@ ul.extensible-set { } } -form { +form.director-form { #_FAKE_SUBMIT { position: absolute; left: -100%; } } -dd.active ul.extensible-set, ul.extensible-set.sortable { +form.director-form dd.active ul.extensible-set, ul.extensible-set.sortable { li { display: list-item; @@ -595,7 +575,7 @@ dd.active ul.extensible-set, ul.extensible-set.sortable { } } -dd.active ul.extensible-set { +form.director-form dd.active ul.extensible-set { border: 1px solid @icinga-blue; input[type=submit]:first-of-type { @@ -607,33 +587,37 @@ dd.active ul.extensible-set { } } -select::-moz-focus-inner { border: 0; } +form.director-form { + select::-moz-focus-inner { + border: 0; + } -select:-moz-focusring { + select:-moz-focusring { color: transparent; text-shadow: 0 0 0 #000; -} + } -select, input[type=text], textarea { + select, input[type=text], textarea { &:hover { - border-style: dotted solid dotted solid; - border-color: @gray-light; + border-style: dotted solid dotted solid; + border-color: @gray-light; } &:focus, &:focus:hover { - border-style: solid; - border-color: @icinga-blue; - outline: none; + border-style: solid; + border-color: @icinga-blue; + outline: none; } -} + } -select option { + select option { padding-left: 0.5em; -} + } -select option[value=""] { - color: @disabled-gray; - background-color: @low-sat-blue; + select option[value=""] { + color: @disabled-gray; + background-color: @low-sat-blue; + } } a { @@ -785,7 +769,7 @@ ul.main-actions { } } -#layout.minimal-layout div.content form { +#layout.minimal-layout div.content form.director-form { dt, dd { display: block; width: auto; @@ -839,7 +823,7 @@ ul.main-actions { } -fieldset { +form.director-form fieldset { margin: 0; padding: 0 0 1.5em 0; border: none; @@ -895,7 +879,7 @@ fieldset { /* BEGIN Forms */ -form dt label { +form.director-form dt label { width: auto; font-weight: normal; font-size: inherit; @@ -912,19 +896,19 @@ form dt label { } } -form fieldset { +form.director-form fieldset { min-width: 36em; } -form dd input.related-action[type='submit'] { +form.director-form dd input.related-action[type='submit'] { display: none; } -form dd.active li.active input.related-action[type='submit'] { +form.director-form dd.active li.active input.related-action[type='submit'] { display: inline-block; } -form { +form.director-form { p.description { color: @gray; font-style: italic; @@ -942,7 +926,7 @@ form { } } -form.db-selector { +form.director-form.db-selector { padding: 0; margin: 0; select { @@ -972,21 +956,21 @@ form.db-selector { flex-grow: 1; } -form dd { +form.director-form dd { padding: 0.3em 0.5em; margin: 0; } -form dt { +form.director-form dt { padding: 0.5em 0.5em; margin: 0; } -form dt.active, form dd.active { +form.director-form dt.active, form.director-form dd.active { background-color: @tr-active-color; } -form dt { +form.director-form dt { display: inline-block; vertical-align: top; min-width: 12em; @@ -997,11 +981,11 @@ form dt { } } -form .errors label { +form.director-form .errors label { color: @color-critical; } -form dd { +form.director-form dd { display: inline-block; width: 63%; min-height: 2.5em; @@ -1019,16 +1003,16 @@ form dd { } } -form dd:after { +form.director-form dd:after { display: block; content: ''; } -form textarea { +form.director-form textarea { height: auto; } -form dd ul.errors { +form.director-form dd ul.errors { list-style-type: none; padding-left: 0.3em; @@ -1038,7 +1022,7 @@ form dd ul.errors { } } -form div.hint { +form.director-form div.hint { padding: 1em; background-color: @tr-hover-color; margin: 1em 0; @@ -1672,13 +1656,13 @@ ul.filter-root { } -li.filter-chain > select.operator { +form.director-form li.filter-chain > select.operator { min-width: 5em; max-width: 5em; width: 5em; } -div.filter-expression { +form.director-form div.filter-expression { .column { min-width: 7em; max-width: 30em; @@ -1721,7 +1705,6 @@ ul.director-suggestions { overflow-y: auto; overflow-x: hidden; border: 1px solid @icinga-blue; - border-top: none; position: absolute; z-index: 2000; padding: 0; diff --git a/register-hooks.php b/register-hooks.php index a37aa706e..62fd5f51a 100644 --- a/register-hooks.php +++ b/register-hooks.php @@ -24,6 +24,7 @@ use Icinga\Module\Director\PropertyModifier\PropertyModifierArrayUnique; use Icinga\Module\Director\PropertyModifier\PropertyModifierBitmask; use Icinga\Module\Director\PropertyModifier\PropertyModifierCombine; +use Icinga\Module\Director\PropertyModifier\PropertyModifierDictionaryToRow; use Icinga\Module\Director\PropertyModifier\PropertyModifierDnsRecords; use Icinga\Module\Director\PropertyModifier\PropertyModifierExtractFromDN; use Icinga\Module\Director\PropertyModifier\PropertyModifierFromAdSid; @@ -45,6 +46,7 @@ use Icinga\Module\Director\PropertyModifier\PropertyModifierRejectOrSelect; use Icinga\Module\Director\PropertyModifier\PropertyModifierRenameColumn; use Icinga\Module\Director\PropertyModifier\PropertyModifierReplace; +use Icinga\Module\Director\PropertyModifier\PropertyModifierReplaceNull; use Icinga\Module\Director\PropertyModifier\PropertyModifierSimpleGroupBy; use Icinga\Module\Director\PropertyModifier\PropertyModifierSkipDuplicates; use Icinga\Module\Director\PropertyModifier\PropertyModifierSplit; @@ -61,10 +63,12 @@ use Icinga\Module\Director\ProvidedHook\IcingaDbCubeLinks; /** @var Module $this */ -$this->provideHook('monitoring/HostActions'); -$this->provideHook('monitoring/ServiceActions'); -$this->provideHook('cube/Actions', CubeLinks::class); -$this->provideHook('cube/IcingaDbActions', IcingaDbCubeLinks::class); +if ($this->getConfig()->get('frontend', 'disabled', 'no') !== 'yes') { + $this->provideHook('monitoring/HostActions'); + $this->provideHook('monitoring/ServiceActions'); + $this->provideHook('cube/Actions', CubeLinks::class); + $this->provideHook('cube/IcingaDbActions', IcingaDbCubeLinks::class); +} $directorHooks = [ 'director/DataType' => [ @@ -97,6 +101,7 @@ PropertyModifierArrayUnique::class, PropertyModifierBitmask::class, PropertyModifierCombine::class, + PropertyModifierDictionaryToRow::class, PropertyModifierDnsRecords::class, PropertyModifierExtractFromDN::class, PropertyModifierFromAdSid::class, @@ -118,6 +123,7 @@ PropertyModifierRejectOrSelect::class, PropertyModifierRenameColumn::class, PropertyModifierReplace::class, + PropertyModifierReplaceNull::class, PropertyModifierSimpleGroupBy::class, PropertyModifierSkipDuplicates::class, PropertyModifierSplit::class, diff --git a/schema/mysql-migrations/upgrade_179.sql b/schema/mysql-migrations/upgrade_179.sql new file mode 100644 index 000000000..8368b183a --- /dev/null +++ b/schema/mysql-migrations/upgrade_179.sql @@ -0,0 +1,5 @@ +ALTER TABLE director_deployment_log ADD INDEX (start_time); + +INSERT INTO director_schema_migration + (schema_version, migration_time) + VALUES ('179', NOW()); diff --git a/schema/mysql.sql b/schema/mysql.sql index 142b91cac..b8f65e8dd 100644 --- a/schema/mysql.sql +++ b/schema/mysql.sql @@ -167,6 +167,7 @@ CREATE TABLE director_deployment_log ( username VARCHAR(64) DEFAULT NULL COMMENT 'The user that triggered this deployment', startup_log MEDIUMTEXT DEFAULT NULL, PRIMARY KEY (id), + INDEX (start_time), CONSTRAINT config_checksum FOREIGN KEY config_checksum (config_checksum) REFERENCES director_generated_config (checksum) @@ -2414,4 +2415,4 @@ CREATE TABLE branched_icinga_dependency ( INSERT INTO director_schema_migration (schema_version, migration_time) - VALUES (178, NOW()); + VALUES (179, NOW()); diff --git a/schema/pgsql-migrations/upgrade_179.sql b/schema/pgsql-migrations/upgrade_179.sql new file mode 100644 index 000000000..d050eee18 --- /dev/null +++ b/schema/pgsql-migrations/upgrade_179.sql @@ -0,0 +1,5 @@ +CREATE INDEX start_time_idx ON director_deployment_log (start_time); + +INSERT INTO director_schema_migration + (schema_version, migration_time) + VALUES (179, NOW()); diff --git a/schema/pgsql.sql b/schema/pgsql.sql index 1fbe9e9e6..025ec5b4d 100644 --- a/schema/pgsql.sql +++ b/schema/pgsql.sql @@ -241,6 +241,8 @@ COMMENT ON COLUMN director_deployment_log.duration_connection IS 'The time it to COMMENT ON COLUMN director_deployment_log.duration_dump IS 'Time spent dumping the config (ms)'; COMMENT ON COLUMN director_deployment_log.username IS 'The user that triggered this deployment'; +CREATE INDEX start_time_idx ON director_deployment_log (start_time); + CREATE TABLE director_datalist ( id serial, @@ -2747,4 +2749,4 @@ CREATE INDEX branched_dependency_search_object_name ON branched_icinga_dependenc INSERT INTO director_schema_migration (schema_version, migration_time) - VALUES (178, NOW()); + VALUES (179, NOW());