summaryrefslogtreecommitdiffstats
path: root/library/Director/DirectorObject/Automation
diff options
context:
space:
mode:
Diffstat (limited to 'library/Director/DirectorObject/Automation')
-rw-r--r--library/Director/DirectorObject/Automation/Basket.php232
-rw-r--r--library/Director/DirectorObject/Automation/BasketContent.php24
-rw-r--r--library/Director/DirectorObject/Automation/BasketSnapshot.php531
-rw-r--r--library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php226
-rw-r--r--library/Director/DirectorObject/Automation/CompareBasketObject.php146
-rw-r--r--library/Director/DirectorObject/Automation/ExportInterface.php20
-rw-r--r--library/Director/DirectorObject/Automation/ImportExport.php149
7 files changed, 1328 insertions, 0 deletions
diff --git a/library/Director/DirectorObject/Automation/Basket.php b/library/Director/DirectorObject/Automation/Basket.php
new file mode 100644
index 0000000..f7eb8e5
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/Basket.php
@@ -0,0 +1,232 @@
+<?php
+
+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
+ *
+ * TODO
+ * - create a UUID like in RFC4122
+ */
+class Basket extends DbObject implements ExportInterface
+{
+ const SELECTION_ALL = true;
+ const SELECTION_NONE = false;
+
+ protected $table = 'director_basket';
+
+ protected $keyName = 'basket_name';
+
+ protected $chosenObjects = [];
+
+ protected $protectedFormerChosenObjects;
+
+ protected $defaultProperties = [
+ 'uuid' => null,
+ 'basket_name' => null,
+ 'objects' => null,
+ 'owner_type' => null,
+ 'owner_value' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'uuid'
+ ];
+
+ public function getHexUuid()
+ {
+ return bin2hex($this->get('uuid'));
+ }
+
+ public function listObjectTypes()
+ {
+ return array_keys($this->objects);
+ }
+
+ public function getChosenObjects()
+ {
+ return $this->chosenObjects;
+ }
+
+ public function isEmpty()
+ {
+ return count($this->getChosenObjects()) === 0;
+ }
+
+ protected function onLoadFromDb()
+ {
+ $this->chosenObjects = (array) Json::decode($this->get('objects'));
+ unset($this->chosenObjects['Datafield']); // Might be in old baskets
+ }
+
+ public function getUniqueIdentifier()
+ {
+ 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)) {
+ return false;
+ }
+
+ return is_array($this->chosenObjects[$type]);
+ }
+
+ public function setObjects($objects)
+ {
+ if (empty($objects)) {
+ $this->chosenObjects = [];
+ } else {
+ $this->protectedFormerChosenObjects = $this->chosenObjects;
+ $this->chosenObjects = [];
+ foreach ((array) $objects as $type => $object) {
+ $this->addObjects($type, $object);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * This is a weird method, as it is required to deal with raw form data
+ *
+ * @param $type
+ * @param ExportInterface[]|bool $objects
+ */
+ public function addObjects($type, $objects = true)
+ {
+ BasketSnapshot::assertValidType($type);
+ // '1' -> from Form!
+ if ($objects === 'ALL') {
+ $objects = true;
+ } elseif ($objects === null || $objects === 'IGNORE') {
+ return;
+ } elseif ($objects === '[]' || 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 === '[]') {
+ $objects = [];
+ }
+ }
+
+ if ($objects === true) {
+ $this->chosenObjects[$type] = true;
+ } elseif ($objects === '0') {
+ // nothing
+ } else {
+ foreach ($objects as $object) {
+ $this->addObject($type, $object);
+ }
+
+ if (array_key_exists($type, $this->chosenObjects)) {
+ ksort($this->chosenObjects[$type]);
+ }
+ }
+
+ $this->reallySet('objects', Json::encode($this->chosenObjects));
+ }
+
+ public function hasObject($type, $object)
+ {
+ if (! $this->hasType($type)) {
+ return false;
+ }
+
+ if ($this->chosenObjects[$type] === true) {
+ return true;
+ }
+
+ if ($object instanceof ExportInterface) {
+ $object = $object->getUniqueIdentifier();
+ }
+
+ if (is_array($this->chosenObjects[$type])) {
+ return in_array($object, $this->chosenObjects[$type]);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param $type
+ * @param string $object
+ */
+ public function addObject($type, $object)
+ {
+ if (is_array($this->chosenObjects[$type])) {
+ $this->chosenObjects[$type][] = $object;
+ } else {
+ throw new \InvalidArgumentException(sprintf(
+ 'The Basket "%s" has not been configured for single objects of type "%s"',
+ $this->get('basket_name'),
+ $type
+ ));
+ }
+ }
+
+ public function hasType($type)
+ {
+ return isset($this->chosenObjects[$type]);
+ }
+
+ protected function beforeStore()
+ {
+ if (! $this->hasBeenLoadedFromDb()) {
+ // TODO: This is BS, use a real UUID
+ $this->set('uuid', hex2bin(substr(sha1(microtime(true) . rand(1, 100000)), 0, 32)));
+ }
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/BasketContent.php b/library/Director/DirectorObject/Automation/BasketContent.php
new file mode 100644
index 0000000..e59c0ae
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/BasketContent.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Data\Db\DbObject;
+
+class BasketContent extends DbObject
+{
+ protected $objects;
+
+ protected $table = 'director_basket_content';
+
+ protected $keyName = 'checksum';
+
+ protected $defaultProperties = [
+ 'checksum' => null,
+ 'summary' => null,
+ 'content' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'checksum'
+ ];
+}
diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php
new file mode 100644
index 0000000..4ddf2ce
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php
@@ -0,0 +1,531 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use gipfl\Json\JsonEncodeException;
+use gipfl\Json\JsonString;
+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;
+use Icinga\Module\Director\Objects\DirectorDatafieldCategory;
+use Icinga\Module\Director\Objects\DirectorDatalist;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\IcingaCommand;
+use Icinga\Module\Director\Objects\IcingaDependency;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaNotification;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaService;
+use Icinga\Module\Director\Objects\IcingaServiceGroup;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoiceHost;
+use Icinga\Module\Director\Objects\IcingaTemplateChoiceService;
+use Icinga\Module\Director\Objects\IcingaTimePeriod;
+use Icinga\Module\Director\Objects\IcingaUser;
+use Icinga\Module\Director\Objects\IcingaUserGroup;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+use InvalidArgumentException;
+use RuntimeException;
+
+class BasketSnapshot extends DbObject
+{
+ protected static $typeClasses = [
+ 'DatafieldCategory' => DirectorDatafieldCategory::class,
+ 'Datafield' => DirectorDatafield::class,
+ 'TimePeriod' => IcingaTimePeriod::class,
+ 'CommandTemplate' => [IcingaCommand::class, ['object_type' => 'template']],
+ 'ExternalCommand' => [IcingaCommand::class, ['object_type' => 'external_object']],
+ 'Command' => [IcingaCommand::class, ['object_type' => 'object']],
+ 'HostGroup' => IcingaHostGroup::class,
+ 'IcingaTemplateChoiceHost' => IcingaTemplateChoiceHost::class,
+ 'HostTemplate' => IcingaHost::class,
+ 'ServiceGroup' => IcingaServiceGroup::class,
+ 'IcingaTemplateChoiceService' => IcingaTemplateChoiceService::class,
+ 'ServiceTemplate' => IcingaService::class,
+ 'ServiceSet' => IcingaServiceSet::class,
+ 'UserGroup' => IcingaUserGroup::class,
+ 'UserTemplate' => [IcingaUser::class, ['object_type' => 'template']],
+ 'User' => [IcingaUser::class, ['object_type' => 'object']],
+ 'NotificationTemplate' => IcingaNotification::class,
+ 'Notification' => [IcingaNotification::class, ['object_type' => 'apply']],
+ 'DataList' => DirectorDatalist::class,
+ 'Dependency' => IcingaDependency::class,
+ 'ImportSource' => ImportSource::class,
+ 'SyncRule' => SyncRule::class,
+ 'DirectorJob' => DirectorJob::class,
+ 'Basket' => Basket::class,
+ ];
+
+ protected $objects = [];
+
+ protected $content;
+
+ protected $table = 'director_basket_snapshot';
+
+ protected $keyName = [
+ 'basket_uuid',
+ 'ts_create',
+ ];
+
+ protected $restoreOrder = [
+ 'CommandTemplate',
+ 'ExternalCommand',
+ 'Command',
+ 'TimePeriod',
+ 'HostGroup',
+ 'IcingaTemplateChoiceHost',
+ 'HostTemplate',
+ 'ServiceGroup',
+ 'IcingaTemplateChoiceService',
+ 'ServiceTemplate',
+ 'ServiceSet',
+ 'UserGroup',
+ 'UserTemplate',
+ 'User',
+ 'NotificationTemplate',
+ 'Notification',
+ 'Dependency',
+ 'ImportSource',
+ 'SyncRule',
+ 'DirectorJob',
+ 'Basket',
+ ];
+
+ protected $defaultProperties = [
+ 'basket_uuid' => null,
+ 'content_checksum' => null,
+ 'ts_create' => null,
+ ];
+
+ protected $binaryProperties = [
+ 'basket_uuid',
+ 'content_checksum',
+ ];
+
+ public static function supports($type)
+ {
+ return isset(self::$typeClasses[$type]);
+ }
+
+ public static function assertValidType($type)
+ {
+ if (! static::supports($type)) {
+ throw new InvalidArgumentException("Basket does not support '$type'");
+ }
+ }
+
+ public static function getClassForType($type)
+ {
+ static::assertValidType($type);
+
+ if (is_array(self::$typeClasses[$type])) {
+ return self::$typeClasses[$type][0];
+ }
+
+ return self::$typeClasses[$type];
+ }
+
+ public static function getClassAndObjectTypeForType($type)
+ {
+ if (is_array(self::$typeClasses[$type])) {
+ return self::$typeClasses[$type];
+ }
+
+ return [self::$typeClasses[$type], null];
+ }
+
+ /**
+ * @param Basket $basket
+ * @param Db $db
+ * @return BasketSnapshot
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function createForBasket(Basket $basket, Db $db)
+ {
+ $snapshot = static::create([
+ 'basket_uuid' => $basket->get('uuid')
+ ], $db);
+ $snapshot->addObjectsChosenByBasket($basket);
+ $snapshot->resolveRequiredFields();
+
+ return $snapshot;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function resolveRequiredFields()
+ {
+ /** @var Db $db */
+ $db = $this->getConnection();
+ $fieldResolver = new BasketSnapshotFieldResolver($this->objects, $db);
+ /** @var DirectorDatafield[] $fields */
+ $fields = $fieldResolver->loadCurrentFields($db);
+ $categories = [];
+ if (! empty($fields)) {
+ $plain = [];
+ foreach ($fields as $id => $field) {
+ $plain[$id] = $field->export();
+ if ($category = $field->getCategory()) {
+ $categories[$category->get('category_name')] = $category->export();
+ }
+ }
+ $this->objects['Datafield'] = $plain;
+ }
+ if (! empty($categories)) {
+ $this->objects['DatafieldCategory'] = $categories;
+ }
+ }
+
+ protected function addObjectsChosenByBasket(Basket $basket)
+ {
+ foreach ($basket->getChosenObjects() as $typeName => $selection) {
+ if ($selection === true) {
+ $this->addAll($typeName);
+ } elseif (! empty($selection)) {
+ $this->addByIdentifiers($typeName, $selection);
+ }
+ }
+ }
+
+ /**
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function beforeStore()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ throw new RuntimeException('A basket snapshot cannot be modified');
+ }
+ $json = $this->getJsonDump();
+ $checksum = sha1($json, true);
+ if (! BasketContent::exists($checksum, $this->getConnection())) {
+ BasketContent::create([
+ 'checksum' => $checksum,
+ 'summary' => $this->getJsonSummary(),
+ 'content' => $json,
+ ], $this->getConnection())->store();
+ }
+
+ $this->set('content_checksum', $checksum);
+ $this->set('ts_create', round(microtime(true) * 1000));
+ }
+
+ /**
+ * @param Db $connection
+ * @param bool $replace
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function restoreTo(Db $connection, $replace = true)
+ {
+ static::restoreJson(
+ $this->getJsonDump(),
+ $connection,
+ $replace
+ );
+ }
+
+ /**
+ * @param Basket $basket
+ * @param $string
+ * @return BasketSnapshot
+ */
+ public static function forBasketFromJson(Basket $basket, $string)
+ {
+ $snapshot = static::create([
+ 'basket_uuid' => $basket->get('uuid')
+ ]);
+ $snapshot->objects = [];
+ foreach ((array) Json::decode($string) as $type => $objects) {
+ $snapshot->objects[$type] = (array) $objects;
+ }
+
+ return $snapshot;
+ }
+
+ public static function restoreJson($string, Db $connection, $replace = true)
+ {
+ $snapshot = new static();
+ $snapshot->restoreObjects(
+ Json::decode($string),
+ $connection,
+ $replace
+ );
+ }
+
+ /**
+ * @param $all
+ * @param Db $connection
+ * @param bool $replace
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ * @throws \Zend_Db_Adapter_Exception
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function restoreObjects($all, Db $connection, $replace = true)
+ {
+ $db = $connection->getDbAdapter();
+ $db->beginTransaction();
+ $fieldResolver = new BasketSnapshotFieldResolver($all, $connection);
+ $this->restoreType($all, 'DataList', $fieldResolver, $connection, $replace);
+ $this->restoreType($all, 'DatafieldCategory', $fieldResolver, $connection, $replace);
+ $fieldResolver->storeNewFields();
+ foreach ($this->restoreOrder as $typeName) {
+ $this->restoreType($all, $typeName, $fieldResolver, $connection, $replace);
+ }
+ $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
+ */
+ public function restoreType(
+ &$all,
+ $typeName,
+ BasketSnapshotFieldResolver $fieldResolver,
+ Db $connection,
+ $replace
+ ) {
+ if (isset($all->$typeName)) {
+ $objects = (array) $all->$typeName;
+ } else {
+ return;
+ }
+ $class = static::getClassForType($typeName);
+
+ $changed = [];
+ foreach ($objects as $key => $object) {
+ /** @var DbObject $new */
+ $new = $class::import($object, $connection, $replace);
+ if ($new->hasBeenModified()) {
+ if ($new instanceof IcingaObject && $new->supportsImports()) {
+ /** @var ExportInterface $new */
+ $changed[$new->getUniqueIdentifier()] = $new;
+ } else {
+ $new->store();
+ // Linking fields right now, as we're not in $changed
+ if ($new instanceof IcingaObject) {
+ $fieldResolver->relinkObjectFields($new, $object);
+ }
+ }
+ } else {
+ // No modification on the object, still, fields might have
+ // been changed
+ if ($new instanceof IcingaObject) {
+ $fieldResolver->relinkObjectFields($new, $object);
+ }
+ }
+ $allObjects[spl_object_hash($new)] = $object;
+ }
+
+ /** @var IcingaObject $object */
+ foreach ($changed as $object) {
+ $this->recursivelyStore($object, $changed);
+ }
+ foreach ($changed as $key => $new) {
+ // Store related fields. As objects might have formerly been
+ // un-stored, let's to it right here
+ if ($new instanceof IcingaObject) {
+ $fieldResolver->relinkObjectFields($new, $objects[$key]);
+ }
+ }
+ }
+
+ /**
+ * @param IcingaObject $object
+ * @param $list
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ protected function recursivelyStore(IcingaObject $object, &$list)
+ {
+ foreach ($object->listImportNames() as $parent) {
+ if (array_key_exists($parent, $list)) {
+ $this->recursivelyStore($list[$parent], $list);
+ }
+ }
+
+ $object->store();
+ }
+
+ /**
+ * @return BasketContent
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getContent()
+ {
+ if ($this->content === null) {
+ $this->content = BasketContent::load($this->get('content_checksum'), $this->getConnection());
+ }
+
+ return $this->content;
+ }
+
+ protected function onDelete()
+ {
+ $db = $this->getDb();
+ $db->delete(
+ ['bc' => 'director_basket_content'],
+ 'NOT EXISTS (SELECT director_basket_checksum WHERE content_checksum = bc.checksum)'
+ );
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getJsonSummary()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->getContent()->get('summary');
+ }
+
+ return Json::encode($this->getSummary(), JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * @return array|mixed
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getSummary()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ return Json::decode($this->getContent()->get('summary'));
+ }
+
+ $summary = [];
+ foreach (array_keys($this->objects) as $key) {
+ $summary[$key] = count($this->objects[$key]);
+ }
+
+ return $summary;
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getJsonDump()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ return $this->getContent()->get('content');
+ }
+
+ try {
+ return JsonString::encode($this->objects, JSON_PRETTY_PRINT);
+ } catch (JsonEncodeException $e) {
+ foreach ($this->objects as $type => $objects) {
+ foreach ($objects as $object) {
+ try {
+ JsonString::encode($object);
+ } catch (JsonEncodeException $singleError) {
+ $dump = var_export($object, 1);
+ if (function_exists('iconv')) {
+ $dump = iconv('UTF-8', 'UTF-8//IGNORE', $dump);
+ }
+ throw new JsonEncodeException(sprintf(
+ 'Failed to encode object ot type "%s": %s, %s',
+ $type,
+ $dump,
+ $singleError->getMessage()
+ ), $singleError->getCode());
+ }
+ }
+ }
+
+ throw $e;
+ }
+ }
+
+ protected function addAll($typeName)
+ {
+ list($class, $filter) = static::getClassAndObjectTypeForType($typeName);
+ $connection = $this->getConnection();
+ assert($connection instanceof Db);
+
+ /** @var IcingaObject $dummy */
+ $dummy = $class::create();
+ if ($dummy instanceof IcingaObject && $dummy->supportsImports()) {
+ $db = $this->getDb();
+ $select = $db->select()->from($dummy->getTableName());
+ if ($filter) {
+ foreach ($filter as $column => $value) {
+ $select->where("$column = ?", $value);
+ }
+ } elseif (! $dummy->isGroup()
+ // TODO: this is ugly.
+ && ! $dummy instanceof IcingaDependency
+ && ! $dummy instanceof IcingaTimePeriod
+ ) {
+ $select->where('object_type = ?', 'template');
+ }
+ $all = $class::loadAll($connection, $select);
+ } else {
+ $all = $class::loadAll($connection);
+ }
+ $exporter = new Exporter($connection);
+ foreach ($all as $object) {
+ $this->objects[$typeName][$object->getUniqueIdentifier()] = $exporter->export($object);
+ }
+ }
+
+ protected function addByIdentifiers($typeName, $identifiers)
+ {
+ foreach ($identifiers as $identifier) {
+ $this->addByIdentifier($typeName, $identifier);
+ }
+ }
+
+ /**
+ * @param $typeName
+ * @param $identifier
+ * @param Db $connection
+ * @return ExportInterface|DbObject|null
+ */
+ public static function instanceByIdentifier($typeName, $identifier, Db $connection)
+ {
+ $class = static::getClassForType($typeName);
+ if (substr($class, -13) === 'IcingaService') {
+ $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;
+ }
+
+ /**
+ * @param $typeName
+ * @param $identifier
+ */
+ 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] = $exporter->export($object);
+ }
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php
new file mode 100644
index 0000000..4653255
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Objects\DirectorDatafield;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class BasketSnapshotFieldResolver
+{
+ /** @var BasketSnapshot */
+ protected $snapshot;
+
+ /** @var \Icinga\Module\Director\Data\Db\DbConnection */
+ protected $targetDb;
+
+ /** @var array|null */
+ protected $requiredIds;
+
+ protected $objects;
+
+ /** @var int */
+ protected $nextNewId = 1;
+
+ /** @var array|null */
+ protected $idMap;
+
+ /** @var DirectorDatafield[]|null */
+ protected $targetFields;
+
+ public function __construct($objects, Db $targetDb)
+ {
+ $this->objects = $objects;
+ $this->targetDb = $targetDb;
+ }
+
+ /**
+ * @param Db $db
+ * @return DirectorDatafield[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function loadCurrentFields(Db $db)
+ {
+ $fields = [];
+ foreach ($this->getRequiredIds() as $id) {
+ $fields[$id] = DirectorDatafield::loadWithAutoIncId((int) $id, $db);
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\DuplicateKeyException
+ */
+ public function storeNewFields()
+ {
+ $this->targetFields = null; // Clear Cache
+ foreach ($this->getTargetFields() as $id => $field) {
+ if ($field->hasBeenModified()) {
+ $field->store();
+ $this->idMap[$id] = $field->get('id');
+ }
+ }
+ }
+
+ /**
+ * @param IcingaObject $new
+ * @param $object
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Zend_Db_Adapter_Exception
+ */
+ public function relinkObjectFields(IcingaObject $new, $object)
+ {
+ if (! $new->supportsFields() || ! isset($object->fields)) {
+ return;
+ }
+ $fieldMap = $this->getIdMap();
+
+ $objectId = (int) $new->get('id');
+ $table = $new->getTableName() . '_field';
+ $objectKey = $new->getShortTableName() . '_id';
+ $existingFields = [];
+
+ $db = $this->targetDb->getDbAdapter();
+
+ foreach ($db->fetchAll(
+ $db->select()->from($table)->where("$objectKey = ?", $objectId)
+ ) as $mapping) {
+ $existingFields[(int) $mapping->datafield_id] = $mapping;
+ }
+ foreach ($object->fields as $field) {
+ $id = $fieldMap[(int) $field->datafield_id];
+ if (isset($existingFields[$id])) {
+ unset($existingFields[$id]);
+ } else {
+ $db->insert($table, [
+ $objectKey => $objectId,
+ 'datafield_id' => $id,
+ 'is_required' => $field->is_required,
+ 'var_filter' => $field->var_filter,
+ ]);
+ }
+ }
+ if (! empty($existingFields)) {
+ $db->delete(
+ $table,
+ $db->quoteInto(
+ "$objectKey = $objectId AND datafield_id IN (?)",
+ array_keys($existingFields)
+ )
+ );
+ }
+ }
+
+ /**
+ * @param object $object
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function tweakTargetIds($object)
+ {
+ $forward = $this->getIdMap();
+ $map = array_flip($forward);
+ if (isset($object->fields)) {
+ foreach ($object->fields as $field) {
+ $id = $field->datafield_id;
+ if (isset($map[$id])) {
+ $field->datafield_id = $map[$id];
+ } else {
+ $field->datafield_id = "(NEW)";
+ }
+ }
+ }
+ }
+
+ /**
+ * @return int
+ */
+ protected function getNextNewId()
+ {
+ return $this->nextNewId++;
+ }
+
+ protected function getRequiredIds()
+ {
+ if ($this->requiredIds === null) {
+ if (isset($this->objects['Datafield'])) {
+ $this->requiredIds = array_keys($this->objects['Datafield']);
+ } else {
+ $ids = [];
+ foreach ($this->objects as $typeName => $objects) {
+ foreach ($objects as $key => $object) {
+ if (isset($object->fields)) {
+ foreach ($object->fields as $field) {
+ $ids[$field->datafield_id] = true;
+ }
+ }
+ }
+ }
+
+ $this->requiredIds = array_keys($ids);
+ }
+ }
+
+ return $this->requiredIds;
+ }
+
+ /**
+ * @param $type
+ * @return object[]
+ */
+ protected function getObjectsByType($type)
+ {
+ if (isset($this->objects->$type)) {
+ return (array) $this->objects->$type;
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * @return DirectorDatafield[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getTargetFields()
+ {
+ if ($this->targetFields === null) {
+ $this->calculateIdMap();
+ }
+
+ return $this->targetFields;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getIdMap()
+ {
+ if ($this->idMap === null) {
+ $this->calculateIdMap();
+ }
+
+ return $this->idMap;
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function calculateIdMap()
+ {
+ $this->idMap = [];
+ $this->targetFields = [];
+ foreach ($this->getObjectsByType('Datafield') as $id => $object) {
+ unset($object->category_id); // Fix old baskets
+ // Hint: import() doesn't store!
+ $new = DirectorDatafield::import($object, $this->targetDb);
+ if ($new->hasBeenLoadedFromDb()) {
+ $newId = (int) $new->get('id');
+ } else {
+ $newId = sprintf('NEW(%s)', $this->getNextNewId());
+ }
+ $this->idMap[$id] = $newId;
+ $this->targetFields[$id] = $new;
+ }
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/CompareBasketObject.php b/library/Director/DirectorObject/Automation/CompareBasketObject.php
new file mode 100644
index 0000000..ef2e9e2
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/CompareBasketObject.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Icinga\Module\Director\DirectorObject\Automation;
+
+use Icinga\Module\Director\Core\Json;
+use ipl\Html\Error;
+use RuntimeException;
+use function array_key_exists;
+use function is_array;
+use function is_object;
+use function is_scalar;
+
+class CompareBasketObject
+{
+ public static function normalize(&$value)
+ {
+ if (is_scalar($value)) {
+ return;
+ }
+ if (is_array($value)) {
+ foreach ($value as $k => &$v) {
+ static::normalize($v);
+ }
+ unset($v);
+ }
+ if (is_object($value)) {
+ $sorted = (array) $value;
+ // foreign baskets might not sort as we do:
+ ksort($sorted);
+ foreach ($sorted as $k => &$v) {
+ static::normalize($v);
+ }
+ unset($v);
+ $value = $sorted;
+
+ // foreign baskets might not sort those lists correctly:
+ if (isset($value->list_name) && isset($value->entries)) {
+ static::sortListBy('entry_name', $value->entries);
+ }
+ if (isset($value->fields)) {
+ static::sortListBy('datafield_id', $value->fields);
+ }
+ }
+ }
+
+ protected static function sortListBy($key, &$list)
+ {
+ usort($list, function ($a, $b) use ($key) {
+ return $a->$key > $b->$key ? -1 : 1;
+ });
+ }
+
+ public static function equals($a, $b)
+ {
+ if (is_scalar($a)) {
+ return $a === $b;
+ }
+
+ if ($a === null) {
+ return $b === null;
+ }
+
+ // Well... this is annoying :-/
+ $a = Json::decode(Json::encode($a, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
+ $b = Json::decode(Json::encode($b, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
+ if (is_array($a)) {
+ // Empty arrays VS empty objects :-( This is a fallback, not needed unless en/decode takes place
+ if (empty($a) && is_object($b) && (array) $b === []) {
+ return true;
+ }
+ if (! is_array($b)) {
+ return false;
+ }
+ if (array_keys($a) !== array_keys($b)) {
+ return false;
+ }
+ foreach ($a as $k => $v) {
+ if (array_key_exists($k, $b) && static::equals($b[$k], $v)) {
+ continue;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ if (is_object($a)) {
+ // Well... empty arrays VS empty objects :-(
+ if ($b === [] && (array) $a === []) {
+ return true;
+ }
+ if (! is_object($b)) {
+ return false;
+ }
+
+ // Workaround, same as above
+ if (isset($a->list_name) && isset($a->entries)) {
+ if (! isset($b->entries)) {
+ return false;
+ }
+ static::sortListBy('entry_name', $a->entries);
+ static::sortListBy('entry_name', $b->entries);
+ }
+ if (isset($a->fields) && isset($b->fields)) {
+ static::sortListBy('datafield_id', $a->fields);
+ static::sortListBy('datafield_id', $b->fields);
+ }
+ foreach ((array) $a as $k => $v) {
+ if (property_exists($b, $k) && static::equals($v, $b->$k)) {
+ continue;
+ }
+ if (! property_exists($b, $k)) {
+ if ($v === null) {
+ continue;
+ }
+ // Deal with two special defaults:
+ if ($k === 'set_if_format' && $v === 'string') {
+ continue;
+ }
+ if ($k === 'disabled' && $v === false) {
+ continue;
+ }
+ }
+ return false;
+ }
+ foreach ((array) $b as $k => $v) {
+ if (! property_exists($a, $k)) {
+ if ($v === null) {
+ continue;
+ }
+ // Once again:
+ if ($k === 'set_if_format' && $v === 'string') {
+ continue;
+ }
+ if ($k === 'disabled' && $v === false) {
+ continue;
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ throw new RuntimeException("Cannot compare " . Error::getPhpTypeName($a));
+ }
+}
diff --git a/library/Director/DirectorObject/Automation/ExportInterface.php b/library/Director/DirectorObject/Automation/ExportInterface.php
new file mode 100644
index 0000000..275dfed
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/ExportInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+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
new file mode 100644
index 0000000..a5e72fa
--- /dev/null
+++ b/library/Director/DirectorObject/Automation/ImportExport.php
@@ -0,0 +1,149 @@
+<?php
+
+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;
+use Icinga\Module\Director\Objects\DirectorJob;
+use Icinga\Module\Director\Objects\IcingaHostGroup;
+use Icinga\Module\Director\Objects\IcingaServiceGroup;
+use Icinga\Module\Director\Objects\IcingaServiceSet;
+use Icinga\Module\Director\Objects\IcingaTemplateChoiceHost;
+use Icinga\Module\Director\Objects\ImportSource;
+use Icinga\Module\Director\Objects\SyncRule;
+
+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()
+ {
+ $res = [];
+ foreach (IcingaServiceSet::loadAll($this->connection) as $object) {
+ if ($object->get('host_id')) {
+ continue;
+ }
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllHostTemplateChoices()
+ {
+ $res = [];
+ foreach (IcingaTemplateChoiceHost::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllHostGroups()
+ {
+ $res = [];
+ foreach (IcingaHostGroup::loadAll($this->connection) as $object) {
+ $res[] = $object->toPlainObject();
+ }
+
+ return $res;
+ }
+
+ public function serializeAllServiceGroups()
+ {
+ $res = [];
+ foreach (IcingaServiceGroup::loadAll($this->connection) as $object) {
+ $res[] = $object->toPlainObject();
+ }
+
+ return $res;
+ }
+
+ public function serializeAllDataFields()
+ {
+ $res = [];
+ foreach (DirectorDatafield::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllDataLists()
+ {
+ $res = [];
+ foreach (DirectorDatalist::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllJobs()
+ {
+ $res = [];
+ foreach (DirectorJob::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllImportSources()
+ {
+ $res = [];
+ foreach (ImportSource::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function serializeAllSyncRules()
+ {
+ $res = [];
+ foreach (SyncRule::loadAll($this->connection) as $object) {
+ $res[] = $this->exporter->export($object);
+ }
+
+ return $res;
+ }
+
+ public function unserializeImportSources($objects)
+ {
+ $count = 0;
+ $this->connection->runFailSafeTransaction(function () use ($objects, &$count) {
+ foreach ($objects as $object) {
+ ImportSource::import($object, $this->connection)->store();
+ $count++;
+ }
+ });
+
+ return $count;
+ }
+
+ public function unserializeSyncRules($objects)
+ {
+ $count = 0;
+ $this->connection->runFailSafeTransaction(function () use ($objects, &$count) {
+ foreach ($objects as $object) {
+ SyncRule::import($object, $this->connection)->store();
+ }
+ $count++;
+ });
+
+ return $count;
+ }
+}