diff --git a/application/controllers/BasketController.php b/application/controllers/BasketController.php index 328cc45c..54076c29 100644 --- a/application/controllers/BasketController.php +++ b/application/controllers/BasketController.php @@ -11,6 +11,7 @@ use Icinga\Module\Director\Core\Json; use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\Basket; use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshot; +use Icinga\Module\Director\DirectorObject\Automation\BasketSnapshotFieldResolver; use Icinga\Module\Director\Forms\BasketCreateSnapshotForm; use Icinga\Module\Director\Forms\BasketForm; use Icinga\Module\Director\Forms\RestoreBasketForm; @@ -38,6 +39,7 @@ class BasketController extends ActionController /** * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Exception\MissingParameterException */ public function indexAction() { @@ -188,8 +190,11 @@ class BasketController extends ActionController $json = $snapshot->getJsonDump(); $this->addSingleTab($this->translate('Snapshot')); $all = Json::decode($json); + $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); foreach ($all as $type => $objects) { if ($type === 'Datafield') { + // TODO: we should now be able to show all fields and link + // to a "diff" for the ones that should be created // $this->content()->add(Html::tag('h2', sprintf('+%d Datafield(s)', count($objects)))); continue; } @@ -219,7 +224,9 @@ class BasketController extends ActionController ); continue; } - $hasChanged = Json::encode($current->export()) !== Json::encode($object); + $currentExport = $current->export(); + $fieldResolver->tweakTargetIds($currentExport); + $hasChanged = Json::encode($currentExport) !== Json::encode($object); $table->addNameValueRow( $key, $hasChanged @@ -296,12 +303,15 @@ class BasketController extends ActionController } else { $connection = Db::fromResourceName($targetDbName); } + $fieldResolver = new BasketSnapshotFieldResolver($objects, $connection); $objectFromBasket = $objects->$type->$key; $current = BasketSnapshot::instanceByIdentifier($type, $key, $connection); if ($current === null) { $current = ''; } else { - $current = Json::encode($current->export(), JSON_PRETTY_PRINT); + $exported = $current->export(); + $fieldResolver->tweakTargetIds($exported); + $current = Json::encode($exported, JSON_PRETTY_PRINT); } $this->content()->add( @@ -312,6 +322,11 @@ class BasketController extends ActionController ); } + /** + * @return Basket + * @throws \Icinga\Exception\MissingParameterException + * @throws \Icinga\Exception\NotFoundError + */ protected function requireBasket() { return Basket::load($this->params->getRequired('name'), $this->db()); diff --git a/application/forms/RestoreBasketForm.php b/application/forms/RestoreBasketForm.php index 07e62587..5c86f13a 100644 --- a/application/forms/RestoreBasketForm.php +++ b/application/forms/RestoreBasketForm.php @@ -65,6 +65,9 @@ class RestoreBasketForm extends QuickForm return Db::fromResourceName($this->getValue('target_db')); } + /** + * @throws \Icinga\Exception\NotFoundError + */ public function onSuccess() { $this->snapshot->restoreTo($this->getDb()); diff --git a/library/Director/DirectorObject/Automation/BasketSnapshot.php b/library/Director/DirectorObject/Automation/BasketSnapshot.php index 49e8c15f..fb63da53 100644 --- a/library/Director/DirectorObject/Automation/BasketSnapshot.php +++ b/library/Director/DirectorObject/Automation/BasketSnapshot.php @@ -25,7 +25,6 @@ class BasketSnapshot extends DbObject ]; protected $restoreOrder = [ - 'Datafield', 'Command', 'HostGroup', 'IcingaTemplateChoiceHost', @@ -93,30 +92,17 @@ class BasketSnapshot extends DbObject */ protected function resolveRequiredFields() { - $requiredIds = []; - foreach ($this->objects as $typeName => $objects) { - foreach ($objects as $key => $object) { - if (isset($object->fields)) { - foreach ($object->fields as $field) { - $requiredIds[$field->datafield_id] = true; - } - } + /** @var Db $db */ + $db = $this->getConnection(); + $fieldResolver = new BasketSnapshotFieldResolver($this->objects, $db); + /** @var DirectorDatafield[] $fields */ + $fields = $fieldResolver->loadCurrentFields($db); + if (! empty($fields)) { + $plain = []; + foreach ($fields as $id => $field) { + $plain[$id] = $field->export(); } - } - - $connection = $this->getConnection(); - if (! isset($this->objects['Datafield'])) { - $this->objects['Datafield'] = []; - } - $fields = & $this->objects['Datafield']; - foreach (array_keys($requiredIds) as $id) { - if (! isset($fields[$id])) { - $fields[$id] = DirectorDatafield::loadWithAutoIncId((int) $id, $connection)->export(); - } - } - - if (empty($this->objects['Datafield'])) { - unset($this->objects['Datafield']); + $this->objects['Datafield'] = $plain; } } @@ -157,9 +143,7 @@ class BasketSnapshot extends DbObject /** * @param Db $connection * @param bool $replace - * @throws \Icinga\Module\Director\Exception\DuplicateKeyException * @throws \Icinga\Exception\NotFoundError - * @throws \Zend_Db_Adapter_Exception */ public function restoreTo(Db $connection, $replace = true) { @@ -186,12 +170,14 @@ class BasketSnapshot extends DbObject * @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(); - $fieldMap = []; + $fieldResolver = new BasketSnapshotFieldResolver($all, $connection); + $fieldResolver->storeNewFields(); foreach ($this->restoreOrder as $typeName) { if (isset($all->$typeName)) { $objects = $all->$typeName; @@ -208,12 +194,9 @@ class BasketSnapshot extends DbObject $new->store(); } } - if ($new instanceof DirectorDatafield) { - $fieldMap[(int) $key] = (int) $new->get('id'); - } if ($new instanceof IcingaObject) { - $this->relinkObjectFields($db, $new, $object, $fieldMap); + $fieldResolver->relinkObjectFields($new, $object); } } @@ -226,53 +209,6 @@ class BasketSnapshot extends DbObject $db->commit(); } - /** - * @param ZfDbAdapter $db - * @param IcingaObject $new - * @param $object - * @param $fieldMap - * @throws \Zend_Db_Adapter_Exception - */ - protected function relinkObjectFields(ZfDbAdapter $db, IcingaObject $new, $object, $fieldMap) - { - if (! $new->supportsFields() || ! isset($object->fields)) { - return; - } - - $objectId = (int) $new->get('id'); - $table = $new->getTableName() . '_field'; - $objectKey = $new->getShortTableName() . '_id'; - $existingFields = []; - - 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 IcingaObject $object * @param $list diff --git a/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php new file mode 100644 index 00000000..21e17e1f --- /dev/null +++ b/library/Director/DirectorObject/Automation/BasketSnapshotFieldResolver.php @@ -0,0 +1,224 @@ +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() + { + 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 $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) { + // 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/Objects/DirectorDatafield.php b/library/Director/Objects/DirectorDatafield.php index c6a41c72..27d6d67b 100644 --- a/library/Director/Objects/DirectorDatafield.php +++ b/library/Director/Objects/DirectorDatafield.php @@ -80,7 +80,6 @@ class DirectorDatafield extends DbObjectWithSettings * @param Db $db * @param bool $replace * @return DirectorDatafield - * @throws DuplicateKeyException * @throws \Icinga\Exception\NotFoundError */ public static function import($plain, Db $db, $replace = false) @@ -93,6 +92,15 @@ class DirectorDatafield extends DbObjectWithSettings $id = null; } + if (isset($properties['settings']->datalist)) { + $list = DirectorDatalist::load( + $properties['settings']->datalist, + $db + ); + $properties['settings']->datalist_id = $list->get('id'); + unset($properties['settings']->datalist); + } + $encoded = Json::encode($properties); if ($id) { if (static::exists($id, $db)) { diff --git a/library/Director/Objects/IcingaHost.php b/library/Director/Objects/IcingaHost.php index 05ff6fff..0aceafe6 100644 --- a/library/Director/Objects/IcingaHost.php +++ b/library/Director/Objects/IcingaHost.php @@ -339,6 +339,9 @@ class IcingaHost extends IcingaObject if (empty($res)) { return []; } else { + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } return $res; } } diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php index 038c67d0..fc13baec 100644 --- a/library/Director/Objects/IcingaService.php +++ b/library/Director/Objects/IcingaService.php @@ -237,6 +237,10 @@ class IcingaService extends IcingaObject if (empty($res)) { return []; } else { + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + return $res; } } diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index 82d55a6f..cec873de 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -4,12 +4,14 @@ namespace Icinga\Module\Director\Objects; use Exception; use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; use Icinga\Module\Director\Exception\DuplicateKeyException; use Icinga\Module\Director\IcingaConfig\IcingaConfig; -use Icinga\Module\Director\Web\Form\IcingaObjectFieldLoader; use InvalidArgumentException; +use RuntimeException; -class IcingaServiceSet extends IcingaObject +class IcingaServiceSet extends IcingaObject implements ExportInterface { protected $table = 'icinga_service_set'; @@ -111,6 +113,15 @@ class IcingaServiceSet extends IcingaObject return $services; } + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @return object + * @throws \Icinga\Exception\NotFoundError + */ public function export() { if ($this->get('host_id')) { @@ -123,27 +134,98 @@ class IcingaServiceSet extends IcingaObject protected function exportSetOnHost() { // TODO. - throw new \RuntimeException('Not yet'); + throw new RuntimeException('Not yet'); } + /** + * @return object + * @throws \Icinga\Exception\NotFoundError + */ protected function exportTemplate() { $props = $this->getProperties(); unset($props['id'], $props['host_id']); $props['services'] = []; - $props['service_templates'] = []; foreach ($this->getServiceObjects() as $serviceObject) { $props['services'][$serviceObject->getObjectName()] = $serviceObject->export(); - foreach ($serviceObject->imports()->getObjects() as $import) { - $name = $import->getObjectName(); - $props['service_templates'][$name] = $import->export(); - } } ksort($props); return (object) $props; } + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaServiceSet + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + $name = $properties['object_name']; + if (isset($properties['services'])) { + $services = $properties['services']; + unset($properties['services']); + } else { + $services = []; + } + + if ($properties['object_type'] !== 'template') { + throw new InvalidArgumentException(sprintf( + 'Can import only Templates, got "%s" for "%s"', + $properties['object_type'], + $name + )); + } + if ($replace && static::exists($name, $db)) { + $object = static::load($name, $db); + } elseif (static::exists($name, $db)) { + throw new DuplicateKeyException( + 'Service Set "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + $object->setProperties($properties); + + // This is not how other imports work, but here we need an ID + if (! $object->hasBeenLoadedFromDb()) { + $object->store(); + } + + $setId = $object->get('id'); + $sQuery = $db->getDbAdapter()->select()->from( + ['s' => 'icinga_service'], + 's.*' + )->where('service_set_id = ?', $setId); + $existingServices = IcingaService::loadAll($db, $sQuery, 'object_name'); + foreach ($services as $service) { + if (isset($service->fields)) { + unset($service->fields); + } + $name = $service->object_name; + if (isset($existingServices[$name])) { + $existing = $existingServices[$name]; + $existing->setProperties((array) $service); + $existing->set('service_set_id', $setId); + if ($existing->hasBeenModified()) { + $existing->store(); + } + } else { + $new = IcingaService::create((array) $service, $db); + $new->set('service_set_id', $setId); + $new->store(); + } + } + + return $object; + } + public function onDelete() { $hostId = $this->get('host_id');