diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php index 432730bc..d5e97634 100644 --- a/library/Director/Objects/IcingaObject.php +++ b/library/Director/Objects/IcingaObject.php @@ -3058,6 +3058,11 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer return $this->templateTree()->listAncestorIdsFor($this); } + public function listAncestorUuIds() + { + return $this->templateTree()->listAncestorUuIdsFor($this); + } + protected function templateTree() { return $this->templates()->tree(); diff --git a/library/Director/Resolver/TemplateTree.php b/library/Director/Resolver/TemplateTree.php index e1c1cfcf..ba5bf5f9 100644 --- a/library/Director/Resolver/TemplateTree.php +++ b/library/Director/Resolver/TemplateTree.php @@ -17,6 +17,8 @@ class TemplateTree protected $parents; + protected $parentsUuids; + protected $children; protected $rootNodes; @@ -29,6 +31,8 @@ class TemplateTree protected $names; + protected $uuids; + protected $templateNameToId; public function __construct($type, Db $connection) @@ -111,6 +115,11 @@ class TemplateTree return array_keys($this->getAncestorsFor($object)); } + public function listAncestorUuIdsFor(IcingaObject $object) + { + return $this->getAncestorsUuidsFor($object); + } + public function listChildIdsFor(IcingaObject $object) { return array_keys($this->getChildrenFor($object)); @@ -148,6 +157,19 @@ class TemplateTree } } + public function getAncestorsUuidsFor(IcingaObject $object) + { + if ( + $object->hasBeenModified() + && $object->gotImports() + && $object->imports()->hasBeenModified() + ) { + return $this->getAncestorsUuidsForUnstoredObject($object); + } else { + return $this->getParentsUuidsById($object->getProperty('id')); + } + } + protected function getAncestorsForUnstoredObject(IcingaObject $object) { $this->requireTree(); @@ -183,6 +205,36 @@ class TemplateTree return $ancestors; } + protected function getAncestorsUuidsForUnstoredObject(IcingaObject $object) + { + $this->requireTree(); + $ancestors = []; + foreach ($object->imports() as $import) { + $name = $import->get('uuid'); + if ($import->hasBeenLoadedFromDb()) { + $pid = (int) $import->get('id'); + } else { + if (! array_key_exists($name, $this->templateNameToId)) { + continue; + } + + $pid = $this->templateNameToId[$name]; + } + + $this->getAncestorsUuidsById($pid, $ancestors); + + $uuid = $import->get('uuid'); + // Hint: inheritance order matters + if (false !== ($key = array_search($uuid, $ancestors))) { + unset($ancestors[$key]); + } + + $ancestors[$pid] = $uuid; + } + + return $ancestors; + } + protected function requireObjectMaps() { if ($this->objectMaps === null) { @@ -211,6 +263,27 @@ class TemplateTree } } + public function getParentsUuidsById($id) + { + $this->requireTree(); + + if (array_key_exists($id, $this->parentsUuids)) { + return $this->parentsUuids[$id]; + } + + $this->requireObjectMaps(); + if (array_key_exists($id, $this->objectMaps)) { + $parentsUuids = []; + foreach ($this->objectMaps[$id] as $pid) { + $parentsUuids[$pid] = $this->uuids[$pid]; + } + + return $parentsUuids; + } else { + return []; + } + } + /** * @param $id * @param $list @@ -287,6 +360,38 @@ class TemplateTree return $ancestors; } + /** + * Get the ancestorUuids for the given object ID + * + * @param $id + * @param array $ancestors + * @param array $path + * + * @return array + */ + public function getAncestorsUuidsById($id, &$ancestors = [], $path = []) + { + $path[$id] = true; + foreach ($this->getParentsUuidsById($id) as $pid => $uuid) { + $this->assertNotInList($pid, $path); + $path[$pid] = true; + + $this->getAncestorsUuidsById($pid, $ancestors, $path); + unset($path[$pid]); + + // Hint: inheritance order matters + if (false !== ($key = array_search($uuid, $ancestors))) { + unset($ancestors[$key]); + } + + $ancestors[$pid] = $uuid; + } + + unset($path[$id]); + + return $ancestors; + } + public function getChildrenFor(IcingaObject $object) { // can not use hasBeenLoadedFromDb() when in onStore() @@ -386,6 +491,8 @@ class TemplateTree $rootNodes = []; $children = []; $names = []; + $parentsUuids = []; + $uuids = []; foreach ($templates as $row) { $id = (int) $row->id; $pid = (int) $row->parent_id; @@ -403,7 +510,9 @@ class TemplateTree } $names[$pid] = $row->parent_name; + $uuids[$pid] = $row->parent_uuid; $parents[$id][$pid] = $row->parent_name; + $parentsUuids[$id][$pid] = $row->parent_uuid; if (! array_key_exists($pid, $children)) { $children[$pid] = []; @@ -412,10 +521,12 @@ class TemplateTree $children[$pid][$id] = $row->name; } - $this->parents = $parents; + $this->parents = $parents; + $this->parentsUuids = $parentsUuids; $this->children = $children; $this->rootNodes = $rootNodes; $this->names = $names; + $this->uuids = $uuids; $this->templateNameToId = array_flip($names); Benchmark::measure(sprintf('"%s" Template Tree ready', $this->type)); } @@ -458,6 +569,7 @@ class TemplateTree 'object_type' => 'o.object_type', 'parent_id' => 'p.id', 'parent_name' => 'p.object_name', + 'parent_uuid' => 'p.uuid' ] )->joinLeft( ['i' => $table . '_inheritance'], diff --git a/library/Director/RestApi/IcingaObjectHandler.php b/library/Director/RestApi/IcingaObjectHandler.php index be58e68a..0693bcc7 100644 --- a/library/Director/RestApi/IcingaObjectHandler.php +++ b/library/Director/RestApi/IcingaObjectHandler.php @@ -14,6 +14,7 @@ use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Resolver\OverrideHelper; use InvalidArgumentException; +use PDO; use RuntimeException; class IcingaObjectHandler extends RequestHandler @@ -96,6 +97,45 @@ class IcingaObjectHandler extends RequestHandler } } + public function getCustomProperties(IcingaObject $object): array + { + if ($object->get('uuid') === null) { + return []; + } + + $type = $object->getShortTableName(); + $db = $object->getConnection(); + $uuids = $object->listAncestorUuIds(); + $query = $db->getDbAdapter() + ->select() + ->from( + ['dp' => 'director_property'], + [ + 'key_name' => 'dp.key_name', + 'uuid' => 'dp.uuid', + 'value_type' => 'dp.value_type', + 'label' => 'dp.label', + 'instantiable' => 'dp.instantiable', + 'required' => 'iop.required', + 'children' => 'COUNT(cdp.uuid)' + ] + ) + ->join(['iop' => "icinga_$type" . '_property'], 'dp.uuid = iop.property_uuid', []) + ->joinLeft(['cdp' => 'director_property'], 'cdp.parent_uuid = dp.uuid', []) + ->where('iop.' . $type . '_uuid IN (?)', $uuids) + ->group(['dp.uuid', 'dp.key_name', 'dp.value_type', 'dp.label', 'dp.instantiable', 'iop.required']) + ->order('children') + ->order('instantiable') + ->order('key_name'); + + $result = []; + foreach ($db->getDbAdapter()->fetchAll($query, fetchMode: PDO::FETCH_ASSOC) as $row) { + $result[$row['key_name']] = $row; + } + + return $result; + } + protected function handleApiRequest() { $request = $this->request; @@ -129,10 +169,30 @@ class IcingaObjectHandler extends RequestHandler $params = $this->request->getUrl()->getParams(); $allowsOverrides = $params->get('allowOverrides'); $type = $this->getType(); - if ($object = $this->loadOptionalObject()) { - if ($this->request->getActionName() === 'variables') { - $data = ['vars' => $data]; + $object = $this->loadOptionalObject(); + $customProperties = $this->getCustomProperties($object); + $overridenCustomVars = $this->getCustomVarsFromData($data); + if (! empty($overridenCustomVars)) { + $diff = array_diff(array_keys($data), array_keys($customProperties)); + if (! empty($diff)) { + throw new Exception(sprintf( + "The custom properties (%s) are not supported by this object", + implode(", ", $diff) + )); + } + } + if ($object) { + if ($this->request->getActionName() === 'variables') { + $diff = array_diff(array_keys($data), array_keys($customProperties)); + if (! empty($diff)) { + throw new Exception(sprintf( + "The custom properties %s are not supported by this object", + implode(", ", $diff) + )); + } + + $data = ['vars' => $data]; $object->setProperties($data); } elseif ($request->getMethod() === 'POST') { $object->setProperties($data); @@ -197,4 +257,21 @@ class IcingaObjectHandler extends RequestHandler throw new RuntimeException('Found a single service, which should have been found (and dealt with) before'); } } + + private function getCustomVarsFromData(array $data): array + { + $customVars = []; + + foreach ($data as $key => $value) { + if ($key === 'vars') { + $customVars = $value; + } + + if (substr($key, 0, 5) === 'vars.') { + $customVars[substr($key, 5)] = $value; + } + } + + return $customVars; + } }