BasketSnapshotFieldResolver: deal with fields

Also, this fixes Service Sets
This commit is contained in:
Thomas Gelf 2018-10-11 23:13:51 +02:00
parent 9ed1503ad6
commit 83dc9dc6c3
8 changed files with 364 additions and 89 deletions

View File

@ -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());

View File

@ -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());

View File

@ -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

View File

@ -0,0 +1,224 @@
<?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()
{
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;
}
}
}

View File

@ -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)) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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');