diff options
Diffstat (limited to 'library/Director/Data')
-rw-r--r-- | library/Director/Data/AssignFilterHelper.php | 2 | ||||
-rw-r--r-- | library/Director/Data/Db/DbDataFormatter.php | 16 | ||||
-rw-r--r-- | library/Director/Data/Db/DbObject.php | 73 | ||||
-rw-r--r-- | library/Director/Data/Db/ServiceSetQueryBuilder.php | 2 | ||||
-rw-r--r-- | library/Director/Data/Exporter.php | 45 | ||||
-rw-r--r-- | library/Director/Data/FieldReferenceLoader.php | 4 | ||||
-rw-r--r-- | library/Director/Data/HostServiceLoader.php | 12 | ||||
-rw-r--r-- | library/Director/Data/ObjectImporter.php | 168 | ||||
-rw-r--r-- | library/Director/Data/PropertyMangler.php | 4 |
9 files changed, 295 insertions, 31 deletions
diff --git a/library/Director/Data/AssignFilterHelper.php b/library/Director/Data/AssignFilterHelper.php index b0253cf..7448c51 100644 --- a/library/Director/Data/AssignFilterHelper.php +++ b/library/Director/Data/AssignFilterHelper.php @@ -135,7 +135,7 @@ class AssignFilterHelper $parts = array(); foreach (preg_split('~\*~', $expression) as $part) { - $parts[] = preg_quote($part); + $parts[] = preg_quote($part, '/'); } // match() is case insensitive $pattern = '/^' . implode('.*', $parts) . '$/i'; diff --git a/library/Director/Data/Db/DbDataFormatter.php b/library/Director/Data/Db/DbDataFormatter.php index d6e4eeb..91fc776 100644 --- a/library/Director/Data/Db/DbDataFormatter.php +++ b/library/Director/Data/Db/DbDataFormatter.php @@ -6,7 +6,7 @@ use InvalidArgumentException; class DbDataFormatter { - public static function normalizeBoolean($value) + public static function normalizeBoolean($value): ?string { if ($value === 'y' || $value === '1' || $value === true || $value === 1) { return 'y'; @@ -20,7 +20,19 @@ class DbDataFormatter throw new InvalidArgumentException(sprintf( 'Got invalid boolean: %s', - var_export($value, 1) + var_export($value, true) )); } + + public static function booleanForDbValue($value): ?bool + { + if ($value === 'y') { + return true; + } + if ($value === 'n') { + return false; + } + + return $value; // let this fail elsewhere, if not null + } } diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php index 6ecae8b..114b61b 100644 --- a/library/Director/Data/Db/DbObject.php +++ b/library/Director/Data/Db/DbObject.php @@ -80,6 +80,9 @@ abstract class DbObject protected $binaryProperties = []; + /* key/value!! */ + protected $booleans = []; + /** * Filled with object instances when prefetchAll is used */ @@ -346,6 +349,16 @@ abstract class DbObject return $this->$func($value); } + if ($this->getUuidColumn() === $key) { + if (strlen($value) > 16) { + $value = Uuid::fromString($value)->getBytes(); + } + } + + if ($this->propertyIsBoolean($key)) { + $value = DbDataFormatter::normalizeBoolean($value); + } + if (! $this->hasProperty($key)) { throw new InvalidArgumentException(sprintf( 'Trying to set invalid key "%s"', @@ -372,7 +385,10 @@ abstract class DbObject return $this; } if ($key === 'id' || substr($key, -3) === '_id') { - if ((int) $value === (int) $this->properties[$key]) { + if ($value !== null + && $this->properties[$key] !== null + && (int) $value === (int) $this->properties[$key] + ) { return $this; } } @@ -553,7 +569,7 @@ abstract class DbObject /** * Unique key name * - * @return string + * @return string|array */ public function getKeyName() { @@ -706,8 +722,7 @@ abstract class DbObject */ protected function loadFromDb() { - $select = $this->db->select()->from($this->table)->where($this->createWhere()); - $properties = $this->db->fetchRow($select); + $properties = $this->db->fetchRow($this->prepareObjectQuery()); if (empty($properties)) { if (is_array($this->getKeyName())) { @@ -728,6 +743,11 @@ abstract class DbObject return $this->setDbProperties($properties); } + public function prepareObjectQuery() + { + return $this->db->select()->from($this->table)->where($this->createWhere()); + } + /** * @param object|array $row * @param Db $db @@ -878,6 +898,11 @@ abstract class DbObject return in_array($column, $this->binaryProperties) || $this->getUuidColumn() === $column; } + public function propertyIsBoolean($property) + { + return array_key_exists($property, $this->booleans); + } + /** * Store object to database * @@ -959,7 +984,7 @@ abstract class DbObject $this->table, $this->getLogId(), $e->getMessage(), - var_export($this->getProperties(), 1) // TODO: Remove properties + var_export($this->getProperties(), true) // TODO: Remove properties )); } @@ -1027,7 +1052,7 @@ abstract class DbObject if ($this->hasUuidColumn() && $this->properties[$this->uuidColumn] !== null) { return $this->db->quoteInto( sprintf('%s = ?', $this->getUuidColumn()), - $this->connection->quoteBinary($this->getUniqueId()->getBytes()) + $this->connection->quoteBinary($this->getOriginalProperty($this->uuidColumn)) ); } if ($id = $this->getAutoincId()) { @@ -1302,6 +1327,40 @@ abstract class DbObject } /** + * @param $id + * @param DbConnection $connection + * @return static + */ + public static function loadOptional($id, DbConnection $connection): ?DbObject + { + if ($prefetched = static::getPrefetched($id)) { + return $prefetched; + } + /** @var DbObject $obj */ + $obj = new static(); + + if (self::$dbObjectStore !== null && $obj->hasUuidColumn()) { + $table = $obj->getTableName(); + assert($connection instanceof Db); + $uuid = UuidLookup::findUuidForKey($id, $table, $connection, self::$dbObjectStore->getBranch()); + if ($uuid) { + return self::$dbObjectStore->load($table, $uuid); + } + + return null; + } + + $obj->setConnection($connection)->setKey($id); + $properties = $connection->getDbAdapter()->fetchRow($obj->prepareObjectQuery()); + if (empty($properties)) { + return null; + } + + $obj->setDbProperties($properties); + return $obj; + } + + /** * @param DbConnection $connection * @param \Zend_Db_Select $query * @param string|null $keyColumn @@ -1436,7 +1495,7 @@ abstract class DbObject )); } - public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection) + public static function loadWithUniqueId(UuidInterface $uuid, DbConnection $connection): ?DbObject { $db = $connection->getDbAdapter(); $obj = new static; diff --git a/library/Director/Data/Db/ServiceSetQueryBuilder.php b/library/Director/Data/Db/ServiceSetQueryBuilder.php index 7841d1e..597fe0e 100644 --- a/library/Director/Data/Db/ServiceSetQueryBuilder.php +++ b/library/Director/Data/Db/ServiceSetQueryBuilder.php @@ -27,6 +27,8 @@ class ServiceSetQueryBuilder /** @var \Zend_Db_Adapter_Abstract */ protected $db; + protected $searchColumns = []; + /** * @param ?UuidInterface $uuid */ diff --git a/library/Director/Data/Exporter.php b/library/Director/Data/Exporter.php index a2e3191..1a3cfcb 100644 --- a/library/Director/Data/Exporter.php +++ b/library/Director/Data/Exporter.php @@ -2,10 +2,14 @@ namespace Icinga\Module\Director\Data; +use gipfl\Json\JsonString; use gipfl\ZfDb\Adapter\Adapter; +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Data\Db\DbDataFormatter; use Icinga\Module\Director\Data\Db\DbObject; use Icinga\Module\Director\Data\Db\DbObjectWithSettings; use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\Basket; use Icinga\Module\Director\Objects\DirectorDatafield; use Icinga\Module\Director\Objects\DirectorDatalist; use Icinga\Module\Director\Objects\DirectorDatalistEntry; @@ -18,6 +22,7 @@ use Icinga\Module\Director\Objects\IcingaTemplateChoice; use Icinga\Module\Director\Objects\ImportSource; use Icinga\Module\Director\Objects\InstantiatedViaHook; use Icinga\Module\Director\Objects\SyncRule; +use Ramsey\Uuid\Uuid; use RuntimeException; class Exporter @@ -68,6 +73,11 @@ class Exporter $props = $chosen; } + if ($column = $object->getUuidColumn()) { + if ($uuid = $object->get($column)) { + $props[$column] = Uuid::fromBytes($uuid)->toString(); + } + } ksort($props); return (object) $props; @@ -152,10 +162,10 @@ class Exporter throw new RuntimeException('Not yet'); } $props['services'] = []; - foreach ($object->getServiceObjects() as $serviceObject) { - $props['services'][$serviceObject->getObjectName()] = $this->export($serviceObject); + foreach ($object->getServices() as $serviceObject) { + $props['services'][] = $this->export($serviceObject); } - ksort($props['services']); + usort($props['services'], [$this, 'sortByName']); } elseif ($object instanceof IcingaHost) { if ($this->exportHostServices) { $services = []; @@ -168,10 +178,15 @@ class Exporter } } + protected function sortByName($left, $right) + { + return $left->object_name < $right->object_name ? '-1' : '1'; + } + public function serviceLoader() { if ($this->serviceLoader === null) { - $this->serviceLoader = new HostServiceLoader($this->connection); + $this->serviceLoader = new HostServiceLoader($this->connection, Auth::getInstance()); $this->serviceLoader->resolveObjects($this->resolveObjects); } @@ -241,6 +256,12 @@ class Exporter protected function exportDbObject(DbObject $object) { $props = $object->getProperties(); + foreach ($props as $key => &$value) { + if ($object->propertyIsBoolean($key)) { + $value = DbDataFormatter::booleanForDbValue($value); + } + } + unset($value); if ($object instanceof DbObjectWithSettings) { if ($object instanceof InstantiatedViaHook) { $props['settings'] = (object) $object->getInstance()->exportSettings(); @@ -248,6 +269,11 @@ class Exporter $props['settings'] = (object) $object->getSettings(); // Already sorted } } + if ($object instanceof Basket) { + if (isset($props['objects']) && is_string($props['objects'])) { + $props['objects'] = JsonString::decode($props['objects']); + } + } unset($props['uuid']); // Not yet if (! $this->showDefaults) { foreach ($props as $key => $value) { @@ -279,16 +305,7 @@ class Exporter 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) { + foreach ($list->getEntries() as $name => $entry) { if ($entry->shouldBeRemoved()) { continue; } diff --git a/library/Director/Data/FieldReferenceLoader.php b/library/Director/Data/FieldReferenceLoader.php index 1e3d92e..99a9925 100644 --- a/library/Director/Data/FieldReferenceLoader.php +++ b/library/Director/Data/FieldReferenceLoader.php @@ -29,12 +29,12 @@ class FieldReferenceLoader } $type = $object->getShortTableName(); $res = $db->fetchAll( - $db->select()->from(['f' => "icinga_${type}_field"], [ + $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) + ->where("{$type}_id = ?", (int) $id) ->order('varname ASC') ); diff --git a/library/Director/Data/HostServiceLoader.php b/library/Director/Data/HostServiceLoader.php index 4cc4b96..c8bd8b9 100644 --- a/library/Director/Data/HostServiceLoader.php +++ b/library/Director/Data/HostServiceLoader.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Director\Data; use gipfl\IcingaWeb2\Table\QueryBasedTable; use gipfl\ZfDb\Select; +use Icinga\Authentication\Auth; use Icinga\Data\SimpleQuery; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\AppliedServiceSetLoader; @@ -26,21 +27,26 @@ class HostServiceLoader /** @var \Zend_Db_Adapter_Abstract */ protected $db; + /** @var Auth */ + protected $auth; + /** @var bool */ protected $resolveHostServices = false; /** @var bool */ protected $resolveObjects = false; - public function __construct(Db $connection) + public function __construct(Db $connection, Auth $auth) { $this->connection = $connection; $this->db = $connection->getDbAdapter(); + $this->auth = $auth; } public function fetchServicesForHost(IcingaHost $host) { - $table = (new ObjectsTableService($this->connection))->setHost($host); + $table = (new ObjectsTableService($this->connection, $this->auth)) + ->setHost($host); $services = $this->fetchServicesForTable($table); if ($this->resolveHostServices) { foreach ($this->fetchAllServicesForHost($host) as $service) { @@ -69,7 +75,7 @@ class HostServiceLoader /** @var IcingaHost[] $parents */ $parents = IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true); foreach ($parents as $parent) { - $table = (new ObjectsTableService($this->connection)) + $table = (new ObjectsTableService($this->connection, $this->auth)) ->setHost($parent) ->setInheritedBy($host); foreach ($this->fetchServicesForTable($table) as $service) { diff --git a/library/Director/Data/ObjectImporter.php b/library/Director/Data/ObjectImporter.php new file mode 100644 index 0000000..231ad1c --- /dev/null +++ b/library/Director/Data/ObjectImporter.php @@ -0,0 +1,168 @@ +<?php + +namespace Icinga\Module\Director\Data; + +use gipfl\Json\JsonDecodeException; +use gipfl\Json\JsonString; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\Basket; +use Icinga\Module\Director\Objects\DirectorJob; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaService; +use Icinga\Module\Director\Objects\IcingaServiceSet; +use Icinga\Module\Director\Objects\ImportSource; +use Icinga\Module\Director\Objects\SyncRule; +use InvalidArgumentException; +use Ramsey\Uuid\Uuid; +use stdClass; + +class ObjectImporter +{ + protected static $templatesOnly = [ + IcingaHost::class, + IcingaService::class, + IcingaServiceSet::class, + ]; + + /** @var Db */ + protected $db; + + public function __construct(Db $db) + { + $this->db = $db; + } + + /** + * @param class-string|DbObject $implementation + * @param stdClass $plain + * @return DbObject + * @throws JsonDecodeException + */ + public function import(string $implementation, stdClass $plain): DbObject + { + $this->assertTemplate($implementation, $plain); + $this->fixRelations($implementation, $plain); + $this->applyOtherWorkarounds($implementation, $plain); + $this->fixLegacyBaskets($implementation, $plain); + $this->fixSubObjects($implementation, $plain); + + $object = $this->loadExistingObject($implementation, $plain); + if ($object === null) { + $object = $implementation::create([], $this->db); + } + + $properties = (array) $plain; + unset($properties['fields']); + unset($properties['originalId']); + if ($implementation === Basket::class) { + if (isset($properties['objects']) && is_string($properties['objects'])) { + $properties['objects'] = JsonString::decode($properties['objects']); + } + } + $object->setProperties($properties); + + return $object; + } + + protected function fixLegacyBaskets(string $implementation, stdClass $plain) + { + // TODO: Check, whether current export sets modifiers = [] in case there is none + if ($implementation == ImportSource::class) { + if (!isset($plain->modifiers)) { + $plain->modifiers = []; + } + } + } + + protected function applyOtherWorkarounds(string $implementation, stdClass $plain) + { + if ($implementation === SyncRule::class) { + if (isset($plain->properties)) { + $plain->syncProperties = $plain->properties; + unset($plain->properties); + } + } + } + + protected function fixSubObjects(string $implementation, stdClass $plain) + { + if ($implementation === IcingaServiceSet::class) { + foreach ($plain->services as $service) { + unset($service->fields); + } + // Hint: legacy baskets are carrying service names as object keys, new baskets have arrays + $plain->services = array_values((array) $plain->services); + } + } + + protected function fixRelations(string $implementation, stdClass $plain) + { + if ($implementation === DirectorJob::class) { + $settings = $plain->settings; + $source = $settings->source ?? null; + if ($source && !isset($settings->source_id)) { + $settings->source_id = ImportSource::load($source, $this->db)->get('id'); + unset($settings->source); + } + $rule = $settings->rule ?? null; + if ($rule && !isset($settings->rule_id)) { + $settings->rule_id = SyncRule::load($rule, $this->db)->get('id'); + unset($settings->rule); + } + } + } + + /** + * @param class-string<DbObject> $implementation + * @param stdClass $plain + * @return DbObject|null + */ + protected function loadExistingObject(string $implementation, stdClass $plain): ?DbObject + { + if (isset($plain->uuid) + && $instance = $implementation::loadWithUniqueId(Uuid::fromString($plain->uuid), $this->db) + ) { + return $instance; + } + + if ($implementation === IcingaService::class) { + $key = [ + 'object_type' => 'template', + 'object_name' => $plain->object_name + ]; + } else { + $dummy = $implementation::create(); + $keyColumn = $dummy->getKeyName(); + if (is_array($keyColumn)) { + if (empty($keyColumn)) { + throw new \RuntimeException("$implementation has an empty keyColumn array"); + } + $key = []; + foreach ($keyColumn as $column) { + if (isset($plain->$column)) { + $key[$column] = $plain->$column; + } + } + } else { + $key = $plain->$keyColumn; + } + } + + return $implementation::loadOptional($key, $this->db); + } + + protected function assertTemplate(string $implementation, stdClass $plain) + { + if (! in_array($implementation, self::$templatesOnly)) { + return; + } + if ($plain->object_type !== 'template') { + throw new InvalidArgumentException(sprintf( + 'Can import only Templates, got "%s" for "%s"', + $plain->object_type, + $plain->name + )); + } + } +} diff --git a/library/Director/Data/PropertyMangler.php b/library/Director/Data/PropertyMangler.php index a457f1d..40b2570 100644 --- a/library/Director/Data/PropertyMangler.php +++ b/library/Director/Data/PropertyMangler.php @@ -19,7 +19,7 @@ class PropertyMangler throw new InvalidArgumentException(sprintf( 'I can only append to arrays, %s is %s', $key, - var_export($current, 1) + var_export($current, true) )); } @@ -52,7 +52,7 @@ class PropertyMangler throw new InvalidArgumentException(sprintf( 'I can only remove strings or from arrays, %s is %s', $key, - var_export($current, 1) + var_export($current, true) )); } } |