diff options
Diffstat (limited to 'library/Director/DirectorObject')
15 files changed, 2136 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; + } +} diff --git a/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php b/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php new file mode 100644 index 0000000..abda497 --- /dev/null +++ b/library/Director/DirectorObject/Lookup/AppliedServiceInfo.php @@ -0,0 +1,109 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +use gipfl\IcingaWeb2\Url; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\HostApplyMatches; +use Icinga\Module\Director\Objects\IcingaHost; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; + +/** + * A Service Apply Rule matching this Host, generating a Service with the given + * name + */ +class AppliedServiceInfo implements ServiceInfo +{ + /** @var string */ + protected $hostName; + + /** @var string */ + protected $serviceName; + + /** @var int */ + protected $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) + { + $matcher = HostApplyMatches::prepare($host); + $connection = $host->getConnection(); + foreach (static::fetchApplyRulesByServiceName($connection, $serviceName) as $rule) { + if ($matcher->matchesFilter($rule->filter)) { + return new static($host->getObjectName(), $serviceName, (int) $rule->id, $rule->uuid); + } + } + + return null; + } + + public function getHostName() + { + return $this->hostName; + } + + /** + * @return int + */ + public function getServiceApplyRuleId() + { + return $this->serviceApplyRuleId; + } + + public function getName() + { + return $this->serviceName; + } + + public function getUuid() + { + return $this->uuid; + } + + public function getUrl() + { + return Url::fromPath('director/host/appliedservice', [ + 'name' => $this->hostName, + 'service_id' => $this->serviceApplyRuleId, + ]); + } + + public function requiresOverrides() + { + return true; + } + + protected static function fetchApplyRulesByServiceName(Db $connection, $serviceName) + { + $db = $connection->getDbAdapter(); + $query = $db->select() + ->from(['s' => 'icinga_service'], [ + 'id' => 's.id', + 'uuid' => 's.uuid', + 'name' => 's.object_name', + 'assign_filter' => 's.assign_filter', + ]) + ->where('object_name = ?', $serviceName) + ->where('object_type = ? AND assign_filter IS NOT NULL', 'apply'); + + $allRules = $db->fetchAll($query); + foreach ($allRules as $rule) { + $rule->uuid = Uuid::fromBytes(Db\DbUtil::binaryResult($rule->uuid)); + $rule->filter = Filter::fromQueryString($rule->assign_filter); + } + + return $allRules; + } +} diff --git a/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php b/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php new file mode 100644 index 0000000..b5785d5 --- /dev/null +++ b/library/Director/DirectorObject/Lookup/AppliedServiceSetServiceInfo.php @@ -0,0 +1,127 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +use gipfl\IcingaWeb2\Url; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\HostApplyMatches; +use Icinga\Module\Director\Objects\IcingaHost; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; + +/** + * A Service that makes part of a Service Set Apply Rule matching this Host, + * generating a Service with the given name + */ +class AppliedServiceSetServiceInfo implements ServiceInfo +{ + /** @var string */ + protected $hostName; + + /** @var string */ + protected $serviceName; + + /** @var string */ + protected $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) + { + $matcher = HostApplyMatches::prepare($host); + $connection = $host->getConnection(); + foreach (static::fetchServiceSetApplyRulesByServiceName($connection, $host->get('id'), $serviceName) as $rule) { + if ($matcher->matchesFilter($rule->filter)) { + return new static( + $host->getObjectName(), + $serviceName, + $rule->service_set_name, + $rule->uuid + ); + } + } + + return null; + } + + public function getHostName() + { + return $this->hostName; + } + + public function getUuid() + { + return $this->uuid; + } + + /** + * @return string + */ + public function getServiceSetName() + { + return $this->serviceSetName; + } + + public function getName() + { + return $this->serviceName; + } + + public function getUrl() + { + return Url::fromPath('director/host/servicesetservice', [ + 'name' => $this->hostName, + 'service' => $this->serviceName, + 'set' => $this->serviceSetName, + ]); + } + + public function requiresOverrides() + { + return true; + } + + protected static function fetchServiceSetApplyRulesByServiceName(Db $connection, $hostId, $serviceName) + { + $db = $connection->getDbAdapter(); + $query = $db->select() + ->from(['s' => 'icinga_service'], [ + 'id' => 's.id', + 'uuid' => 'ss.uuid', + 'name' => 's.object_name', + 'assign_filter' => 'ss.assign_filter', + 'service_set_name' => 'ss.object_name', + ]) + ->join( + ['ss' => 'icinga_service_set'], + 's.service_set_id = ss.id', + [] + ) + ->where('s.object_name = ?', $serviceName) + ->where('ss.assign_filter IS NOT NULL') + ->where( // Ignore deactivated Services: + 'NOT EXISTS (SELECT 1 FROM icinga_host_service_blacklist hsb' + . ' WHERE hsb.host_id = ? AND hsb.service_id = s.id)', + (int) $hostId + ); + ; + + $allRules = $db->fetchAll($query); + foreach ($allRules as $rule) { + $rule->uuid = Uuid::fromBytes(Db\DbUtil::binaryResult($rule->uuid)); + $rule->filter = Filter::fromQueryString($rule->assign_filter); + } + + return $allRules; + } +} diff --git a/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php b/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php new file mode 100644 index 0000000..875d5fb --- /dev/null +++ b/library/Director/DirectorObject/Lookup/InheritedServiceInfo.php @@ -0,0 +1,94 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +use gipfl\IcingaWeb2\Url; +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 + * 'assign where "Template Name" in templates' + */ +class InheritedServiceInfo implements ServiceInfo +{ + /** @var string */ + protected $hostName; + + /** @var string */ + protected $hostTemplateName; + + /** @var string */ + protected $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) { + $key = [ + 'host_id' => $parent->get('id'), + 'object_name' => $serviceName + ]; + if (IcingaService::exists($key, $db)) { + return new static( + $host->getObjectName(), + $parent->getObjectName(), + $serviceName, + IcingaService::load($key, $db)->getUniqueId() + ); + } + } + + return false; + } + + public function getHostName() + { + return $this->hostName; + } + + public function getUuid() + { + return $this->uuid; + } + + /** + * @return string + */ + public function getHostTemplateName() + { + return $this->hostTemplateName; + } + + public function getName() + { + return $this->serviceName; + } + + public function getUrl() + { + return Url::fromPath('director/host/inheritedservice', [ + 'name' => $this->hostName, + 'service' => $this->serviceName, + 'inheritedFrom' => $this->hostTemplateName + ]); + } + + public function requiresOverrides() + { + return true; + } +} diff --git a/library/Director/DirectorObject/Lookup/ServiceFinder.php b/library/Director/DirectorObject/Lookup/ServiceFinder.php new file mode 100644 index 0000000..fb8d74c --- /dev/null +++ b/library/Director/DirectorObject/Lookup/ServiceFinder.php @@ -0,0 +1,79 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +use gipfl\IcingaWeb2\Url; +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 */ + protected $auth; + + /** @var IcingaHost[] */ + protected $parents; + + /** @var HostApplyMatches */ + protected $applyMatcher; + + /** @var \Icinga\Module\Director\Db */ + protected $db; + + public function __construct(IcingaHost $host, Auth $auth = null) + { + $this->host = $host; + $this->auth = $auth; + $this->db = $host->getConnection(); + } + + public static function find(IcingaHost $host, $serviceName) + { + foreach ([ + SingleServiceInfo::class, + InheritedServiceInfo::class, + ServiceSetServiceInfo::class, + AppliedServiceInfo::class, + AppliedServiceSetServiceInfo::class, + ] as $class) { + /** @var ServiceInfo $class */ + if ($info = $class::find($host, $serviceName)) { + return $info; + } + } + + 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/Lookup/ServiceInfo.php b/library/Director/DirectorObject/Lookup/ServiceInfo.php new file mode 100644 index 0000000..3c8c51b --- /dev/null +++ b/library/Director/DirectorObject/Lookup/ServiceInfo.php @@ -0,0 +1,46 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +use gipfl\IcingaWeb2\Url; +use Icinga\Module\Director\Objects\IcingaHost; +use Ramsey\Uuid\UuidInterface; + +interface ServiceInfo +{ + /** + * The final Service name + * + * @return string + */ + public function getName(); + + /** + * The host the final (rendered, processed) Service belongs to + * + * @return string + */ + public function getHostName(); + + /** + * @return Url + */ + public function getUrl(); + + /** + * @return UuidInterface + */ + public function getUuid(); + + /** + * @return bool + */ + public function requiresOverrides(); + + /** + * @param IcingaHost $host + * @param $serviceName + * @return ServiceInfo|false + */ + public static function find(IcingaHost $host, $serviceName); +} diff --git a/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php b/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php new file mode 100644 index 0000000..a980da8 --- /dev/null +++ b/library/Director/DirectorObject/Lookup/ServiceSetServiceInfo.php @@ -0,0 +1,121 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +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 + * Host or to one of it's inherited Host Templates + */ +class ServiceSetServiceInfo implements ServiceInfo +{ + /** @var string */ + protected $hostName; + + /** @var string */ + protected $serviceName; + + /** @var string */ + protected $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) + { + $ids = [$host->get('id')]; + + foreach (IcingaTemplateRepository::instanceByObject($host)->getTemplatesFor($host, true) as $parent) { + $ids[] = $parent->get('id'); + } + + $db = $host->getConnection()->getDbAdapter(); + $query = $db->select() + ->from( + ['s' => 'icinga_service'], + [ + 'service_set_name' => 'ss.object_name', + 'uuid' => 's.uuid', + ] + )->join( + ['ss' => 'icinga_service_set'], + 's.service_set_id = ss.id', + [] + )->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 IN (?)', $ids) + ->where('s.object_name = ?', $serviceName) + ->where( // Ignore deactivated Services: + 'NOT EXISTS (SELECT 1 FROM icinga_host_service_blacklist hsb' + . ' WHERE hsb.host_id = ? AND hsb.service_id = s.id)', + (int) $host->get('id') + ); + + if ($row = $db->fetchRow($query)) { + return new static( + $host->getObjectName(), + $serviceName, + $row->service_set_name, + Uuid::fromBytes($row->uuid) + ); + } + + return null; + } + + public function getHostName() + { + return $this->hostName; + } + + public function getName() + { + return $this->serviceName; + } + + public function getUuid() + { + return $this->uuid; + } + + /** + * @return string + */ + public function getServiceSetName() + { + return $this->serviceSetName; + } + + public function getUrl() + { + return Url::fromPath('director/host/servicesetservice', [ + 'name' => $this->hostName, + 'service' => $this->serviceName, + 'set' => $this->serviceSetName, + ]); + } + + public function requiresOverrides() + { + return true; + } +} diff --git a/library/Director/DirectorObject/Lookup/SingleServiceInfo.php b/library/Director/DirectorObject/Lookup/SingleServiceInfo.php new file mode 100644 index 0000000..af54fc7 --- /dev/null +++ b/library/Director/DirectorObject/Lookup/SingleServiceInfo.php @@ -0,0 +1,83 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject\Lookup; + +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 + * still be used when use_var_overrides is true. + */ +class SingleServiceInfo implements ServiceInfo +{ + /** @var string */ + protected $hostName; + + /** @var string */ + protected $serviceName; + + /** @var bool */ + protected $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) + { + $keyParams = [ + 'host_id' => $host->get('id'), + 'object_name' => $serviceName + ]; + $connection = $host->getConnection(); + if (IcingaService::exists($keyParams, $connection)) { + $service = IcingaService::load($keyParams, $connection); + $useOverrides = $service->getResolvedVar('use_var_overrides') === 'y'; + + return new static($host->getObjectName(), $serviceName, $service->getUniqueId(), $useOverrides); + } + + return false; + } + + public function getHostName() + { + return $this->hostName; + } + + public function getName() + { + return $this->serviceName; + } + + /** + * @return UuidInterface + */ + public function getUuid() + { + return $this->uuid; + } + + public function getUrl() + { + return Url::fromPath('director/service/edit', [ + 'host' => $this->hostName, + 'name' => $this->serviceName, + ]); + } + + public function requiresOverrides() + { + return $this->useOverrides; + } +} diff --git a/library/Director/DirectorObject/ObjectPurgeHelper.php b/library/Director/DirectorObject/ObjectPurgeHelper.php new file mode 100644 index 0000000..a043965 --- /dev/null +++ b/library/Director/DirectorObject/ObjectPurgeHelper.php @@ -0,0 +1,149 @@ +<?php + +namespace Icinga\Module\Director\DirectorObject; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaObject; +use InvalidArgumentException; + +class ObjectPurgeHelper +{ + protected $db; + + protected $force = false; + + public function __construct(Db $db) + { + $this->db = $db; + } + + public function force($force = true) + { + $this->force = $force; + return $this; + } + + public function purge(array $keep, $class, $objectType = null) + { + if (empty($keep) && ! $this->force) { + throw new InvalidArgumentException('I will NOT purge all object unless being forced to do so'); + } + $db = $this->db->getDbAdapter(); + /** @var IcingaObject $class cheating, it's a class name, not an object */ + $dummy = $class::create(); + assert($dummy instanceof IcingaObject); + $keyCols = (array) $dummy->getKeyName(); + if ($objectType !== null) { + $keyCols[] = 'object_type'; + } + + $keepKeys = []; + foreach ($keep as $object) { + if ($object instanceof \stdClass) { + $properties = (array) $object; + // 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) { + throw new InvalidArgumentException( + 'Can keep only matching objects, expected "%s", got "%s', + $class, + \get_class($keep) + ); + } + $key = []; + foreach ($keyCols as $col) { + $key[$col] = $object->get($col); + } + $keepKeys[$this->makeRowKey($key)] = true; + } + + $query = $db->select()->from(['o' => $dummy->getTableName()], $keyCols); + if ($objectType !== null) { + $query->where('object_type = ?', $objectType); + } + $allExisting = []; + foreach ($db->fetchAll($query) as $row) { + $allExisting[$this->makeRowKey($row)] = $row; + } + $remove = []; + foreach ($allExisting as $key => $keyProperties) { + if (! isset($keepKeys[$key])) { + $remove[] = $keyProperties; + } + } + $db->beginTransaction(); + foreach ($remove as $keyProperties) { + $keyColumn = $class::getKeyColumnName(); + if (is_array($keyColumn)) { + $object = $class::load((array) $keyProperties, $this->db); + } else { + $object = $class::load($keyProperties->$keyColumn, $this->db); + } + $object->delete(); + } + $db->commit(); + } + + public static function listObjectTypesAvailableForPurge() + { + return [ + 'Basket', + 'Command', + 'CommandTemplate', + 'Dependency', + 'DirectorJob', + 'ExternalCommand', + 'HostGroup', + 'HostTemplate', + 'IcingaTemplateChoiceHost', + 'IcingaTemplateChoiceService', + 'ImportSource', + 'Notification', + 'NotificationTemplate', + 'ServiceGroup', + 'ServiceSet', + 'ServiceTemplate', + 'SyncRule', + 'TimePeriod', + ]; + } + + public static function objectTypeIsEligibleForPurge($type) + { + return in_array($type, static::listObjectTypesAvailableForPurge(), true); + } + + public static function assertObjectTypesAreEligibleForPurge($types) + { + $invalid = []; + foreach ($types as $type) { + if (! static::objectTypeIsEligibleForPurge($type)) { + $invalid[] = $type; + } + } + + if (empty($invalid)) { + return; + } + + if (count($invalid) === 1) { + $message = sprintf('"%s" is not eligible for purge', $invalid[0]); + } else { + $message = 'The following types are not eligible for purge: ' + . implode(', ', $invalid); + } + + throw new InvalidArgumentException( + "$message. Valid types: " + . implode(', ', static::listObjectTypesAvailableForPurge()) + ); + } + + protected function makeRowKey($row) + { + $row = (array) $row; + ksort($row); + return json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } +} |