From 5419d4428c86c488a43124f85e5407d7cbae6541 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 15:17:47 +0200 Subject: Adding upstream version 1.11.1. Signed-off-by: Daniel Baumann --- .../Director/DirectorObject/Automation/Basket.php | 70 ++---------- .../DirectorObject/Automation/BasketDiff.php | 121 +++++++++++++++++++++ .../DirectorObject/Automation/BasketSnapshot.php | 101 ++++++++--------- .../Automation/BasketSnapshotFieldResolver.php | 43 ++++++-- .../Automation/CompareBasketObject.php | 8 +- .../DirectorObject/Automation/ExportInterface.php | 10 -- .../DirectorObject/Automation/ImportExport.php | 7 +- .../DirectorObject/Lookup/ServiceFinder.php | 29 +---- .../Director/DirectorObject/ObjectPurgeHelper.php | 4 +- 9 files changed, 224 insertions(+), 169 deletions(-) create mode 100644 library/Director/DirectorObject/Automation/BasketDiff.php (limited to 'library/Director/DirectorObject') diff --git a/library/Director/DirectorObject/Automation/Basket.php b/library/Director/DirectorObject/Automation/Basket.php index f7eb8e5..81ae107 100644 --- a/library/Director/DirectorObject/Automation/Basket.php +++ b/library/Director/DirectorObject/Automation/Basket.php @@ -4,8 +4,6 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Data\Db\DbObject; -use Icinga\Module\Director\Db; -use Icinga\Module\Director\Exception\DuplicateKeyException; /** * Class Basket @@ -15,16 +13,17 @@ use Icinga\Module\Director\Exception\DuplicateKeyException; */ class Basket extends DbObject implements ExportInterface { - const SELECTION_ALL = true; - const SELECTION_NONE = false; + const SELECTION_ALL = 'ALL'; + const SELECTION_NONE = 'IGNORE'; + const SELECTION_CUSTOM = '[]'; protected $table = 'director_basket'; protected $keyName = 'basket_name'; - protected $chosenObjects = []; + protected $uuidColumn = 'uuid'; - protected $protectedFormerChosenObjects; + protected $chosenObjects = []; protected $defaultProperties = [ 'uuid' => null, @@ -69,44 +68,6 @@ class Basket extends DbObject implements ExportInterface return $this->get('basket_name'); } - public function export() - { - $result = $this->getProperties(); - unset($result['uuid']); - $result['objects'] = Json::decode($result['objects']); - ksort($result); - - return (object) $result; - } - - /** - * @param $plain - * @param Db $db - * @param bool $replace - * @return static - * @throws DuplicateKeyException - * @throws \Icinga\Exception\NotFoundError - */ - public static function import($plain, Db $db, $replace = false) - { - $properties = (array) $plain; - $name = $properties['basket_name']; - - if ($replace && static::exists($name, $db)) { - $object = static::load($name, $db); - } elseif (static::exists($name, $db)) { - throw new DuplicateKeyException( - 'Basket "%s" already exists', - $name - ); - } else { - $object = static::create([], $db); - } - $object->setProperties($properties); - - return $object; - } - public function supportsCustomSelectionFor($type) { if (! array_key_exists($type, $this->chosenObjects)) { @@ -121,7 +82,6 @@ class Basket extends DbObject implements ExportInterface if (empty($objects)) { $this->chosenObjects = []; } else { - $this->protectedFormerChosenObjects = $this->chosenObjects; $this->chosenObjects = []; foreach ((array) $objects as $type => $object) { $this->addObjects($type, $object); @@ -141,32 +101,22 @@ class Basket extends DbObject implements ExportInterface { BasketSnapshot::assertValidType($type); // '1' -> from Form! - if ($objects === 'ALL') { + if ($objects === self::SELECTION_ALL) { $objects = true; - } elseif ($objects === null || $objects === 'IGNORE') { + } elseif ($objects === null || $objects === self::SELECTION_NONE) { return; - } elseif ($objects === '[]' || is_array($objects)) { + } elseif ($objects === self::SELECTION_CUSTOM || is_array($objects)) { if (! isset($this->chosenObjects[$type]) || ! is_array($this->chosenObjects[$type])) { $this->chosenObjects[$type] = []; } - if (isset($this->protectedFormerChosenObjects[$type])) { - if (is_array($this->protectedFormerChosenObjects[$type])) { - $this->chosenObjects[$type] = $this->protectedFormerChosenObjects[$type]; - } else { - $this->chosenObjects[$type] = []; - } - } - - if ($objects === '[]') { + if ($objects === self::SELECTION_CUSTOM) { $objects = []; } } if ($objects === true) { $this->chosenObjects[$type] = true; - } elseif ($objects === '0') { - // nothing - } else { + } elseif ($objects !== '0') { // TODO: what would generate '0'? foreach ($objects as $object) { $this->addObject($type, $object); } diff --git a/library/Director/DirectorObject/Automation/BasketDiff.php b/library/Director/DirectorObject/Automation/BasketDiff.php new file mode 100644 index 0000000..8dbb423 --- /dev/null +++ b/library/Director/DirectorObject/Automation/BasketDiff.php @@ -0,0 +1,121 @@ +db = $db; + $this->importer = new ObjectImporter($db); + $this->exporter = new Exporter($db); + $this->snapshot = $snapshot; + } + + public function hasChangedFor(string $type, string $key, ?UuidInterface $uuid = null): bool + { + return $this->getCurrentString($type, $key, $uuid) !== $this->getBasketString($type, $key); + } + + public function getCurrentString(string $type, string $key, ?UuidInterface $uuid = null): string + { + $current = $this->getCurrent($type, $key, $uuid); + return $current ? JsonString::encode($current, JSON_PRETTY_PRINT) : ''; + } + + public function getBasketString(string $type, string $key): string + { + return JsonString::encode($this->getBasket($type, $key), JSON_PRETTY_PRINT); + } + + protected function getFieldResolver(): BasketSnapshotFieldResolver + { + if ($this->fieldResolver === null) { + $this->fieldResolver = new BasketSnapshotFieldResolver($this->getBasketObjects(), $this->db); + } + + return $this->fieldResolver; + } + + protected function getCurrent(string $type, string $key, ?UuidInterface $uuid = null): ?stdClass + { + if ($uuid && $current = BasketSnapshot::instanceByUuid($type, $uuid, $this->db)) { + $exported = $this->exporter->export($current); + $this->getFieldResolver()->tweakTargetIds($exported); + } elseif ($current = BasketSnapshot::instanceByIdentifier($type, $key, $this->db)) { + $exported = $this->exporter->export($current); + $this->getFieldResolver()->tweakTargetIds($exported); + } else { + $exported = null; + } + CompareBasketObject::normalize($exported); + + return $exported; + } + + protected function getBasket($type, $key): stdClass + { + $object = $this->getBasketObject($type, $key); + $fields = $object->fields ?? null; + $reExport = $this->exporter->export( + $this->importer->import(BasketSnapshot::getClassForType($type), $object) + ); + if ($fields === null) { + unset($reExport->fields); + } else { + $reExport->fields = $fields; + } + CompareBasketObject::normalize($reExport); + + return $reExport; + } + + public function hasCurrentInstance(string $type, string $key, ?UuidInterface $uuid = null): bool + { + return $this->getCurrentInstance($type, $key, $uuid) !== null; + } + + public function getCurrentInstance(string $type, string $key, ?UuidInterface $uuid = null) + { + if ($uuid && $instance = BasketSnapshot::instanceByUuid($type, $uuid, $this->db)) { + return $instance; + } else { + return BasketSnapshot::instanceByIdentifier($type, $key, $this->db); + } + } + + public function getBasketObjects(): stdClass + { + if ($this->objects === null) { + $this->objects = JsonString::decode($this->snapshot->getJsonDump()); + } + + return $this->objects; + } + + public function getBasketObject(string $type, string $key): stdClass + { + return $this->getBasketObjects()->$type->$key; + } +} diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php index 4ddf2ce..9638e49 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshot.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php @@ -2,10 +2,11 @@ namespace Icinga\Module\Director\DirectorObject\Automation; +use gipfl\Json\JsonDecodeException; use gipfl\Json\JsonEncodeException; use gipfl\Json\JsonString; -use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\ObjectImporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Objects\DirectorDatafield; @@ -29,7 +30,9 @@ use Icinga\Module\Director\Objects\IcingaUserGroup; use Icinga\Module\Director\Objects\ImportSource; use Icinga\Module\Director\Objects\SyncRule; use InvalidArgumentException; +use Ramsey\Uuid\UuidInterface; use RuntimeException; +use stdClass; class BasketSnapshot extends DbObject { @@ -217,16 +220,11 @@ class BasketSnapshot extends DbObject /** * @param Db $connection - * @param bool $replace * @throws \Icinga\Exception\NotFoundError */ - public function restoreTo(Db $connection, $replace = true) + public function restoreTo(Db $connection) { - static::restoreJson( - $this->getJsonDump(), - $connection, - $replace - ); + static::restoreJson($this->getJsonDump(), $connection); } /** @@ -240,61 +238,49 @@ class BasketSnapshot extends DbObject 'basket_uuid' => $basket->get('uuid') ]); $snapshot->objects = []; - foreach ((array) Json::decode($string) as $type => $objects) { + foreach ((array) JsonString::decode($string) as $type => $objects) { $snapshot->objects[$type] = (array) $objects; } return $snapshot; } - public static function restoreJson($string, Db $connection, $replace = true) + public static function restoreJson($string, Db $connection) { - $snapshot = new static(); - $snapshot->restoreObjects( - Json::decode($string), - $connection, - $replace - ); + (new static())->restoreObjects(JsonString::decode($string), $connection); } /** - * @param $all - * @param Db $connection - * @param bool $replace * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Zend_Db_Adapter_Exception * @throws \Icinga\Exception\NotFoundError + * @throws JsonDecodeException */ - protected function restoreObjects($all, Db $connection, $replace = true) + protected function restoreObjects(stdClass $all, Db $connection) { $db = $connection->getDbAdapter(); $db->beginTransaction(); $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); - $this->restoreType($all, 'DataList', $fieldResolver, $connection, $replace); - $this->restoreType($all, 'DatafieldCategory', $fieldResolver, $connection, $replace); + $this->restoreType($all, 'DataList', $fieldResolver, $connection); + $this->restoreType($all, 'DatafieldCategory', $fieldResolver, $connection); $fieldResolver->storeNewFields(); foreach ($this->restoreOrder as $typeName) { - $this->restoreType($all, $typeName, $fieldResolver, $connection, $replace); + $this->restoreType($all, $typeName, $fieldResolver, $connection); } $db->commit(); } /** - * @param $all - * @param $typeName - * @param BasketSnapshotFieldResolver $fieldResolver - * @param Db $connection - * @param $replace * @throws \Icinga\Exception\NotFoundError * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Zend_Db_Adapter_Exception + * @throws JsonDecodeException */ public function restoreType( - &$all, - $typeName, + stdClass $all, + string $typeName, BasketSnapshotFieldResolver $fieldResolver, - Db $connection, - $replace + Db $connection ) { if (isset($all->$typeName)) { $objects = (array) $all->$typeName; @@ -302,11 +288,10 @@ class BasketSnapshot extends DbObject return; } $class = static::getClassForType($typeName); - + $importer = new ObjectImporter($connection); $changed = []; - foreach ($objects as $key => $object) { - /** @var DbObject $new */ - $new = $class::import($object, $connection, $replace); + foreach ($objects as $object) { + $new = $importer->import($class, $object); if ($new->hasBeenModified()) { if ($new instanceof IcingaObject && $new->supportsImports()) { /** @var ExportInterface $new */ @@ -325,7 +310,6 @@ class BasketSnapshot extends DbObject $fieldResolver->relinkObjectFields($new, $object); } } - $allObjects[spl_object_hash($new)] = $object; } /** @var IcingaObject $object */ @@ -334,7 +318,7 @@ class BasketSnapshot extends DbObject } foreach ($changed as $key => $new) { // Store related fields. As objects might have formerly been - // un-stored, let's to it right here + // un-stored, let's do it right here if ($new instanceof IcingaObject) { $fieldResolver->relinkObjectFields($new, $objects[$key]); } @@ -358,10 +342,9 @@ class BasketSnapshot extends DbObject } /** - * @return BasketContent * @throws \Icinga\Exception\NotFoundError */ - protected function getContent() + protected function getContent(): BasketContent { if ($this->content === null) { $this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection()); @@ -380,26 +363,25 @@ class BasketSnapshot extends DbObject } /** - * @return string - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonEncodeException */ - public function getJsonSummary() + public function getJsonSummary(): string { if ($this->hasBeenLoadedFromDb()) { return $this->getContent()->get('summary'); } - return Json::encode($this->getSummary(), JSON_PRETTY_PRINT); + return JsonString::encode($this->getSummary(), JSON_PRETTY_PRINT); } /** * @return array|mixed - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonDecodeException */ public function getSummary() { if ($this->hasBeenLoadedFromDb()) { - return Json::decode($this->getContent()->get('summary')); + return JsonString::decode($this->getContent()->get('summary')); } $summary = []; @@ -412,7 +394,7 @@ class BasketSnapshot extends DbObject /** * @return string - * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\NotFoundError|JsonEncodeException */ public function getJsonDump() { @@ -428,7 +410,7 @@ class BasketSnapshot extends DbObject try { JsonString::encode($object); } catch (JsonEncodeException $singleError) { - $dump = var_export($object, 1); + $dump = var_export($object, true); if (function_exists('iconv')) { $dump = iconv('UTF-8', 'UTF-8//IGNORE', $dump); } @@ -485,6 +467,17 @@ class BasketSnapshot extends DbObject } } + /** + * @return ExportInterface|DbObject|null + */ + public static function instanceByUuid(string $typeName, UuidInterface $uuid, Db $connection) + { + /** @var class-string $class */ + $class = static::getClassForType($typeName); + /** @var ExportInterface $object */ + return $class::loadWithUniqueId($uuid, $connection); + } + /** * @param $typeName * @param $identifier @@ -493,21 +486,17 @@ class BasketSnapshot extends DbObject */ public static function instanceByIdentifier($typeName, $identifier, Db $connection) { + /** @var class-string $class */ $class = static::getClassForType($typeName); - if (substr($class, -13) === 'IcingaService') { + if ($class === IcingaService::class) { $identifier = [ 'object_type' => 'template', 'object_name' => $identifier, ]; } - /** @var ExportInterface $object */ - if ($class::exists($identifier, $connection)) { - $object = $class::load($identifier, $connection); - } else { - $object = null; - } - return $object; + /** @var ExportInterface $object */ + return $class::loadOptional($identifier, $connection); } /** diff --git a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php index 4653255..e565f77 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php @@ -4,7 +4,10 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\DirectorDatafield; +use Icinga\Module\Director\Objects\DirectorDatalist; use Icinga\Module\Director\Objects\IcingaObject; +use InvalidArgumentException; +use stdClass; class BasketSnapshotFieldResolver { @@ -39,7 +42,7 @@ class BasketSnapshotFieldResolver * @return DirectorDatafield[] * @throws \Icinga\Exception\NotFoundError */ - public function loadCurrentFields(Db $db) + public function loadCurrentFields(Db $db): array { $fields = []; foreach ($this->getRequiredIds() as $id) { @@ -90,6 +93,11 @@ class BasketSnapshotFieldResolver $existingFields[(int) $mapping->datafield_id] = $mapping; } foreach ($object->fields as $field) { + if (! isset($fieldMap[(int) $field->datafield_id])) { + throw new InvalidArgumentException( + 'Basket Snapshot contains invalid field reference: ' . $field->datafield_id + ); + } $id = $fieldMap[(int) $field->datafield_id]; if (isset($existingFields[$id])) { unset($existingFields[$id]); @@ -114,6 +122,8 @@ class BasketSnapshotFieldResolver } /** + * For diff purposes only, gives '(UNKNOWN)' for fields missing in our DB + * * @param object $object * @throws \Icinga\Exception\NotFoundError */ @@ -127,21 +137,34 @@ class BasketSnapshotFieldResolver if (isset($map[$id])) { $field->datafield_id = $map[$id]; } else { - $field->datafield_id = "(NEW)"; + $field->datafield_id = "(UNKNOWN)"; } } } } - /** - * @return int - */ - protected function getNextNewId() + public static function fixOptionalDatalistReference(stdClass $plain, Db $db) + { + if (isset($plain->settings->datalist_uuid)) { + unset($plain->settings->datalist); + return; + } + if (isset($plain->settings->datalist)) { + // Just try to load the list, final import will fail if missing + // No modification in case we do not find the list, + if ($list = DirectorDatalist::loadOptional($plain->settings->datalist, $db)) { + unset($plain->settings->datalist); + $plain->settings->datalist_id = $list->get('id'); + } + } + } + + protected function getNextNewId(): int { return $this->nextNewId++; } - protected function getRequiredIds() + protected function getRequiredIds(): array { if ($this->requiredIds === null) { if (isset($this->objects['Datafield'])) { @@ -169,7 +192,7 @@ class BasketSnapshotFieldResolver * @param $type * @return object[] */ - protected function getObjectsByType($type) + protected function getObjectsByType($type): array { if (isset($this->objects->$type)) { return (array) $this->objects->$type; @@ -182,7 +205,7 @@ class BasketSnapshotFieldResolver * @return DirectorDatafield[] * @throws \Icinga\Exception\NotFoundError */ - protected function getTargetFields() + protected function getTargetFields(): array { if ($this->targetFields === null) { $this->calculateIdMap(); @@ -194,7 +217,7 @@ class BasketSnapshotFieldResolver /** * @throws \Icinga\Exception\NotFoundError */ - protected function getIdMap() + protected function getIdMap(): array { if ($this->idMap === null) { $this->calculateIdMap(); diff --git a/library/Director/DirectorObject/Automation/CompareBasketObject.php b/library/Director/DirectorObject/Automation/CompareBasketObject.php index ef2e9e2..f1ab6a9 100644 --- a/library/Director/DirectorObject/Automation/CompareBasketObject.php +++ b/library/Director/DirectorObject/Automation/CompareBasketObject.php @@ -31,7 +31,7 @@ class CompareBasketObject static::normalize($v); } unset($v); - $value = $sorted; + $value = (object) $sorted; // foreign baskets might not sort those lists correctly: if (isset($value->list_name) && isset($value->entries)) { @@ -46,7 +46,11 @@ class CompareBasketObject protected static function sortListBy($key, &$list) { usort($list, function ($a, $b) use ($key) { - return $a->$key > $b->$key ? -1 : 1; + if (is_array($a)) { + return $a[$key] > $b[$key] ? -1 : 1; + } else { + return $a->$key > $b->$key ? -1 : 1; + } }); } diff --git a/library/Director/DirectorObject/Automation/ExportInterface.php b/library/Director/DirectorObject/Automation/ExportInterface.php index 275dfed..271824f 100644 --- a/library/Director/DirectorObject/Automation/ExportInterface.php +++ b/library/Director/DirectorObject/Automation/ExportInterface.php @@ -2,18 +2,8 @@ namespace Icinga\Module\Director\DirectorObject\Automation; -use Icinga\Module\Director\Db; - interface ExportInterface { - /** - * @deprecated - * @return \stdClass - */ - public function export(); - - public static function import($plain, Db $db, $replace = false); - // TODO: // public function getXyzChecksum(); public function getUniqueIdentifier(); diff --git a/library/Director/DirectorObject/Automation/ImportExport.php b/library/Director/DirectorObject/Automation/ImportExport.php index a5e72fa..1664f5d 100644 --- a/library/Director/DirectorObject/Automation/ImportExport.php +++ b/library/Director/DirectorObject/Automation/ImportExport.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\DirectorObject\Automation; use Icinga\Module\Director\Data\Exporter; +use Icinga\Module\Director\Data\ObjectImporter; use Icinga\Module\Director\Db; use Icinga\Module\Director\Objects\DirectorDatafield; use Icinga\Module\Director\Objects\DirectorDatalist; @@ -125,8 +126,9 @@ class ImportExport { $count = 0; $this->connection->runFailSafeTransaction(function () use ($objects, &$count) { + $importer = new ObjectImporter($this->connection); foreach ($objects as $object) { - ImportSource::import($object, $this->connection)->store(); + $importer->import(ImportSource::class, $object)->store(); $count++; } }); @@ -138,8 +140,9 @@ class ImportExport { $count = 0; $this->connection->runFailSafeTransaction(function () use ($objects, &$count) { + $importer = new ObjectImporter($this->connection); foreach ($objects as $object) { - SyncRule::import($object, $this->connection)->store(); + $importer->import(SyncRule::class, $object)->store(); } $count++; }); diff --git a/library/Director/DirectorObject/Lookup/ServiceFinder.php b/library/Director/DirectorObject/Lookup/ServiceFinder.php index fb8d74c..a14d853 100644 --- a/library/Director/DirectorObject/Lookup/ServiceFinder.php +++ b/library/Director/DirectorObject/Lookup/ServiceFinder.php @@ -4,6 +4,8 @@ namespace Icinga\Module\Director\DirectorObject\Lookup; use gipfl\IcingaWeb2\Url; use Icinga\Authentication\Auth; +use Icinga\Module\Director\Auth\Permission; +use Icinga\Module\Director\Integration\MonitoringModule\Monitoring; use Icinga\Module\Director\Objects\HostApplyMatches; use Icinga\Module\Director\Objects\IcingaHost; use RuntimeException; @@ -49,31 +51,4 @@ class ServiceFinder return false; } - - /** - * @param $serviceName - * @return Url - */ - 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(); - } - } - if ($this->auth->hasPermission('director/monitoring/services-ro')) { - return Url::fromPath('director/host/servicesro', [ - 'name' => $this->host->getObjectName(), - 'service' => $serviceName - ]); - } - - return Url::fromPath('director/host/invalidservice', [ - 'name' => $this->host->getObjectName(), - 'service' => $serviceName, - ]); - } } diff --git a/library/Director/DirectorObject/ObjectPurgeHelper.php b/library/Director/DirectorObject/ObjectPurgeHelper.php index a043965..5e50727 100644 --- a/library/Director/DirectorObject/ObjectPurgeHelper.php +++ b/library/Director/DirectorObject/ObjectPurgeHelper.php @@ -44,11 +44,11 @@ class ObjectPurgeHelper // TODO: this is object-specific and to be found in the ::import() function! unset($properties['fields']); $object = $class::fromPlainObject($properties); - } elseif (\get_class($object) !== $class) { + } elseif (get_class($object) !== $class) { throw new InvalidArgumentException( 'Can keep only matching objects, expected "%s", got "%s', $class, - \get_class($keep) + get_class($object) ); } $key = []; -- cgit v1.2.3