diff options
Diffstat (limited to 'library/Director/Objects')
71 files changed, 15680 insertions, 0 deletions
diff --git a/library/Director/Objects/DirectorActivityLog.php b/library/Director/Objects/DirectorActivityLog.php new file mode 100644 index 0000000..cb041b6 --- /dev/null +++ b/library/Director/Objects/DirectorActivityLog.php @@ -0,0 +1,232 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Authentication\Auth; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; + +class DirectorActivityLog extends DbObject +{ + const ACTION_CREATE = 'create'; + const ACTION_DELETE = 'delete'; + const ACTION_MODIFY = 'modify'; + + /** @deprecated */ + const AUDIT_REMOVE = 'remove'; + + protected $table = 'director_activity_log'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = [ + 'id' => null, + 'object_name' => null, + 'action_name' => null, + 'object_type' => null, + 'old_properties' => null, + 'new_properties' => null, + 'author' => null, + 'change_time' => null, + 'checksum' => null, + 'parent_checksum' => null, + ]; + + protected $binaryProperties = [ + 'checksum', + 'parent_checksum' + ]; + + /** @var ?string */ + protected static $overriddenUsername = null; + + /** + * @param $name + * + * @codingStandardsIgnoreStart + * + * @return self + */ + protected function setObject_Name($name) + { + // @codingStandardsIgnoreEnd + + if ($name === null) { + $name = ''; + } + + return $this->reallySet('object_name', $name); + } + + public static function username() + { + if (self::$overriddenUsername) { + return self::$overriddenUsername; + } + + if (Icinga::app()->isCli()) { + return 'cli'; + } + + $auth = Auth::getInstance(); + if ($auth->isAuthenticated()) { + return $auth->getUser()->getUsername(); + } elseif (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { + return '<' . $_SERVER['HTTP_X_FORWARDED_FOR'] . '>'; + } elseif (array_key_exists('REMOTE_ADDR', $_SERVER)) { + return '<' . $_SERVER['REMOTE_ADDR'] . '>'; + } else { + return '<unknown>'; + } + } + + protected static function ip() + { + if (Icinga::app()->isCli()) { + return 'cli'; + } + + if (array_key_exists('REMOTE_ADDR', $_SERVER)) { + return $_SERVER['REMOTE_ADDR']; + } else { + return '0.0.0.0'; + } + } + + /** + * @param Db $connection + * @return DirectorActivityLog + * @throws \Icinga\Exception\NotFoundError + */ + public static function loadLatest(Db $connection) + { + $db = $connection->getDbAdapter(); + $query = $db->select()->from('director_activity_log', ['id' => 'MAX(id)']); + + return static::load($db->fetchOne($query), $connection); + } + + public static function logCreation(IcingaObject $object, Db $db) + { + // TODO: extend this to support non-IcingaObjects and multikey objects + $name = $object->getObjectName(); + $type = $object->getTableName(); + $newProps = $object->toJson(null, true); + + $data = [ + 'object_name' => $name, + 'action_name' => self::ACTION_CREATE, + 'author' => static::username(), + 'object_type' => $type, + 'new_properties' => $newProps, + 'change_time' => date('Y-m-d H:i:s'), + 'parent_checksum' => $db->getLastActivityChecksum() + ]; + + $data['checksum'] = sha1(json_encode($data), true); + $data['parent_checksum'] = hex2bin($data['parent_checksum']); + + static::audit($db, [ + 'action' => self::ACTION_CREATE, + 'object_type' => $type, + 'object_name' => $name, + 'new_props' => $newProps, + ]); + + return static::create($data)->store($db); + } + + public static function logModification(IcingaObject $object, Db $db) + { + $name = $object->getObjectName(); + $type = $object->getTableName(); + $oldProps = json_encode($object->getPlainUnmodifiedObject()); + $newProps = $object->toJson(null, true); + + $data = [ + 'object_name' => $name, + 'action_name' => self::ACTION_MODIFY, + 'author' => static::username(), + 'object_type' => $type, + 'old_properties' => $oldProps, + 'new_properties' => $newProps, + 'change_time' => date('Y-m-d H:i:s'), + 'parent_checksum' => $db->getLastActivityChecksum() + ]; + + $data['checksum'] = sha1(json_encode($data), true); + $data['parent_checksum'] = hex2bin($data['parent_checksum']); + + static::audit($db, [ + 'action' => self::ACTION_MODIFY, + 'object_type' => $type, + 'object_name' => $name, + 'old_props' => $oldProps, + 'new_props' => $newProps, + ]); + + return static::create($data)->store($db); + } + + public static function logRemoval(IcingaObject $object, Db $db) + { + $name = $object->getObjectName(); + $type = $object->getTableName(); + $oldProps = json_encode($object->getPlainUnmodifiedObject()); + + $data = [ + 'object_name' => $name, + 'action_name' => self::ACTION_DELETE, + 'author' => static::username(), + 'object_type' => $type, + 'old_properties' => $oldProps, + 'change_time' => date('Y-m-d H:i:s'), + 'parent_checksum' => $db->getLastActivityChecksum() + ]; + + $data['checksum'] = sha1(json_encode($data), true); + $data['parent_checksum'] = hex2bin($data['parent_checksum']); + + static::audit($db, [ + 'action' => self::AUDIT_REMOVE, + 'object_type' => $type, + 'object_name' => $name, + 'old_props' => $oldProps + ]); + + return static::create($data)->store($db); + } + + public static function audit(Db $db, $properties) + { + if ($db->settings()->get('enable_audit_log') !== 'y') { + return; + } + + $log = []; + $properties = array_merge([ + 'username' => static::username(), + 'address' => static::ip(), + ], $properties); + + foreach ($properties as $key => $val) { + $log[] = "$key=" . json_encode($val); + } + + Logger::info('(director) ' . implode(' ', $log)); + } + + public static function overrideUsername($username) + { + self::$overriddenUsername = $username; + } + + public static function restoreUsername() + { + self::$overriddenUsername = null; + } +} diff --git a/library/Director/Objects/DirectorDatafield.php b/library/Director/Objects/DirectorDatafield.php new file mode 100644 index 0000000..84db068 --- /dev/null +++ b/library/Director/Objects/DirectorDatafield.php @@ -0,0 +1,344 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Core\Json; +use Icinga\Module\Director\Data\Db\DbObjectWithSettings; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\CompareBasketObject; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Forms\IcingaServiceForm; +use Icinga\Module\Director\Hook\DataTypeHook; +use Icinga\Module\Director\Resolver\OverriddenVarsResolver; +use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use InvalidArgumentException; +use Zend_Form_Element as ZfElement; + +class DirectorDatafield extends DbObjectWithSettings +{ + protected $table = 'director_datafield'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = [ + 'id' => null, + 'category_id' => null, + 'varname' => null, + 'caption' => null, + 'description' => null, + 'datatype' => null, + 'format' => null, + ]; + + protected $relations = [ + 'category' => 'DirectorDatafieldCategory' + ]; + + protected $settingsTable = 'director_datafield_setting'; + + protected $settingsRemoteId = 'datafield_id'; + + /** @var DirectorDatafieldCategory|null */ + private $category; + + private $object; + + public static function fromDbRow($row, Db $connection) + { + $obj = static::create((array) $row, $connection); + $obj->loadedFromDb = true; + // TODO: $obj->setUnmodified(); + $obj->hasBeenModified = false; + $obj->modifiedProperties = array(); + $settings = $obj->getSettings(); + // TODO: eventually prefetch + $obj->onLoadFromDb(); + + // Restoring values eventually destroyed by onLoadFromDb + foreach ($settings as $key => $value) { + $obj->settings[$key] = $value; + } + + return $obj; + } + + public function hasCategory() + { + return $this->category !== null || $this->get('category_id') !== null; + } + + /** + * @return DirectorDatafieldCategory|null + * @throws \Icinga\Exception\NotFoundError + */ + public function getCategory() + { + if ($this->category) { + return $this->category; + } elseif ($id = $this->get('category_id')) { + return DirectorDatafieldCategory::loadWithAutoIncId($id, $this->getConnection()); + } else { + return null; + } + } + + public function getCategoryName() + { + $category = $this->getCategory(); + if ($category === null) { + return null; + } else { + return $category->get('category_name'); + } + } + + public function setCategory($category) + { + if ($category === null) { + $this->category = null; + $this->set('category_id', null); + } elseif ($category instanceof DirectorDatafieldCategory) { + if ($category->hasBeenLoadedFromDb()) { + $this->set('category_id', $category->get('id')); + } + $this->category = $category; + } else { + if (DirectorDatafieldCategory::exists($category, $this->getConnection())) { + $this->setCategory(DirectorDatafieldCategory::load($category, $this->getConnection())); + } else { + $this->setCategory(DirectorDatafieldCategory::create([ + 'category_name' => $category + ], $this->getConnection())); + } + } + + return $this; + } + + /** + * @return object + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + $plain = (object) $this->getProperties(); + $plain->originalId = $plain->id; + unset($plain->id); + $plain->settings = (object) $this->getSettings(); + + if (property_exists($plain->settings, 'datalist_id')) { + $plain->settings->datalist = DirectorDatalist::loadWithAutoIncId( + $plain->settings->datalist_id, + $this->getConnection() + )->get('list_name'); + unset($plain->settings->datalist_id); + } + if (property_exists($plain, 'category_id')) { + $plain->category = $this->getCategoryName(); + unset($plain->category_id); + } + + return $plain; + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return DirectorDatafield + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + if (isset($properties['originalId'])) { + $id = $properties['originalId']; + unset($properties['originalId']); + } else { + $id = null; + } + + if (isset($properties['settings']->datalist)) { + // Just try to load the list, import should fail if missing + $list = DirectorDatalist::load( + $properties['settings']->datalist, + $db + ); + } else { + $list = null; + } + + $compare = Json::decode(Json::encode($properties)); + if ($id && static::exists($id, $db)) { + $existing = static::loadWithAutoIncId($id, $db); + $existingProperties = (array) $existing->export(); + unset($existingProperties['originalId']); + if (CompareBasketObject::equals((object) $compare, (object) $existingProperties)) { + return $existing; + } + } + + if ($list) { + unset($properties['settings']->datalist); + $properties['settings']->datalist_id = $list->get('id'); + } + + $dba = $db->getDbAdapter(); + $query = $dba->select() + ->from('director_datafield') + ->where('varname = ?', $plain->varname); + $candidates = DirectorDatafield::loadAll($db, $query); + + foreach ($candidates as $candidate) { + $export = $candidate->export(); + unset($export->originalId); + CompareBasketObject::normalize($export); + if (CompareBasketObject::equals($export, $compare)) { + return $candidate; + } + } + + return static::create($properties, $db); + } + + protected function beforeStore() + { + if ($this->category) { + if (!$this->category->hasBeenLoadedFromDb()) { + throw new \RuntimeException('Trying to store a datafield with an unstored Category'); + } + $this->set('category_id', $this->category->get('id')); + } + } + + protected function setObject(IcingaObject $object) + { + $this->object = $object; + } + + protected function getObject() + { + return $this->object; + } + + public function getFormElement(DirectorObjectForm $form, $name = null) + { + $className = $this->get('datatype'); + + if ($name === null) { + $name = 'var_' . $this->get('varname'); + } + + if (! class_exists($className)) { + $form->addElement('text', $name, array('disabled' => 'disabled')); + $el = $form->getElement($name); + $msg = $form->translate('Form element could not be created, %s is missing'); + $el->addError(sprintf($msg, $className)); + return $el; + } + + /** @var DataTypeHook $dataType */ + $dataType = new $className; + $dataType->setSettings($this->getSettings()); + $el = $dataType->getFormElement($name, $form); + + if ($this->getSetting('icinga_type') !== 'command' + && $this->getSetting('is_required') === 'y' + ) { + $el->setRequired(true); + } + if ($caption = $this->get('caption')) { + $el->setLabel($caption); + } + + if ($description = $this->get('description')) { + $el->setDescription($description); + } + + $this->applyObjectData($el, $form); + + return $el; + } + + protected function applyObjectData(ZfElement $el, DirectorObjectForm $form) + { + $object = $form->getObject(); + if (! ($object instanceof IcingaObject)) { + return; + } + if ($object->isTemplate()) { + $el->setRequired(false); + } + + $varName = $this->get('varname'); + $inherited = $origin = null; + + if ($form instanceof IcingaServiceForm && $form->providesOverrides()) { + $resolver = new OverriddenVarsResolver($form->getDb()); + $vars = $resolver->fetchForServiceName($form->getHost(), $object->getObjectName()); + foreach ($vars as $host => $values) { + if (\property_exists($values, $varName)) { + $inherited = $values->$varName; + $origin = $host; + } + } + } + + if ($inherited === null) { + $inherited = $object->getInheritedVar($varName); + if (null !== $inherited) { + $origin = $object->getOriginForVar($varName); + } + } + + if ($inherited === null) { + $cmd = $this->eventuallyGetResolvedCommandVar($object, $varName); + if ($cmd !== null) { + list($inherited, $origin) = $cmd; + } + } + + if ($inherited !== null) { + $form->setInheritedValue($el, $inherited, $origin); + } + } + + protected function eventuallyGetResolvedCommandVar(IcingaObject $object, $varName) + { + if (! $object->hasRelation('check_command')) { + return null; + } + + // TODO: Move all of this elsewhere and test it + try { + /** @var IcingaCommand $command */ + $command = $object->getResolvedRelated('check_command'); + if ($command === null) { + return null; + } + $inherited = $command->vars()->get($varName); + $inheritedFrom = null; + + if ($inherited !== null) { + $inherited = $inherited->getValue(); + } + + if ($inherited === null) { + $inherited = $command->getResolvedVar($varName); + if ($inherited === null) { + $inheritedFrom = $command->getOriginForVar($varName); + } + } else { + $inheritedFrom = $command->getObjectName(); + } + + $inherited = $command->getResolvedVar($varName); + + return [$inherited, $inheritedFrom]; + } catch (\Exception $e) { + return null; + } + } +} diff --git a/library/Director/Objects/DirectorDatafieldCategory.php b/library/Director/Objects/DirectorDatafieldCategory.php new file mode 100644 index 0000000..6cb4fb4 --- /dev/null +++ b/library/Director/Objects/DirectorDatafieldCategory.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Exception\DuplicateKeyException; + +class DirectorDatafieldCategory extends DbObject +{ + protected $table = 'director_datafield_category'; + + protected $keyName = 'category_name'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = [ + 'id' => null, + 'category_name' => null, + 'description' => null, + ]; + + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return object + */ + public function export() + { + $plain = (object) $this->getProperties(); + $plain->originalId = $plain->id; + unset($plain->id); + return $plain; + } + + /** + * @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; + unset($properties['originalId']); + $key = $properties['category_name']; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Cannot import, DatafieldCategory "%s" already exists', + $key + ); + } else { + $object = static::create([], $db); + } + + $object->setProperties($properties); + + return $object; + } +} diff --git a/library/Director/Objects/DirectorDatalist.php b/library/Director/Objects/DirectorDatalist.php new file mode 100644 index 0000000..ae5c983 --- /dev/null +++ b/library/Director/Objects/DirectorDatalist.php @@ -0,0 +1,225 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Exception; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; + +class DirectorDatalist extends DbObject implements ExportInterface +{ + protected $table = 'director_datalist'; + + protected $keyName = 'list_name'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = array( + 'id' => null, + 'list_name' => null, + 'owner' => null + ); + + /** @var DirectorDatalistEntry[] */ + protected $storedEntries; + + public function getUniqueIdentifier() + { + return $this->get('list_name'); + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return static + * @throws \Icinga\Exception\NotFoundError + * @throws DuplicateKeyException + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + if (isset($properties['originalId'])) { + unset($properties['originalId']); + } else { + $id = null; + } + $name = $properties['list_name']; + + if ($replace && static::exists($name, $db)) { + $object = static::load($name, $db); + } elseif (static::exists($name, $db)) { + throw new DuplicateKeyException( + 'Data List %s already exists', + $name + ); + } else { + $object = static::create([], $db); + } + $object->setProperties($properties); + + return $object; + } + + public function setEntries($entries) + { + $existing = $this->getStoredEntries(); + + $new = []; + $seen = []; + $modified = false; + + foreach ($entries as $entry) { + $name = $entry->entry_name; + $entry = DirectorDatalistEntry::create((array) $entry); + $seen[$name] = true; + if (isset($existing[$name])) { + $existing[$name]->replaceWith($entry); + if (! $modified && $existing[$name]->hasBeenModified()) { + $modified = true; + } + } else { + $modified = true; + $new[] = $entry; + } + } + + foreach (array_keys($existing) as $key) { + if (! isset($seen[$key])) { + $existing[$key]->markForRemoval(); + $modified = true; + } + } + + foreach ($new as $entry) { + $existing[$entry->get('entry_name')] = $entry; + } + + if ($modified) { + $this->hasBeenModified = true; + } + + $this->storedEntries = $existing; + ksort($this->storedEntries); + + return $this; + } + + protected function beforeDelete() + { + if ($this->hasBeenUsed()) { + throw new Exception( + sprintf( + "Cannot delete '%s', as the datalist '%s' is currently being used.", + $this->get('list_name'), + $this->get('list_name') + ) + ); + } + } + + protected function hasBeenUsed() + { + $datalistType = 'Icinga\\Module\\Director\\DataType\\DataTypeDatalist'; + $db = $this->getDb(); + + $dataFieldsCheck = $db->select() + ->from(['df' =>'director_datafield'], ['varname']) + ->join( + ['dfs' => 'director_datafield_setting'], + 'dfs.datafield_id = df.id AND dfs.setting_name = \'datalist_id\'', + [] + ) + ->join( + ['l' => 'director_datalist'], + 'l.id = dfs.setting_value', + [] + ) + ->where('datatype = ?', $datalistType) + ->where('setting_value = ?', $this->get('id')); + + if ($db->fetchOne($dataFieldsCheck)) { + return true; + } + + $syncCheck = $db->select() + ->from(['sp' =>'sync_property'], ['source_expression']) + ->where('sp.destination_field = ?', 'list_id') + ->where('sp.source_expression = ?', $this->get('id')); + + if ($db->fetchOne($syncCheck)) { + return true; + } + + return false; + } + + /** + * @throws DuplicateKeyException + */ + public function onStore() + { + if ($this->storedEntries) { + $db = $this->getConnection(); + $removedKeys = []; + $myId = $this->get('id'); + + foreach ($this->storedEntries as $key => $entry) { + if ($entry->shouldBeRemoved()) { + $entry->delete(); + $removedKeys[] = $key; + } else { + if (! $entry->hasBeenLoadedFromDb()) { + $entry->set('list_id', $myId); + } + $entry->set('list_id', $myId); + $entry->store($db); + } + } + + foreach ($removedKeys as $key) { + unset($this->storedEntries[$key]); + } + } + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return object + */ + public function export() + { + $plain = (object) $this->getProperties(); + $plain->originalId = $plain->id; + unset($plain->id); + + $plain->entries = []; + foreach ($this->getStoredEntries() as $key => $entry) { + if ($entry->shouldBeRemoved()) { + continue; + } + $plainEntry = (object) $entry->getProperties(); + unset($plainEntry->list_id); + + $plain->entries[] = $plainEntry; + } + + return $plain; + } + + protected function getStoredEntries() + { + if ($this->storedEntries === null) { + if ($id = $this->get('id')) { + $this->storedEntries = DirectorDatalistEntry::loadAllForList($this); + ksort($this->storedEntries); + } else { + $this->storedEntries = []; + } + } + + return $this->storedEntries; + } +} diff --git a/library/Director/Objects/DirectorDatalistEntry.php b/library/Director/Objects/DirectorDatalistEntry.php new file mode 100644 index 0000000..086686a --- /dev/null +++ b/library/Director/Objects/DirectorDatalistEntry.php @@ -0,0 +1,112 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; +use RuntimeException; + +class DirectorDatalistEntry extends DbObject +{ + protected $keyName = ['list_id', 'entry_name']; + + protected $table = 'director_datalist_entry'; + + private $shouldBeRemoved = false; + + protected $defaultProperties = [ + 'list_id' => null, + 'entry_name' => null, + 'entry_value' => null, + 'format' => null, + 'allowed_roles' => null, + ]; + + /** + * @param DirectorDatalist $list + * @return static[] + */ + public static function loadAllForList(DirectorDatalist $list) + { + $query = $list->getDb() + ->select() + ->from('director_datalist_entry') + ->where('list_id = ?', $list->get('id')) + ->order('entry_name ASC'); + + return static::loadAll($list->getConnection(), $query, 'entry_name'); + } + + /** + * @param $roles + * @codingStandardsIgnoreStart + */ + public function setAllowed_roles($roles) + { + // @codingStandardsIgnoreEnd + $key = 'allowed_roles'; + if (is_array($roles)) { + $this->reallySet($key, json_encode($roles)); + } elseif (null === $roles) { + $this->reallySet($key, null); + } else { + throw new RuntimeException( + 'Expected array or null for allowed_roles, got %s', + var_export($roles, 1) + ); + } + } + + /** + * @return array|null + * @codingStandardsIgnoreStart + */ + public function getAllowed_roles() + { + // @codingStandardsIgnoreEnd + $roles = $this->getProperty('allowed_roles'); + if (is_string($roles)) { + return json_decode($roles); + } else { + return $roles; + } + } + + public function replaceWith(DirectorDatalistEntry $object) + { + $this->set('entry_value', $object->get('entry_value')); + if ($object->get('format')) { + $this->set('format', $object->get('format')); + } + + return $this; + } + + public function merge(DirectorDatalistEntry $object) + { + return $this->replaceWith($object); + } + + public function markForRemoval($remove = true) + { + $this->shouldBeRemoved = $remove; + + return $this; + } + + public function shouldBeRemoved() + { + return $this->shouldBeRemoved; + } + + public function onInsert() + { + } + + public function onUpdate() + { + } + + public function onDelete() + { + } +} diff --git a/library/Director/Objects/DirectorDeploymentLog.php b/library/Director/Objects/DirectorDeploymentLog.php new file mode 100644 index 0000000..0794a3c --- /dev/null +++ b/library/Director/Objects/DirectorDeploymentLog.php @@ -0,0 +1,199 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Core\CoreApi; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Util; + +class DirectorDeploymentLog extends DbObject +{ + protected $table = 'director_deployment_log'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + protected $config; + + protected $defaultProperties = [ + 'id' => null, + 'config_checksum' => null, + 'last_activity_checksum' => null, + 'peer_identity' => null, + 'start_time' => null, + 'end_time' => null, + 'abort_time' => null, + 'duration_connection' => null, + 'duration_dump' => null, + 'stage_name' => null, + 'stage_collected' => null, + 'connection_succeeded' => null, + 'dump_succeeded' => null, + 'startup_succeeded' => null, + 'username' => null, + 'startup_log' => null, + ]; + + protected $binaryProperties = [ + 'config_checksum', + 'last_activity_checksum' + ]; + + public function getConfigHexChecksum() + { + return bin2hex($this->config_checksum); + } + + public function getConfig() + { + if ($this->config === null) { + $this->config = IcingaConfig::load($this->config_checksum, $this->connection); + } + + return $this->config; + } + + public function isPending() + { + return $this->dump_succeeded === 'y' && $this->startup_log === null; + } + + public function succeeded() + { + return $this->startup_succeeded === 'y'; + } + + public function configEquals(IcingaConfig $config) + { + return $this->config_checksum === $config->getChecksum(); + } + + public function getDeploymentTimestamp() + { + return strtotime($this->start_time); + } + + public static function hasDeployments(Db $connection) + { + $db = $connection->getDbAdapter(); + $query = $db->select()->from( + 'director_deployment_log', + array('c' => 'COUNT(*)') + ); + + return (int) $db->fetchOne($query) > 0; + } + + public static function getConfigChecksumForStageName(Db $connection, $stage) + { + if ($stage === null) { + return null; + } + $db = $connection->getDbAdapter(); + $query = $db->select() + ->from( + array('l' => 'director_deployment_log'), + array('c' => $connection->dbHexFunc('l.config_checksum')) + )->where('l.stage_name = ?'); + + return $db->fetchOne($query, $stage); + } + + /** + * @param Db $connection + * @return DirectorDeploymentLog + * @throws NotFoundError + */ + public static function loadLatest(Db $connection) + { + $db = $connection->getDbAdapter(); + $query = $db->select()->from( + array('l' => 'director_deployment_log'), + array('id' => 'MAX(l.id)') + ); + + return static::load($db->fetchOne($query), $connection); + } + + /** + * @param Db $connection + * @return ?DirectorDeploymentLog + */ + public static function optionalLatest(Db $connection) + { + try { + return static::loadLatest($connection); + } catch (NotFoundError $exception) { + return null; + } + } + + /** + * @param CoreApi $api + * @param Db $connection + * @return DirectorDeploymentLog + */ + public static function getRelatedToActiveStage(CoreApi $api, Db $connection) + { + try { + return static::requireRelatedToActiveStage($api, $connection); + } catch (Exception $e) { + return null; + } + } + + /** + * @param CoreApi $api + * @param Db $connection + * @return DirectorDeploymentLog + * @throws NotFoundError + */ + public static function requireRelatedToActiveStage(CoreApi $api, Db $connection) + { + $stage = $api->getActiveStageName(); + + if (! strlen($stage)) { + throw new NotFoundError('Got no active stage name'); + } + $db = $connection->getDbAdapter(); + $query = $db->select()->from( + ['l' => 'director_deployment_log'], + ['id' => 'MAX(l.id)'] + )->where('l.stage_name = ?', $stage); + + return static::load($db->fetchOne($query), $connection); + } + + /** + * @return static[] + */ + public static function getUncollected(Db $connection) + { + $db = $connection->getDbAdapter(); + $query = $db->select() + ->from('director_deployment_log') + ->where('stage_name IS NOT NULL') + ->where('stage_collected IS NULL') + ->where('startup_succeeded IS NULL') + ->order('stage_name'); + + return static::loadAll($connection, $query, 'stage_name'); + } + + public static function hasUncollected(Db $connection) + { + $db = $connection->getDbAdapter(); + $query = $db->select() + ->from('director_deployment_log', ['cnt' => 'COUNT(*)']) + ->where('stage_name IS NOT NULL') + ->where('stage_collected IS NULL') + ->where('startup_succeeded IS NULL'); + + return $db->fetchOne($query) > 0; + } +} diff --git a/library/Director/Objects/DirectorJob.php b/library/Director/Objects/DirectorJob.php new file mode 100644 index 0000000..361f764 --- /dev/null +++ b/library/Director/Objects/DirectorJob.php @@ -0,0 +1,314 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Daemon\Logger; +use Icinga\Module\Director\Data\Db\DbObjectWithSettings; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Hook\JobHook; +use Exception; +use InvalidArgumentException; + +class DirectorJob extends DbObjectWithSettings implements ExportInterface, InstantiatedViaHook +{ + /** @var JobHook */ + protected $job; + + protected $table = 'director_job'; + + protected $keyName = 'job_name'; + + protected $autoincKeyName = 'id'; + + protected $protectAutoinc = false; + + protected $defaultProperties = [ + 'id' => null, + 'job_name' => null, + 'job_class' => null, + 'disabled' => null, + 'run_interval' => null, + 'last_attempt_succeeded' => null, + 'ts_last_attempt' => null, + 'ts_last_error' => null, + 'last_error_message' => null, + 'timeperiod_id' => null, + ]; + + protected $stateProperties = [ + 'last_attempt_succeeded', + 'last_error_message', + 'ts_last_attempt', + 'ts_last_error', + ]; + + protected $settingsTable = 'director_job_setting'; + + protected $settingsRemoteId = 'job_id'; + + public function getUniqueIdentifier() + { + return $this->get('job_name'); + } + + /** + * @deprecated please use JobHook::getInstance() + * @return JobHook + */ + public function job() + { + return $this->getInstance(); + } + + /** + * @return JobHook + */ + public function getInstance() + { + if ($this->job === null) { + $class = $this->get('job_class'); + $this->job = new $class; + $this->job->setDb($this->connection); + $this->job->setDefinition($this); + } + + return $this->job; + } + + /** + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function run() + { + $job = $this->getInstance(); + $this->set('ts_last_attempt', date('Y-m-d H:i:s')); + + try { + $job->run(); + $this->set('last_attempt_succeeded', 'y'); + $success = true; + } catch (Exception $e) { + Logger::error($e->getMessage()); + $this->set('ts_last_error', date('Y-m-d H:i:s')); + $this->set('last_error_message', $e->getMessage()); + $this->set('last_attempt_succeeded', 'n'); + $success = false; + } + + if ($this->hasBeenModified()) { + $this->store(); + } + + return $success; + } + + /** + * @return bool + */ + public function shouldRun() + { + return (! $this->hasBeenDisabled()) && $this->isPending(); + } + + /** + * @return bool + */ + public function isOverdue() + { + if (! $this->shouldRun()) { + return false; + } + + return ( + strtotime((int) $this->get('ts_last_attempt')) + $this->get('run_interval') * 2 + ) < time(); + } + + public function hasBeenDisabled() + { + return $this->get('disabled') === 'y'; + } + + /** + * @return bool + */ + public function isPending() + { + if ($this->get('ts_last_attempt') === null) { + return $this->isWithinTimeperiod(); + } + + if (strtotime($this->get('ts_last_attempt')) + $this->get('run_interval') < time()) { + return $this->isWithinTimeperiod(); + } + + return false; + } + + /** + * @return bool + */ + public function isWithinTimeperiod() + { + if ($this->hasTimeperiod()) { + return $this->timeperiod()->isActive(); + } else { + return true; + } + } + + public function lastAttemptSucceeded() + { + return $this->get('last_attempt_succeeded') === 'y'; + } + + public function lastAttemptFailed() + { + return $this->get('last_attempt_succeeded') === 'n'; + } + + public function hasTimeperiod() + { + return $this->get('timeperiod_id') !== null; + } + + /** + * @param $timeperiod + * @return $this + * @throws \Icinga\Exception\NotFoundError + */ + public function setTimeperiod($timeperiod) + { + if (is_string($timeperiod)) { + $timeperiod = IcingaTimePeriod::load($timeperiod, $this->connection); + } elseif (! $timeperiod instanceof IcingaTimePeriod) { + throw new InvalidArgumentException('TimePeriod expected'); + } + + $this->set('timeperiod_id', $timeperiod->get('id')); + + return $this; + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + $plain = (object) $this->getProperties(); + $plain->originalId = $plain->id; + unset($plain->id); + unset($plain->timeperiod_id); + if ($this->hasTimeperiod()) { + $plain->timeperiod = $this->timeperiod()->getObjectName(); + } + + foreach ($this->stateProperties as $key) { + unset($plain->$key); + } + $plain->settings = $this->getInstance()->exportSettings(); + + return $plain; + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return DirectorJob + * @throws DuplicateKeyException + * @throws NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $dummy = new static; + $idCol = $dummy->autoincKeyName; + $keyCol = $dummy->keyName; + $properties = (array) $plain; + if (isset($properties['originalId'])) { + $id = $properties['originalId']; + unset($properties['originalId']); + } else { + $id = null; + } + $name = $properties[$keyCol]; + + if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) { + $object = static::loadWithAutoIncId($id, $db); + } elseif ($replace && static::exists($name, $db)) { + $object = static::load($name, $db); + } elseif (static::exists($name, $db)) { + throw new DuplicateKeyException( + 'Director Job "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + $settings = (array) $properties['settings']; + + if (array_key_exists('source', $settings) && ! (array_key_exists('source_id', $settings))) { + $val = ImportSource::load($settings['source'], $db)->get('id'); + $settings['source_id'] = $val; + unset($settings['source']); + } + + if (array_key_exists('rule', $settings) && ! (array_key_exists('rule_id', $settings))) { + $val = SyncRule::load($settings['rule'], $db)->get('id'); + $settings['rule_id'] = $val; + unset($settings['rule']); + } + + $properties['settings'] = (object) $settings; + $object->setProperties($properties); + if ($id !== null) { + $object->reallySet($idCol, $id); + } + + return $object; + } + + /** + * @param string $name + * @param int $id + * @param Db $connection + * @api internal + * @return bool + */ + protected static function existsWithNameAndId($name, $id, Db $connection) + { + $db = $connection->getDbAdapter(); + $dummy = new static; + $idCol = $dummy->autoincKeyName; + $keyCol = $dummy->keyName; + + return (string) $id === (string) $db->fetchOne( + $db->select() + ->from($dummy->table, $idCol) + ->where("$idCol = ?", $id) + ->where("$keyCol = ?", $name) + ); + } + + /** + * @api internal Exporter only + * @return IcingaTimePeriod + */ + public function timeperiod() + { + try { + return IcingaTimePeriod::loadWithAutoIncId($this->get('timeperiod_id'), $this->connection); + } catch (NotFoundError $e) { + throw new \RuntimeException(sprintf( + 'The TimePeriod configured for Job "%s" could not have been found', + $this->get('name') + )); + } + } +} diff --git a/library/Director/Objects/DynamicApplyMatches.php b/library/Director/Objects/DynamicApplyMatches.php new file mode 100644 index 0000000..9341d1a --- /dev/null +++ b/library/Director/Objects/DynamicApplyMatches.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class DynamicApplyMatches extends ObjectApplyMatches +{ + protected static $type = ''; + + public static function setType($type) + { + static::$type = $type; + return static::$type; + } +} diff --git a/library/Director/Objects/Extension/Arguments.php b/library/Director/Objects/Extension/Arguments.php new file mode 100644 index 0000000..3acbdd3 --- /dev/null +++ b/library/Director/Objects/Extension/Arguments.php @@ -0,0 +1,61 @@ +<?php + +namespace Icinga\Module\Director\Objects\Extension; + +use Icinga\Module\Director\Objects\IcingaArguments; +use Icinga\Module\Director\Objects\IcingaObject; + +trait Arguments +{ + private $arguments; + + public function arguments() + { + /** @var IcingaObject $this */ + if ($this->arguments === null) { + if ($this->hasBeenLoadedFromDb()) { + $this->arguments = IcingaArguments::loadForStoredObject($this); + } else { + $this->arguments = new IcingaArguments($this); + } + } + + return $this->arguments; + } + + public function gotArguments() + { + return null !== $this->arguments; + } + + public function unsetArguments() + { + unset($this->arguments); + } + + /** + * @return string + */ + protected function renderArguments() + { + return $this->arguments()->toConfigString(); + } + + /** + * @param $value + * @return $this + */ + protected function setArguments($value) + { + $this->arguments()->setArguments($value); + return $this; + } + + /** + * @return array + */ + protected function getArguments() + { + return $this->arguments()->toPlainObject(); + } +} diff --git a/library/Director/Objects/Extension/FlappingSupport.php b/library/Director/Objects/Extension/FlappingSupport.php new file mode 100644 index 0000000..a86f10d --- /dev/null +++ b/library/Director/Objects/Extension/FlappingSupport.php @@ -0,0 +1,54 @@ +<?php + +namespace Icinga\Module\Director\Objects\Extension; + +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; + +trait FlappingSupport +{ + /** + * @param $value + * @return string + * @codingStandardsIgnoreStart + */ + protected function renderFlapping_threshold_high($value) + { + return $this->renderFlappingThreshold('flapping_threshold_high', $value); + } + + /** + * @param $value + * @return string + */ + protected function renderFlapping_threshold_low($value) + { + return $this->renderFlappingThreshold('flapping_threshold_low', $value); + } + + protected function renderFlappingThreshold($key, $value) + { + return sprintf( + " try { // This setting is only available in Icinga >= 2.8.0\n" + . " %s" + . " } except { globals.directorWarnOnceForThresholds() }\n", + c::renderKeyValue($key, c::renderFloat($value)) + ); + } + + protected function renderLegacyEnable_flapping($value) + { + return c1::renderKeyValue('flap_detection_enabled', c1::renderBoolean($value)); + } + + protected function renderLegacyFlapping_threshold_high($value) + { + return c1::renderKeyValue('high_flap_threshold', $value); + } + + protected function renderLegacyFlapping_threshold_low($value) + { + // @codingStandardsIgnoreEnd + return c1::renderKeyValue('low_flap_threshold', $value); + } +} diff --git a/library/Director/Objects/Extension/PriorityColumn.php b/library/Director/Objects/Extension/PriorityColumn.php new file mode 100644 index 0000000..638bdc6 --- /dev/null +++ b/library/Director/Objects/Extension/PriorityColumn.php @@ -0,0 +1,40 @@ +<?php + +namespace Icinga\Module\Director\Objects\Extension; + +use Zend_Db_Expr as Expr; + +trait PriorityColumn +{ + public function setNextPriority($prioSetColumn = null, $prioColumn = 'priority') + { + /** @var \Zend_Db_Adapter_Abstract $db */ + $db = $this->getDb(); + $prioValue = '(CASE WHEN MAX(priosub.priority) IS NULL THEN 1' + . ' ELSE MAX(priosub.priority) + 1 END)'; + $query = $db->select() + ->from( + ['priosub' => $this->getTableName()], + "$prioValue" + ); + + if ($prioSetColumn !== null) { + $query->where("priosub.$prioSetColumn = ?", $this->get($prioSetColumn)); + } + + $this->set($prioColumn, new Expr('(' . $query . ')')); + + return $this; + } + + protected function refreshPriortyProperty($prioColumn = 'priority') + { + /** @var \Zend_Db_Adapter_Abstract $db */ + $db = $this->getDb(); + $idCol = $this->getAutoincKeyName(); + $query = $db->select() + ->from($this->getTableName(), $prioColumn) + ->where("$idCol = ?", $this->get($idCol)); + $this->reallySet($prioColumn, $db->fetchOne($query)); + } +} diff --git a/library/Director/Objects/GroupMembershipResolver.php b/library/Director/Objects/GroupMembershipResolver.php new file mode 100644 index 0000000..f5ef418 --- /dev/null +++ b/library/Director/Objects/GroupMembershipResolver.php @@ -0,0 +1,689 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\IcingaObjectFilterHelper; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use InvalidArgumentException; +use LogicException; +use Zend_Db_Select as ZfSelect; + +/** + * Class GroupMembershipResolver + * + * - Fetches all involved assignments + * - Fetch all (or one) object + * - Fetch all (or one) group + */ +abstract class GroupMembershipResolver +{ + /** @var string Object type, 'host', 'service', 'user' or similar */ + protected $type; + + /** @var array */ + protected $existingMappings; + + /** @var array */ + protected $newMappings; + + /** @var Db */ + protected $connection; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + /** @var IcingaObject[] */ + protected $objects; + + /** @var IcingaObjectGroup[] */ + protected $groups = array(); + + /** @var array */ + protected $staticGroups = array(); + + /** @var bool */ + protected $deferred = false; + + /** @var bool */ + protected $checked = false; + + /** @var bool */ + protected $useTransactions = false; + + protected $groupMap; + + public function __construct(Db $connection) + { + $this->connection = $connection; + $this->db = $connection->getDbAdapter(); + } + + /** + * @return $this + * @throws \Zend_Db_Adapter_Exception + */ + public function refreshAllMappings() + { + return $this->clearGroups()->clearObjects()->refreshDb(true); + } + + public function checkDb() + { + if ($this->checked) { + return $this; + } + + if ($this->isDeferred()) { + // ensure we are not working with cached data + IcingaTemplateRepository::clear(); + } + + Benchmark::measure('Rechecking all objects'); + $this->recheckAllObjects($this->getAppliedGroups()); + if (empty($this->objects) && empty($this->groups)) { + Benchmark::measure('Nothing to check, got no qualified object'); + return $this; + } + + Benchmark::measure('Recheck done, loading existing mappings'); + $this->fetchStoredMappings(); + Benchmark::measure('Got stored group mappings'); + + $this->checked = true; + return $this; + } + + /** + * @param bool $force + * @return $this + * @throws \Zend_Db_Adapter_Exception + */ + public function refreshDb($force = false) + { + if ($force || ! $this->isDeferred()) { + $this->checkDb(); + + if (empty($this->objects) && empty($this->groups)) { + Benchmark::measure('Nothing to check, got no qualified object'); + + return $this; + } + + Benchmark::measure('Ready, going to store new mappings'); + $this->storeNewMappings(); + $this->removeOutdatedMappings(); + Benchmark::measure('Updated group mappings in db'); + } + + return $this; + } + + /** + * @param bool $defer + * @return $this + */ + public function defer($defer = true) + { + $this->deferred = $defer; + return $this; + } + + /** + * @param $use + * @return $this + */ + public function setUseTransactions($use) + { + $this->useTransactions = $use; + return $this; + } + + public function getType() + { + if ($this->type === null) { + throw new LogicException(sprintf( + '"type" is required when extending %s, got none in %s', + __CLASS__, + get_class($this) + )); + } + + return $this->type; + } + + /** + * @return bool + */ + public function isDeferred() + { + return $this->deferred; + } + + /** + * @param IcingaObject $object + * @return $this + */ + public function addObject(IcingaObject $object) + { + // Hint: cannot use hasBeenLoadedFromDB, as it is false in onStore() + // for new objects + if (null === ($id = $object->get('id'))) { + return $this; + } + // Disabling for now, how should this work? + // $this->assertBeenLoadedFromDb($object); + if ($this->objects === null) { + $this->objects = []; + } + + if ($object->isTemplate()) { + $this->includeChildObjects($object); + } else { + $this->objects[$id] = $object; + } + + return $this; + } + + /** + * @param IcingaObject[] $objects + * @return $this + */ + public function addObjects(array $objects) + { + foreach ($objects as $object) { + $this->addObject($object); + } + + return $this; + } + + protected function includeChildObjects(IcingaObject $object) + { + $query = $this->db->select() + ->from(['o' => $object->getTableName()]) + ->where('o.object_type = ?', 'object'); + + IcingaObjectFilterHelper::filterByTemplate( + $query, + $object, + 'o', + Db\IcingaObjectFilterHelper::INHERIT_DIRECT_OR_INDIRECT + ); + + foreach ($object::loadAll($this->connection, $query) as $child) { + $this->objects[$child->getProperty('id')] = $child; + } + + return $this; + } + + /** + * @param IcingaObject $object + * @return $this + */ + public function setObject(IcingaObject $object) + { + $this->clearObjects(); + return $this->addObject($object); + } + + /** + * @param IcingaObject[] $objects + * @return $this + */ + public function setObjects(array $objects) + { + $this->clearObjects(); + return $this->addObjects($objects); + } + + /** + * @return $this + */ + public function clearObjects() + { + $this->objects = array(); + return $this; + } + + /** + * @param IcingaObjectGroup $group + * @return $this + */ + public function addGroup(IcingaObjectGroup $group) + { + $this->assertBeenLoadedFromDb($group); + $this->groups[$group->get('id')] = $group; + + $this->checked = false; + + return $this; + } + + /** + * @param IcingaObjectGroup[] $groups + * @return $this + */ + public function addGroups(array $groups) + { + foreach ($groups as $group) { + $this->addGroup($group); + } + + $this->checked = false; + + return $this; + } + + /** + * @param IcingaObjectGroup $group + * @return $this + */ + public function setGroup(IcingaObjectGroup $group) + { + $this->clearGroups(); + return $this->addGroup($group); + } + + /** + * @param array $groups + * @return $this + */ + public function setGroups(array $groups) + { + $this->clearGroups(); + return $this->addGroups($groups); + } + + /** + * @return $this + */ + public function clearGroups() + { + $this->objects = array(); + $this->checked = false; + return $this; + } + + public function getNewMappings() + { + if ($this->newMappings !== null && $this->existingMappings !== null) { + return $this->getDifference($this->newMappings, $this->existingMappings); + } else { + return []; + } + } + + /** + * @throws \Zend_Db_Adapter_Exception + */ + protected function storeNewMappings() + { + $diff = $this->getNewMappings(); + $count = count($diff); + if ($count === 0) { + return; + } + + $db = $this->db; + $this->beginTransaction(); + foreach ($diff as $row) { + $db->insert( + $this->getResolvedTableName(), + $row + ); + } + + $this->commit(); + Benchmark::measure( + sprintf( + 'Stored %d new resolved group memberships', + $count + ) + ); + } + + protected function getGroupId($name) + { + $type = $this->type; + if ($this->groupMap === null) { + $this->groupMap = $this->db->fetchPairs( + $this->db->select()->from("icinga_${type}group", ['object_name', 'id']) + ); + } + + if (array_key_exists($name, $this->groupMap)) { + return $this->groupMap[$name]; + } else { + throw new InvalidArgumentException(sprintf( + 'Unable to lookup the group name for "%s"', + $name + )); + } + } + + public function getOutdatedMappings() + { + if ($this->newMappings !== null && $this->existingMappings !== null) { + return $this->getDifference($this->existingMappings, $this->newMappings); + } else { + return []; + } + } + + protected function removeOutdatedMappings() + { + $diff = $this->getOutdatedMappings(); + $count = count($diff); + if ($count === 0) { + return; + } + + $type = $this->getType(); + $db = $this->db; + $this->beginTransaction(); + foreach ($diff as $row) { + $db->delete( + $this->getResolvedTableName(), + sprintf( + "(${type}group_id = %d AND ${type}_id = %d)", + $row["${type}group_id"], + $row["${type}_id"] + ) + ); + } + + $this->commit(); + Benchmark::measure( + sprintf( + 'Removed %d outdated group memberships', + $count + ) + ); + } + + protected function getDifference(&$left, &$right) + { + $diff = array(); + + $type = $this->getType(); + foreach ($left as $groupId => $objectIds) { + if (array_key_exists($groupId, $right)) { + foreach ($objectIds as $objectId) { + if (! array_key_exists($objectId, $right[$groupId])) { + $diff[] = array( + "${type}group_id" => $groupId, + "${type}_id" => $objectId, + ); + } + } + } else { + foreach ($objectIds as $objectId) { + $diff[] = array( + "${type}group_id" => $groupId, + "${type}_id" => $objectId, + ); + } + } + } + + return $diff; + } + + /** + * This fetches already resolved memberships + */ + protected function fetchStoredMappings() + { + $mappings = array(); + + $type = $this->getType(); + $query = $this->db->select()->from( + array('hgh' => $this->getResolvedTableName()), + array( + 'group_id' => "${type}group_id", + 'object_id' => "${type}_id", + ) + ); + + $this->addMembershipWhere($query, "${type}_id", $this->objects); + $this->addMembershipWhere($query, "${type}group_id", $this->groups); + if (! empty($this->groups)) { + // load staticGroups (we touched here) additionally, so we can compare changes + $this->addMembershipWhere($query, "${type}group_id", $this->staticGroups); + } + + foreach ($this->db->fetchAll($query) as $row) { + $groupId = $row->group_id; + $objectId = $row->object_id; + if (! array_key_exists($groupId, $mappings)) { + $mappings[$groupId] = array(); + } + + $mappings[$groupId][$objectId] = $objectId; + } + + $this->existingMappings = $mappings; + } + + /** + * @param ZfSelect $query + * @param string $column + * @param IcingaObject[]|int[] $objects + * @return ZfSelect + */ + protected function addMembershipWhere(ZfSelect $query, $column, &$objects) + { + if (empty($objects)) { + return $query; + } + + $ids = array(); + foreach ($objects as $k => $object) { + if (is_int($object)) { + $ids[] = $k; + } elseif (is_string($object)) { + $ids[] = (int) $object; + } else { + $ids[] = (int) $object->get('id'); + } + } + + if (count($ids) === 1) { + $query->orWhere($column . ' = ?', $ids[0]); + } else { + $query->orWhere($column . ' IN (?)', $ids); + } + + return $query; + } + + protected function recheckAllObjects($groups) + { + $mappings = []; + $staticGroups = []; + + if ($this->objects === null) { + $objects = $this->fetchAllObjects(); + } else { + $objects = & $this->objects; + } + + $times = array(); + + foreach ($objects as $object) { + if ($object->shouldBeRemoved()) { + continue; + } + if ($object->isTemplate()) { + continue; + } + + $mt = microtime(true); + $id = $object->get('id'); + + DynamicApplyMatches::setType($this->type); + $resolver = DynamicApplyMatches::prepare($object); + foreach ($groups as $groupId => $filter) { + if ($resolver->matchesFilter($filter)) { + if (! array_key_exists($groupId, $mappings)) { + $mappings[$groupId] = []; + } + $mappings[$groupId][$id] = $id; + } + } + + // can only be run reliably when updating for all groups + $groupNames = $object->get('groups'); + if (empty($groupNames)) { + $groupNames = $object->listInheritedGroupNames(); + } + foreach ($groupNames as $name) { + $groupId = $this->getGroupId($name); + if (! array_key_exists($groupId, $mappings)) { + $mappings[$groupId] = []; + } + + $mappings[$groupId][$id] = $id; + $staticGroups[$groupId] = $groupId; + } + + $times[] = (microtime(true) - $mt) * 1000; + } + + $count = count($times); + $min = $max = $avg = 0; + if ($count > 0) { + $min = min($times); + $max = max($times); + $avg = array_sum($times) / $count; + } + + Benchmark::measure(sprintf( + '%sgroup apply recalculated: objects=%d groups=%d min=%d max=%d avg=%d (in ms)', + $this->type, + $count, + count($groups), + $min, + $max, + $avg + )); + + Benchmark::measure('Done with single assignments'); + + $this->newMappings = $mappings; + $this->staticGroups = $staticGroups; + } + + protected function getAppliedGroups() + { + if (empty($this->groups)) { + return $this->fetchAppliedGroups(); + } else { + return $this->buildAppliedGroups(); + } + } + + protected function buildAppliedGroups() + { + $list = array(); + foreach ($this->groups as $id => $group) { + $list[$id] = $group->get('assign_filter'); + } + + return $this->parseFilters($list); + } + + protected function fetchAppliedGroups() + { + $type = $this->getType(); + $query = $this->db->select()->from( + array('hg' => "icinga_${type}group"), + array( + 'id', + 'assign_filter', + ) + )->where("assign_filter IS NOT NULL AND assign_filter != ''"); + + return $this->parseFilters($this->db->fetchPairs($query)); + } + + /** + * Parsing a list of query strings to Filter + * + * @param string[] $list List of query strings + * + * @return Filter[] + */ + protected function parseFilters($list) + { + return array_map(function ($s) { + return Filter::fromQueryString($s); + }, $list); + } + + protected function getTableName() + { + $type = $this->getType(); + return "icinga_${type}group_${type}"; + } + + protected function getResolvedTableName() + { + return $this->getTableName() . '_resolved'; + } + + /** + * @return $this + */ + protected function beginTransaction() + { + if ($this->useTransactions) { + $this->db->beginTransaction(); + } + + return $this; + } + + /** + * @return $this + */ + protected function commit() + { + if ($this->useTransactions) { + $this->db->commit(); + } + + return $this; + } + + /** + * @return IcingaObject[] + */ + protected function getObjects() + { + if ($this->objects === null) { + $this->objects = $this->fetchAllObjects(); + } + + return $this->objects; + } + + protected function fetchAllObjects() + { + return IcingaObject::loadAllByType($this->getType(), $this->connection); + } + + protected function assertBeenLoadedFromDb(IcingaObject $object) + { + if (! is_int($object->get('id')) && ! ctype_digit($object->get('id'))) { + throw new LogicException( + 'Group resolver does not support unstored objects' + ); + } + } +} diff --git a/library/Director/Objects/HostApplyMatches.php b/library/Director/Objects/HostApplyMatches.php new file mode 100644 index 0000000..5feaee7 --- /dev/null +++ b/library/Director/Objects/HostApplyMatches.php @@ -0,0 +1,8 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class HostApplyMatches extends ObjectApplyMatches +{ + protected static $type = 'host'; +} diff --git a/library/Director/Objects/HostGroupMembershipResolver.php b/library/Director/Objects/HostGroupMembershipResolver.php new file mode 100644 index 0000000..b597017 --- /dev/null +++ b/library/Director/Objects/HostGroupMembershipResolver.php @@ -0,0 +1,8 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class HostGroupMembershipResolver extends GroupMembershipResolver +{ + protected $type = 'host'; +} diff --git a/library/Director/Objects/IcingaApiUser.php b/library/Director/Objects/IcingaApiUser.php new file mode 100644 index 0000000..bb4f9f8 --- /dev/null +++ b/library/Director/Objects/IcingaApiUser.php @@ -0,0 +1,31 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; + +class IcingaApiUser extends IcingaObject +{ + protected $table = 'icinga_apiuser'; + + protected $uuidColumn = 'uuid'; + + // TODO: Enable (and add table) if required + protected $supportsImports = false; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'password' => null, + 'client_dn' => null, + 'permissions' => null, + ]; + + protected function renderPassword() + { + return c::renderKeyValue('password', c::renderString('***')); + } +} diff --git a/library/Director/Objects/IcingaArguments.php b/library/Director/Objects/IcingaArguments.php new file mode 100644 index 0000000..e788da8 --- /dev/null +++ b/library/Director/Objects/IcingaArguments.php @@ -0,0 +1,442 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Countable; +use Exception; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use InvalidArgumentException; +use Iterator; + +class IcingaArguments implements Iterator, Countable, IcingaConfigRenderer +{ + const COMMENT_DSL_UNSUPPORTED = '/* Icinga 2 does not export DSL function bodies via API */'; + + /** @var IcingaCommandArgument[] */ + protected $storedArguments = []; + + /** @var IcingaCommandArgument[] */ + protected $arguments = []; + + protected $modified = false; + + protected $object; + + private $position = 0; + + protected $idx = []; + + public function __construct(IcingaObject $object) + { + $this->object = $object; + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->arguments); + } + + #[\ReturnTypeWillChange] + public function rewind() + { + $this->position = 0; + } + + public function hasBeenModified() + { + return $this->modified; + } + + #[\ReturnTypeWillChange] + public function current() + { + if (! $this->valid()) { + return null; + } + + return $this->arguments[$this->idx[$this->position]]; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->idx[$this->position]; + } + + #[\ReturnTypeWillChange] + public function next() + { + ++$this->position; + } + + #[\ReturnTypeWillChange] + public function valid() + { + return array_key_exists($this->position, $this->idx); + } + + public function get($key) + { + if (array_key_exists($key, $this->arguments)) { + if ($this->arguments[$key]->shouldBeRemoved()) { + return null; + } + + return $this->arguments[$key]; + } + + return null; + } + + public function set($key, $value) + { + if ($value === null) { + return $this->remove($key); + } + + if ($value instanceof IcingaCommandArgument) { + $argument = $value; + } else { + $argument = IcingaCommandArgument::create( + $this->mungeCommandArgument($key, $value) + ); + } + + $argument->set('command_id', $this->object->get('id')); + + $key = $argument->argument_name; + if (array_key_exists($key, $this->arguments)) { + $this->arguments[$key]->replaceWith($argument); + if ($this->arguments[$key]->hasBeenModified()) { + $this->modified = true; + } + } elseif (array_key_exists($key, $this->storedArguments)) { + $this->arguments[$key] = clone($this->storedArguments[$key]); + $this->arguments[$key]->replaceWith($argument); + if ($this->arguments[$key]->hasBeenModified()) { + $this->modified = true; + } + } else { + $this->add($argument); + $this->modified = true; + } + + return $this; + } + + protected function mungeCommandArgument($key, $value) + { + $attrs = [ + 'argument_name' => (string) $key, + ]; + + $map = [ + 'skip_key' => 'skip_key', + 'repeat_key' => 'repeat_key', + 'required' => 'required', + // 'order' => 'sort_order', + 'description' => 'description', + 'set_if' => 'set_if', + ]; + + $argValue = null; + if (is_object($value)) { + if (property_exists($value, 'order')) { + $attrs['sort_order'] = (string) $value->order; + } + + foreach ($map as $apiKey => $dbKey) { + if (property_exists($value, $apiKey)) { + $attrs[$dbKey] = $value->$apiKey; + } + } + if (property_exists($value, 'type')) { + // argument is directly set as function, no further properties + if ($value->type === 'Function') { + $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED; + $attrs['argument_format'] = 'expression'; + } + } elseif (property_exists($value, 'value')) { + // argument is a dictionary with further settings + if (is_object($value->value)) { + if ($value->value->type === 'Function' && property_exists($value->value, 'body')) { + // likely an export from Baskets that contains the actual function body + $attrs['argument_value'] = $value->value->body; + $attrs['argument_format'] = 'expression'; + } elseif ($value->value->type === 'Function') { + $attrs['argument_value'] = self::COMMENT_DSL_UNSUPPORTED; + $attrs['argument_format'] = 'expression'; + } else { + die('Unable to resolve command argument'); + } + } else { + $argValue = $value->value; + if (is_string($argValue)) { + $attrs['argument_value'] = $argValue; + $attrs['argument_format'] = 'string'; + } else { + $attrs['argument_value'] = $argValue; + $attrs['argument_format'] = 'json'; + } + } + } + } else { + if (is_string($value)) { + $attrs['argument_value'] = $value; + $attrs['argument_format'] = 'string'; + } else { + $attrs['argument_value'] = $value; + $attrs['argument_format'] = 'json'; + } + } + + if (array_key_exists('set_if', $attrs)) { + if (is_object($attrs['set_if']) && $attrs['set_if']->type === 'Function') { + $attrs['set_if'] = self::COMMENT_DSL_UNSUPPORTED; + $attrs['set_if_format'] = 'expression'; + } elseif (property_exists($value, 'set_if_format')) { + if (in_array($value->set_if_format, ['string', 'expression', 'json'])) { + $attrs['set_if_format'] = $value->set_if_format; + } + } + } + + return $attrs; + } + + public function setArguments($arguments) + { + $arguments = (array) $arguments; + + foreach ($arguments as $arg => $val) { + $this->set($arg, $val); + } + + foreach (array_diff( + array_keys($this->arguments), + array_keys($arguments) + ) as $arg) { + if ($this->arguments[$arg]->hasBeenLoadedFromDb()) { + $this->arguments[$arg]->markForRemoval(); + $this->modified = true; + } else { + unset($this->arguments[$arg]); + } + } + + return $this; + } + + /** + * Magic isset check + * + * @param string $argument + * @return boolean + */ + public function __isset($argument) + { + return array_key_exists($argument, $this->arguments); + } + + public function remove($argument) + { + if (array_key_exists($argument, $this->arguments)) { + $this->arguments[$argument]->markForRemoval(); + $this->modified = true; + $this->refreshIndex(); + } + + return $this; + } + + protected function refreshIndex() + { + ksort($this->arguments); + $this->idx = array_keys($this->arguments); + } + + public function add(IcingaCommandArgument $argument) + { + $name = $argument->get('argument_name'); + if (array_key_exists($name, $this->arguments)) { + // TODO: Fail unless $argument equals existing one + return $this; + } + + $this->arguments[$name] = $argument; + $this->modified = true; + $this->refreshIndex(); + + return $this; + } + + protected function getGroupTableName() + { + return $this->object->getTableName() . 'group'; + } + + protected function loadFromDb() + { + $db = $this->object->getDb(); + $connection = $this->object->getConnection(); + + $table = $this->object->getTableName(); + $query = $db->select()->from( + ['o' => $table], + [] + )->join( + ['a' => 'icinga_command_argument'], + 'o.id = a.command_id', + '*' + )->where('o.object_name = ?', $this->object->getObjectName()) + ->order('a.sort_order')->order('a.argument_name'); + + $this->arguments = IcingaCommandArgument::loadAll($connection, $query, 'argument_name'); + $this->cloneStored(); + $this->refreshIndex(); + + return $this; + } + + public function toPlainObject( + $resolved = false, + $skipDefaults = false, + array $chosenProperties = null, + $resolveIds = true + ) { + if ($chosenProperties !== null) { + throw new InvalidArgumentException( + 'IcingaArguments does not support chosenProperties[]' + ); + } + + $args = []; + foreach ($this->arguments as $arg) { + if ($arg->shouldBeRemoved()) { + continue; + } + + $args[$arg->get('argument_name')] = $arg->toPlainObject( + $resolved, + $skipDefaults, + null, + $resolveIds + ); + } + + return $args; + } + + public function toUnmodifiedPlainObject() + { + $args = []; + foreach ($this->storedArguments as $key => $arg) { + $args[$arg->argument_name] = $arg->toPlainObject(); + } + + return $args; + } + + protected function cloneStored() + { + $this->storedArguments = []; + foreach ($this->arguments as $k => $v) { + $this->storedArguments[$k] = clone($v); + } + } + + public static function loadForStoredObject(IcingaObject $object) + { + $arguments = new static($object); + return $arguments->loadFromDb(); + } + + public function setBeingLoadedFromDb() + { + foreach ($this->arguments as $argument) { + $argument->setBeingLoadedFromDb(); + } + $this->refreshIndex(); + $this->cloneStored(); + } + + /** + * @return $this + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function store() + { + $db = $this->object->getConnection(); + $deleted = []; + foreach ($this->arguments as $key => $argument) { + if ($argument->shouldBeRemoved()) { + $deleted[] = $key; + } else { + if ($argument->hasBeenModified()) { + if ($argument->hasBeenLoadedFromDb()) { + $argument->setLoadedProperty('command_id', $this->object->get('id')); + } else { + $argument->set('command_id', $this->object->get('id')); + } + $argument->store($db); + } + } + } + + foreach ($deleted as $key) { + $argument = $this->arguments[$key]; + $argument->setLoadedProperty('command_id', $this->object->get('id')); + $argument->setConnection($this->object->getConnection()); + $argument->delete(); + unset($this->arguments[$key]); + } + + $this->cloneStored(); + + return $this; + } + + public function toConfigString() + { + if (empty($this->arguments)) { + return ''; + } + + $args = []; + foreach ($this->arguments as $arg) { + if ($arg->shouldBeRemoved()) { + continue; + } + + $args[$arg->get('argument_name')] = $arg->toConfigString(); + } + return c::renderKeyOperatorValue('arguments', '+=', c::renderDictionary($args)); + } + + public function __toString() + { + try { + return $this->toConfigString(); + } catch (Exception $e) { + trigger_error($e); + $previousHandler = set_exception_handler( + function () { + } + ); + restore_error_handler(); + if ($previousHandler !== null) { + call_user_func($previousHandler, $e); + die(); + } else { + die($e->getMessage()); + } + } + } + + public function toLegacyConfigString() + { + return 'UNSUPPORTED'; + } +} diff --git a/library/Director/Objects/IcingaCommand.php b/library/Director/Objects/IcingaCommand.php new file mode 100644 index 0000000..35f38a4 --- /dev/null +++ b/library/Director/Objects/IcingaCommand.php @@ -0,0 +1,365 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; +use Icinga\Module\Director\Objects\Extension\Arguments; +use Zend_Db_Select as DbSelect; + +class IcingaCommand extends IcingaObject implements ObjectWithArguments, ExportInterface +{ + use Arguments; + + protected $table = 'icinga_command'; + + protected $type = 'CheckCommand'; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'methods_execute' => null, + 'command' => null, + 'timeout' => null, + 'zone_id' => null, + 'is_string' => null, + ]; + + protected $booleans = [ + 'is_string' => 'is_string', + ]; + + protected $supportsCustomVars = true; + + protected $supportsFields = true; + + protected $supportsImports = true; + + protected $supportedInLegacy = true; + + protected $intervalProperties = [ + 'timeout' => 'timeout', + ]; + + protected $relations = [ + 'zone' => 'IcingaZone', + ]; + + protected static $pluginDir; + + protected $hiddenExecuteTemplates = [ + 'PluginCheck' => 'plugin-check-command', + 'PluginNotification' => 'plugin-notification-command', + 'PluginEvent' => 'plugin-event-command', + + // Special, internal: + 'IcingaCheck' => 'icinga-check-command', + 'ClusterCheck' => 'cluster-check-command', + 'ClusterZoneCheck' => 'plugin-check-command', + 'IdoCheck' => 'ido-check-command', + 'RandomCheck' => 'random-check-command', + ]; + + /** + * Render the 'medhods_execute' property as 'execute' + * + * Execute is a reserved word in SQL, column name was prefixed + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + protected function renderMethods_execute() + { + // @codingStandardsIgnoreEnd + return ''; + } + + protected function renderObjectHeader() + { + if ($execute = $this->get('methods_execute')) { + $itlImport = sprintf( + ' import "%s"' . "\n", + $this->hiddenExecuteTemplates[$execute] + ); + } else { + $itlImport = ''; + } + + $execute = $this->getSingleResolvedProperty('methods_execute'); + if ($execute === 'PluginNotification') { + return $this->renderObjectHeaderWithType('NotificationCommand') . $itlImport; + } elseif ($execute === 'PluginEvent') { + return $this->renderObjectHeaderWithType('EventCommand') . $itlImport; + } else { + return parent::renderObjectHeader() . $itlImport; + } + } + + /** + * @param $type + * @return string + */ + protected function renderObjectHeaderWithType($type) + { + return sprintf( + "%s %s %s {\n", + $this->getObjectTypeName(), + $type, + c::renderString($this->getObjectName()) + ); + } + + public function mungeCommand($value) + { + if (is_array($value)) { + $value = implode(' ', $value); + } elseif (is_object($value)) { + // { type => Function } -> really?? + return null; + // return $value; + } + + if (self::$pluginDir !== null) { + if (($pos = strpos($value, self::$pluginDir)) === 0) { + $value = substr($value, strlen(self::$pluginDir) + 1); + } + } + + return $value; + } + + public function getNextSkippableKeyName() + { + $key = $this->makeSkipKey(); + $cnt = 1; + while (isset($this->arguments()->$key)) { + $cnt++; + $key = $this->makeSkipKey($cnt); + } + + return $key; + } + + protected function makeSkipKey($num = null) + { + if ($num === null) { + return '(no key)'; + } + + return sprintf('(no key.%d)', $num); + } + + protected function prefersGlobalZone() + { + return true; + } + + /** + * @return string + * @throws \Zend_Db_Select_Exception + */ + public function countDirectUses() + { + $db = $this->getDb(); + $id = (int) $this->get('id'); + + $qh = $db->select()->from( + array('h' => 'icinga_host'), + array('cnt' => 'COUNT(*)') + )->where('h.check_command_id = ?', $id) + ->orWhere('h.event_command_id = ?', $id); + $qs = $db->select()->from( + array('s' => 'icinga_service'), + array('cnt' => 'COUNT(*)') + )->where('s.check_command_id = ?', $id) + ->orWhere('s.event_command_id = ?', $id); + $qn = $db->select()->from( + array('n' => 'icinga_notification'), + array('cnt' => 'COUNT(*)') + )->where('n.command_id = ?', $id); + $query = $db->select()->union( + [$qh, $qs, $qn], + DbSelect::SQL_UNION_ALL + ); + + return $db->fetchOne($db->select()->from( + ['all_cnts' => $query], + ['cnt' => 'SUM(cnt)'] + )); + } + + /** + * @return bool + * @throws \Zend_Db_Select_Exception + */ + public function isInUse() + { + return $this->countDirectUses() > 0; + } + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + $props = (array) $this->toPlainObject(); + if (isset($props['arguments'])) { + foreach ($props['arguments'] as $key => $argument) { + if (property_exists($argument, 'command_id')) { + unset($props['arguments'][$key]->command_id); + } + } + } + $props['fields'] = $this->loadFieldReferences(); + ksort($props); + + return (object) $props; + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaCommand + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + $name = $properties['object_name']; + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Command "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + unset($properties['fields']); + $object->setProperties($properties); + + return $object; + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ + protected function loadFieldReferences() + { + $db = $this->getDb(); + + $res = $db->fetchAll( + $db->select()->from([ + 'cf' => 'icinga_command_field' + ], [ + 'cf.datafield_id', + 'cf.is_required', + 'cf.var_filter', + ])->join(['df' => 'director_datafield'], 'df.id = cf.datafield_id', []) + ->where('command_id = ?', $this->get('id')) + ->order('varname ASC') + ); + + if (empty($res)) { + return []; + } else { + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + + return $res; + } + } + + protected function renderCommand() + { + $command = $this->get('command'); + $prefix = ''; + if (preg_match('~^([A-Z][A-Za-z0-9_]+\s\+\s)(.+?)$~', $command, $m)) { + $prefix = $m[1]; + $command = $m[2]; + } elseif (! $this->isAbsolutePath($command)) { + $prefix = 'PluginDir + '; + $command = '/' . $command; + } + + $inherited = $this->getInheritedProperties(); + + if ($this->get('is_string') === 'y' || ($this->get('is_string') === null + && property_exists($inherited, 'is_string') && $inherited->is_string === 'y')) { + return c::renderKeyValue('command', $prefix . c::renderString($command)); + } else { + $parts = preg_split('/\s+/', $command, -1, PREG_SPLIT_NO_EMPTY); + array_unshift($parts, c::alreadyRendered($prefix . c::renderString(array_shift($parts)))); + + return c::renderKeyValue('command', c::renderArray($parts)); + } + } + + /** + * @codingStandardsIgnoreStart + */ + protected function renderIs_string() + { + // @codingStandardsIgnoreEnd + return ''; + } + + protected function isAbsolutePath($path) + { + return $path[0] === '/' + || $path[0] === '\\' + || preg_match('/^[A-Za-z]:\\\/', substr($path, 0, 3)) + || preg_match('/^%[A-Z][A-Za-z0-9\(\)-]*%/', $path); + } + + public static function setPluginDir($pluginDir) + { + self::$pluginDir = $pluginDir; + } + + public function getLegacyObjectType() + { + // there is only one type of command in Icinga 1.x + return 'command'; + } + + protected function renderLegacyCommand() + { + $command = $this->get('command'); + if (preg_match('~^(\$USER\d+\$/?)(.+)$~', $command)) { + // should be fine, since the user decided to use a macro + } elseif (! $this->isAbsolutePath($command)) { + $command = '$USER1$/'.$command; + } + + return c1::renderKeyValue( + $this->getLegacyObjectType().'_line', + c1::renderString($command) + ); + } +} diff --git a/library/Director/Objects/IcingaCommandArgument.php b/library/Director/Objects/IcingaCommandArgument.php new file mode 100644 index 0000000..96101ce --- /dev/null +++ b/library/Director/Objects/IcingaCommandArgument.php @@ -0,0 +1,263 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use RuntimeException; + +class IcingaCommandArgument extends IcingaObject +{ + protected $keyName = ['command_id', 'argument_name']; + + protected $autoincKeyName = 'id'; + + protected $table = 'icinga_command_argument'; + + protected $supportsImports = false; + + protected $booleans = array( + 'skip_key' => 'skip_key', + 'repeat_key' => 'repeat_key', + 'required' => 'required' + ); + + protected $defaultProperties = array( + 'id' => null, + 'command_id' => null, + 'argument_name' => null, + 'argument_value' => null, + 'argument_format' => null, + 'key_string' => null, + 'description' => null, + 'skip_key' => null, + 'set_if' => null, + 'sort_order' => null, + 'repeat_key' => null, + 'set_if_format' => null, + 'required' => null, + ); + + public function onInsert() + { + // No log right now, we have to handle "sub-objects" + } + + public function onUpdate() + { + // No log right now, we have to handle "sub-objects" + } + + public function onDelete() + { + // No log right now, we have to handle "sub-objects" + } + + public function isSkippingKey() + { + return $this->get('skip_key') === 'y' || $this->get('argument_name') === null; + } + + // Preserve is not supported + public function replaceWith(IcingaObject $object, $preserve = null) + { + $this->setProperties((array) $object->toPlainObject( + false, + false, + null, + false + )); + return $this; + } + + protected function makePlainArgumentValue($value, $format) + { + if ($format === 'expression') { + return (object) [ + 'type' => 'Function', + // TODO: Not for dummy comment + 'body' => $value + ]; + } else { + // json or string + return $value; + } + } + + protected function extractValueFromPlain($plain) + { + if ($plain->argument_value) { + return $this->makePlainArgumentValue( + $plain->argument_value, + $plain->argument_format + ); + } else { + return null; + } + } + + protected function transformPlainArgumentValue($plain) + { + if (property_exists($plain, 'argument_value')) { + if (property_exists($plain, 'argument_format')) { + $format = $plain->argument_format; + } else { + $format = 'string'; + } + $plain->value = $this->makePlainArgumentValue( + $plain->argument_value, + $format + ); + unset($plain->argument_value); + unset($plain->argument_format); + } + } + + public function toCompatPlainObject() + { + $plain = parent::toPlainObject( + false, + true, + null, + false + ); + + unset($plain->id); + unset($plain->argument_name); + if (! isset($plain->argument_value)) { + unset($plain->argument_format); + } + if (! isset($plain->set_if)) { + unset($plain->set_if_format); + } + + $this->transformPlainArgumentValue($plain); + unset($plain->command_id); + + // Will happen only combined with $skipDefaults + if (array_keys((array) $plain) === ['value']) { + return $plain->value; + } else { + if (property_exists($plain, 'sort_order') && $plain->sort_order !== null) { + $plain->order = $plain->sort_order; + unset($plain->sort_order); + } + + return $plain; + } + } + + public function toFullPlainObject($skipDefaults = false) + { + $plain = parent::toPlainObject( + false, + $skipDefaults, + null, + false + ); + + unset($plain->id); + + return $plain; + } + + public function toPlainObject( + $resolved = false, + $skipDefaults = false, + array $chosenProperties = null, + $resolveIds = true, + $keepId = false + ) { + if ($resolved) { + throw new RuntimeException( + 'A single CommandArgument cannot be resolved' + ); + } + + if ($chosenProperties) { + throw new RuntimeException( + 'IcingaCommandArgument does not support chosenProperties[]' + ); + } + + if ($keepId) { + throw new RuntimeException( + 'IcingaCommandArgument does not support $keepId' + ); + } + + // $resolveIds is misused here + if ($resolveIds) { + return $this->toCompatPlainObject(); + } else { + return $this->toFullPlainObject($skipDefaults); + } + } + + public function toConfigString() + { + $data = array(); + $value = $this->get('argument_value'); + if ($value) { + switch ($this->get('argument_format')) { + case 'string': + $data['value'] = c::renderString($value); + break; + case 'json': + if (is_object($value)) { + $data['value'] = c::renderDictionary($value); + } elseif (is_array($value)) { + $data['value'] = c::renderArray($value); + } elseif (is_null($value)) { + // TODO: recheck all this. I bet we never reach this: + $data['value'] = 'null'; + } elseif (is_bool($value)) { + $data['value'] = c::renderBoolean($value); + } else { + $data['value'] = $value; + } + break; + case 'expression': + $data['value'] = c::renderExpression($value); + break; + } + } + + if ($this->get('sort_order') !== null) { + $data['order'] = $this->get('sort_order'); + } + + if (null !== $this->get('set_if')) { + switch ($this->get('set_if_format')) { + case 'expression': + $data['set_if'] = c::renderExpression($this->get('set_if')); + break; + case 'string': + default: + $data['set_if'] = c::renderString($this->get('set_if')); + break; + } + } + + if (null !== $this->get('required')) { + $data['required'] = c::renderBoolean($this->get('required')); + } + + if ($this->isSkippingKey()) { + $data['skip_key'] = c::renderBoolean('y'); + } + + if (null !== $this->get('repeat_key')) { + $data['repeat_key'] = c::renderBoolean($this->get('repeat_key')); + } + + if (null !== $this->get('description')) { + $data['description'] = c::renderString($this->get('description')); + } + + if (array_keys($data) === ['value']) { + return $data['value']; + } else { + return c::renderDictionary($data); + } + } +} diff --git a/library/Director/Objects/IcingaCommandField.php b/library/Director/Objects/IcingaCommandField.php new file mode 100644 index 0000000..086cb56 --- /dev/null +++ b/library/Director/Objects/IcingaCommandField.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaCommandField extends IcingaObjectField +{ + protected $keyName = array('command_id', 'datafield_id'); + + protected $table = 'icinga_command_field'; + + protected $defaultProperties = array( + 'command_id' => null, + 'datafield_id' => null, + 'is_required' => null, + 'var_filter' => null, + ); +} diff --git a/library/Director/Objects/IcingaDependency.php b/library/Director/Objects/IcingaDependency.php new file mode 100644 index 0000000..c9d9b89 --- /dev/null +++ b/library/Director/Objects/IcingaDependency.php @@ -0,0 +1,631 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Exception\NotFoundError; +use Icinga\Data\Filter\Filter; + +class IcingaDependency extends IcingaObject implements ExportInterface +{ + protected $table = 'icinga_dependency'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'apply_to' => null, + 'parent_host_id' => null, + 'parent_host_var' => null, + 'parent_service_id' => null, + 'child_host_id' => null, + 'child_service_id' => null, + 'disable_checks' => null, + 'disable_notifications' => null, + 'ignore_soft_states' => null, + 'period_id' => null, + 'zone_id' => null, + 'assign_filter' => null, + 'parent_service_by_name' => null, + ]; + + protected $uuidColumn = 'uuid'; + + protected $supportsCustomVars = false; + + protected $supportsImports = true; + + protected $supportsApplyRules = true; + + /** + * @internal + * @var bool + */ + protected $renderApplyForArray = false; + + protected $relatedSets = [ + 'states' => 'StateFilterSet', + ]; + + protected $relations = [ + 'zone' => 'IcingaZone', + 'parent_host' => 'IcingaHost', + 'parent_service' => 'IcingaService', + 'child_host' => 'IcingaHost', + 'child_service' => 'IcingaService', + 'period' => 'IcingaTimePeriod', + ]; + + protected $booleans = [ + 'disable_checks' => 'disable_checks', + 'disable_notifications' => 'disable_notifications', + 'ignore_soft_states' => 'ignore_soft_states' + ]; + + protected $propertiesNotForRendering = [ + 'id', + 'object_name', + 'object_type', + 'apply_to', + ]; + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + $props = (array) $this->toPlainObject(); + ksort($props); + + return (object) $props; + } + + /** + * @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['object_name']; + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Dependency "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + $object->setProperties($properties); + + return $object; + } + + public function parentHostIsVar() + { + return $this->get('parent_host_var') !== null; + } + + /** + * @return string + * @throws ConfigurationError + */ + protected function renderObjectHeader() + { + if ($this->isApplyRule()) { + if (($to = $this->get('apply_to')) === null) { + throw new ConfigurationError( + 'Applied dependency "%s" has no valid object type', + $this->getObjectName() + ); + } + + if ($this->renderApplyForArray) { + return $this->renderArrayObjectHeader($to); + } + + return $this->renderSingleObjectHeader($to); + } + + return parent::renderObjectHeader(); + } + + protected function renderSingleObjectHeader($to) + { + return sprintf( + "%s %s %s to %s {\n", + $this->getObjectTypeName(), + $this->getType(), + c::renderString($this->getObjectName()), + ucfirst($to) + ); + } + + protected function renderArrayObjectHeader($to) + { + return sprintf( + "%s %s %s for (host_parent_name in %s) to %s {\n", + $this->getObjectTypeName(), + $this->getType(), + c::renderString($this->getObjectName()), + $this->get('parent_host_var'), + ucfirst($to) + ); + } + + /** + * @return string + */ + protected function renderSuffix() + { + if (! $this->parentHostIsVar()) { + return parent::renderSuffix(); + } + + if ((string) $this->get('assign_filter') !== '') { + $suffix = parent::renderSuffix(); + } else { + $suffix = ' assign where ' . $this->renderAssignFilterExtension('') + . "\n" . parent::renderSuffix(); + } + + if ($this->renderApplyForArray) { + return $suffix; + } + + return $suffix . $this->renderApplyForArrayClone(); + } + + protected function renderApplyForArrayClone() + { + $clone = clone($this); + $clone->renderApplyForArray = true; + + return $clone->toConfigString(); + } + + public function isApplyForArrayClone() + { + return $this->renderApplyForArray; + } + + /** + * @codingStandardsIgnoreStart + */ + public function renderAssign_Filter() + { + if ($this->parentHostIsVar()) { + return preg_replace( + '/\n$/m', + $this->renderAssignFilterExtension() . "\n", + parent::renderAssign_Filter() + ); + } + + return parent::renderAssign_Filter(); + } + + protected function renderAssignFilterExtension($pre = ' && ') + { + $varName = $this->get('parent_host_var'); + if ($this->renderApplyForArray) { + return sprintf('%stypeof(%s) == Array', $pre, $varName); + } + + return sprintf('%stypeof(%s) == String', $pre, $varName); + } + + protected function setKey($key) + { + // TODO: Check if this method can be removed + if (is_int($key)) { + $this->id = $key; + } elseif (is_array($key)) { + $keys = [ + 'id', + 'parent_host_id', + 'parent_service_id', + 'child_host_id', + 'child_service_id', + 'object_name' + ]; + + foreach ($keys as $k) { + if (array_key_exists($k, $key)) { + $this->set($k, $key[$k]); + } + } + } else { + return parent::setKey($key); + } + + return $this; + } + + protected function renderAssignments() + { + // TODO: this will never be reached + if ($this->hasBeenAssignedToServiceApply()) { + /** @var IcingaService $tmpService */ + $tmpService = $this->getRelatedObject( + 'child_service', + $this->get('child_service_id') + ); + // TODO: fix this, will crash: + $assigns = $tmpService->assignments()->toConfigString(); + + $filter = sprintf( + '%s && service.name == "%s"', + trim($assigns), + $this->get('child_service') + ); + return "\n " . $filter . "\n"; + } + + if ($this->hasBeenAssignedToHostTemplateService()) { + $filter = sprintf( + 'assign where "%s" in host.templates && service.name == "%s"', + $this->get('child_host'), + $this->get('child_service') + ); + return "\n " . $filter . "\n"; + } + if ($this->hasBeenAssignedToHostTemplate()) { + $filter = sprintf( + 'assign where "%s" in host.templates', + $this->get('child_host') + ); + return "\n " . $filter . "\n"; + } + + if ($this->hasBeenAssignedToServiceTemplate()) { + $filter = sprintf( + 'assign where "%s" in service.templates', + $this->get('child_service') + ); + return "\n " . $filter . "\n"; + } + + return parent::renderAssignments(); + } + + protected function hasBeenAssignedToHostTemplate() + { + try { + $id = $this->get('child_host_id'); + return $id && $this->getRelatedObject( + 'child_host', + $id + )->isTemplate(); + } catch (NotFoundError $e) { + return false; + } + } + + protected function hasBeenAssignedToServiceTemplate() + { + try { + $id = $this->get('child_service_id'); + return $id && $this->getRelatedObject( + 'child_service', + $id + )->isTemplate(); + } catch (NotFoundError $e) { + return false; + } + } + + protected function hasBeenAssignedToHostTemplateService() + { + if (!$this->hasBeenAssignedToHostTemplate()) { + return false; + } + try { + $id = $this->get('child_service_id'); + return $id && $this->getRelatedObject( + 'child_service', + $id + )->isObject(); + } catch (NotFoundError $e) { + return false; + } + } + + protected function hasBeenAssignedToServiceApply() + { + try { + $id = $this->get('child_service_id'); + return $id && $this->getRelatedObject( + 'child_service', + $id + )->isApplyRule(); + } catch (NotFoundError $e) { + return false; + } + } + + /** + * Render child_host_id as host_name + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderChild_host_id() + { + // @codingStandardsIgnoreEnd + + if ($this->hasBeenAssignedToHostTemplate()) { + return ''; + } + + return $this->renderRelationProperty( + 'child_host', + $this->get('child_host_id'), + 'child_host_name' + ); + } + + /** + * Render parent_host_id as parent_host_name + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderParent_host_id() + { + // @codingStandardsIgnoreEnd + return $this->renderRelationProperty( + 'parent_host', + $this->get('parent_host_id'), + 'parent_host_name' + ); + } + + /** + * Render parent_host_var as parent_host + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderParent_host_var() + { + // @codingStandardsIgnoreEnd + if ($this->renderApplyForArray) { + return c::renderKeyValue( + 'parent_host_name', + 'host_parent_name' + ); + } + + // @codingStandardsIgnoreEnd + return c::renderKeyValue( + 'parent_host_name', + $this->get('parent_host_var') + ); + } + + /** + * Render child_service_id as host_name + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderChild_service_id() + { + // @codingStandardsIgnoreEnd + if ($this->hasBeenAssignedToServiceTemplate() + || $this->hasBeenAssignedToHostTemplateService() + || $this->hasBeenAssignedToServiceApply() + ) { + return ''; + } + + return $this->renderRelationProperty( + 'child_service', + $this->get('child_service_id'), + 'child_service_name' + ); + } + + /** + * Render parent_service_id as parent_service_name + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderParent_service_id() + { + return $this->renderRelationProperty( + 'parent_service', + $this->get('parent_service_id'), + 'parent_service_name' + ); + } + + // + /** + * Render parent_service_by_name as parent_service_name + * + * Special case for parent service set as plain string for Apply rules + * + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderParent_service_by_name() + { + // @codingStandardsIgnoreEnd + return c::renderKeyValue( + 'parent_service_name', + c::renderString($this->get('parent_service_by_name')) + ); + } + + public function isApplyRule() + { + if ($this->hasBeenAssignedToHostTemplate() + || $this->hasBeenAssignedToServiceTemplate() + || $this->hasBeenAssignedToServiceApply() + ) { + return true; + } + + return parent::isApplyRule(); + } + + protected function resolveUnresolvedRelatedProperty($name) + { + $short = substr($name, 0, -3); + /** @var IcingaObject $class */ + $class = $this->getRelationClass($short); + $objKey = $this->unresolvedRelatedProperties[$name]; + + # related services need array key + if ($class === IcingaService::class) { + if ($name === 'parent_service_id' && $this->get('object_type') === 'apply') { + //special case , parent service can be set as simple string for Apply + if ($this->properties['parent_host_id'] === null) { + $this->reallySet( + 'parent_service_by_name', + $this->unresolvedRelatedProperties[$name] + ); + $this->reallySet('parent_service_id', null); + unset($this->unresolvedRelatedProperties[$name]); + return; + } + } + + $this->reallySet('parent_service_by_name', null); + $hostIdProperty = str_replace('service', 'host', $name); + if (isset($this->properties[$hostIdProperty])) { + $objKey = [ + 'host_id' => $this->properties[$hostIdProperty], + 'object_name' => $this->unresolvedRelatedProperties[$name] + ]; + } else { + $objKey = [ + 'host_id' => null, + 'object_name' => $this->unresolvedRelatedProperties[$name] + ]; + } + + try { + $class::load($objKey, $this->connection); + } catch (NotFoundError $e) { + // Not a simple service on host + // Hunt through inherited services, use service assigned to + // template if found + $tmpHost = IcingaHost::loadWithAutoIncId( + $this->properties[$hostIdProperty], + $this->connection + ); + + // services for applicable templates + $resolver = $tmpHost->templateResolver(); + foreach ($resolver->fetchResolvedParents() as $template_obj) { + $objKey = [ + 'host_id' => $template_obj->id, + 'object_name' => $this->unresolvedRelatedProperties[$name] + ]; + try { + $object = $class::load($objKey, $this->connection); + } catch (NotFoundError $e) { + continue; + } + break; + } + + if (!isset($object)) { + // Not an inherited service, now try apply rules + $matcher = HostApplyMatches::prepare($tmpHost); + foreach ($this->getAllApplyRules() as $rule) { + if ($matcher->matchesFilter($rule->filter)) { + if ($rule->name === $this->unresolvedRelatedProperties[$name]) { + $object = IcingaService::loadWithAutoIncId( + $rule->id, + $this->connection + ); + break; + } + } + } + } + } + } else { + $object = $class::load($objKey, $this->connection); + } + + if (isset($object)) { + $this->reallySet($name, $object->get('id')); + unset($this->unresolvedRelatedProperties[$name]); + } else { + throw new NotFoundError('Unable to resolve related property: "%s"', $name); + } + } + + protected function getAllApplyRules() + { + $allApplyRules = $this->fetchAllApplyRules(); + foreach ($allApplyRules as $rule) { + $rule->filter = Filter::fromQueryString($rule->assign_filter); + } + + return $allApplyRules; + } + + protected function fetchAllApplyRules() + { + $db = $this->connection->getDbAdapter(); + $query = $db->select()->from(['s' => 'icinga_service'], [ + 'id' => 's.id', + 'name' => 's.object_name', + 'assign_filter' => 's.assign_filter', + ])->where('object_type = ? AND assign_filter IS NOT NULL', 'apply'); + + return $db->fetchAll($query); + } + + protected function getRelatedProperty($key) + { + $related = parent::getRelatedProperty($key); + // handle special case for plain string parent service on Dependency + // Apply rules + if ($related === null && $key === 'parent_service' + && null !== $this->get('parent_service_by_name') + ) { + return $this->get('parent_service_by_name'); + } + + return $related; + } +} diff --git a/library/Director/Objects/IcingaEndpoint.php b/library/Director/Objects/IcingaEndpoint.php new file mode 100644 index 0000000..030183b --- /dev/null +++ b/library/Director/Objects/IcingaEndpoint.php @@ -0,0 +1,157 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Core\CoreApi; +use Icinga\Module\Director\Core\LegacyDeploymentApi; +use Icinga\Module\Director\Core\RestApiClient; +use Icinga\Module\Director\Exception\NestingError; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use InvalidArgumentException; +use RuntimeException; + +class IcingaEndpoint extends IcingaObject +{ + protected $table = 'icinga_endpoint'; + + protected $supportsImports = true; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'zone_id' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'host' => null, + 'port' => null, + 'log_duration' => null, + 'apiuser_id' => null, + ]; + + protected $relations = [ + 'zone' => 'IcingaZone', + 'apiuser' => 'IcingaApiUser', + ]; + + public function hasApiUser() + { + return $this->getResolvedProperty('apiuser_id') !== null; + } + + public function getApiUser() + { + $id = $this->getResolvedProperty('apiuser_id'); + if ($id === null) { + throw new RuntimeException('Trying to get API User for Endpoint without such: ' . $this->getObjectName()); + } + + return $this->getRelatedObject('apiuser', $id); + } + + /** + * Return a core API, depending on the configuration format + * + * @return CoreApi|LegacyDeploymentApi + */ + public function api() + { + $format = $this->connection->settings()->config_format; + if ($format === 'v2') { + $api = new CoreApi($this->getRestApiClient()); + $api->setDb($this->getConnection()); + + return $api; + } elseif ($format === 'v1') { + return new LegacyDeploymentApi($this->connection); + } else { + throw new InvalidArgumentException("Unsupported config format: $format"); + } + } + + /** + * @return RestApiClient + */ + public function getRestApiClient() + { + $client = new RestApiClient( + $this->getResolvedProperty('host', $this->getObjectName()), + $this->getResolvedProperty('port') + ); + + $user = $this->getApiUser(); + $client->setCredentials( + // TODO: $user->client_dn, + $user->object_name, + $user->password + ); + + return $client; + } + + public function getRenderingZone(IcingaConfig $config = null) + { + try { + if ($zone = $this->getResolvedRelated('zone')) { + return $zone->getRenderingZone($config); + } + } catch (NestingError $e) { + return self::RESOLVE_ERROR; + } + + return parent::getRenderingZone($config); + } + + /** + * @return int + */ + public function getResolvedPort() + { + $port = $this->getSingleResolvedProperty('port'); + if (null === $port) { + return 5665; + } else { + return (int) $port; + } + } + + public function getDescriptiveUrl() + { + return sprintf( + 'https://%s@%s:%d/v1/', + $this->getApiUser()->getObjectName(), + $this->getResolvedProperty('host', $this->getObjectName()), + $this->getResolvedPort() + ); + } + + /** + * Use duration time renderer helper + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + protected function renderLog_duration() + { + // @codingStandardsIgnoreEnd + return $this->renderPropertyAsSeconds('log_duration'); + } + + /** + * Internal property, will not be rendered + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + protected function renderApiuser_id() + { + // @codingStandardsIgnoreEnd + return ''; + } +} diff --git a/library/Director/Objects/IcingaFlatVar.php b/library/Director/Objects/IcingaFlatVar.php new file mode 100644 index 0000000..3bbf81c --- /dev/null +++ b/library/Director/Objects/IcingaFlatVar.php @@ -0,0 +1,61 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\CustomVariable\CustomVariable; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; + +class IcingaFlatVar extends DbObject +{ + protected $table = 'icinga_flat_var'; + + protected $keyName = [ + 'var_checksum', + 'flatname_checksum' + ]; + + protected $defaultProperties = [ + 'var_checksum' => null, + 'flatname_checksum' => null, + 'flatname' => null, + 'flatvalue' => null, + ]; + + protected $binaryProperties = [ + 'var_checksum', + 'flatname_checksum', + ]; + + public static function generateForCustomVar(CustomVariable $var, Db $db) + { + $flatVars = static::forCustomVar($var, $db); + foreach ($flatVars as $flat) { + $flat->store(); + } + + return $flatVars; + } + + public static function forCustomVar(CustomVariable $var, Db $db) + { + $flat = []; + $varSum = $var->checksum(); + $var->flatten($flat, $var->getKey()); + $flatVars = []; + + foreach ($flat as $name => $value) { + $flatVar = static::create([ + 'var_checksum' => $varSum, + 'flatname_checksum' => sha1($name, true), + 'flatname' => $name, + 'flatvalue' => $value, + ], $db); + + $flatVar->store(); + $flatVars[] = $flatVar; + } + + return $flatVars; + } +} diff --git a/library/Director/Objects/IcingaHost.php b/library/Director/Objects/IcingaHost.php new file mode 100644 index 0000000..2731f4a --- /dev/null +++ b/library/Director/Objects/IcingaHost.php @@ -0,0 +1,668 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Data\Db\DbConnection; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Data\PropertiesFilter; +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\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; +use Icinga\Module\Director\Objects\Extension\FlappingSupport; +use InvalidArgumentException; +use RuntimeException; + +class IcingaHost extends IcingaObject implements ExportInterface +{ + use FlappingSupport; + + protected $table = 'icinga_host'; + + protected $defaultProperties = array( + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'display_name' => null, + 'address' => null, + 'address6' => null, + 'check_command_id' => null, + 'max_check_attempts' => null, + 'check_period_id' => null, + 'check_interval' => null, + 'retry_interval' => null, + 'check_timeout' => null, + 'enable_notifications' => null, + 'enable_active_checks' => null, + 'enable_passive_checks' => null, + 'enable_event_handler' => null, + 'enable_flapping' => null, + 'enable_perfdata' => null, + 'event_command_id' => null, + 'flapping_threshold_high' => null, + 'flapping_threshold_low' => null, + 'volatile' => null, + 'zone_id' => null, + 'command_endpoint_id' => null, + 'notes' => null, + 'notes_url' => null, + 'action_url' => null, + 'icon_image' => null, + 'icon_image_alt' => null, + 'has_agent' => null, + 'master_should_connect' => null, + 'accept_config' => null, + 'custom_endpoint_name' => null, + 'api_key' => null, + 'template_choice_id' => null, + ); + + protected $relations = array( + 'check_command' => 'IcingaCommand', + 'event_command' => 'IcingaCommand', + 'check_period' => 'IcingaTimePeriod', + 'command_endpoint' => 'IcingaEndpoint', + 'zone' => 'IcingaZone', + 'template_choice' => 'IcingaTemplateChoiceHost', + ); + + protected $booleans = array( + 'enable_notifications' => 'enable_notifications', + 'enable_active_checks' => 'enable_active_checks', + 'enable_passive_checks' => 'enable_passive_checks', + 'enable_event_handler' => 'enable_event_handler', + 'enable_flapping' => 'enable_flapping', + 'enable_perfdata' => 'enable_perfdata', + 'volatile' => 'volatile', + 'has_agent' => 'has_agent', + 'master_should_connect' => 'master_should_connect', + 'accept_config' => 'accept_config', + ); + + protected $intervalProperties = array( + 'check_interval' => 'check_interval', + 'check_timeout' => 'check_timeout', + 'retry_interval' => 'retry_interval', + ); + + protected $supportsCustomVars = true; + + protected $supportsGroups = true; + + protected $supportsImports = true; + + protected $supportsFields = true; + + protected $supportsChoices = true; + + protected $supportedInLegacy = true; + + /** @var HostGroupMembershipResolver */ + protected $hostgroupMembershipResolver; + + protected $uuidColumn = 'uuid'; + + public static function enumProperties( + DbConnection $connection = null, + $prefix = '', + $filter = null + ) { + $hostProperties = array(); + if ($filter === null) { + $filter = new PropertiesFilter(); + } + $realProperties = array_merge(['templates'], static::create()->listProperties()); + sort($realProperties); + + if ($filter->match(PropertiesFilter::$HOST_PROPERTY, 'name')) { + $hostProperties[$prefix . 'name'] = 'name'; + } + foreach ($realProperties as $prop) { + if (!$filter->match(PropertiesFilter::$HOST_PROPERTY, $prop)) { + continue; + } + + if (substr($prop, -3) === '_id') { + if ($prop === 'template_choice_id') { + continue; + } + $prop = substr($prop, 0, -3); + } + + $hostProperties[$prefix . $prop] = $prop; + } + unset($hostProperties[$prefix . 'uuid']); + unset($hostProperties[$prefix . 'custom_endpoint_name']); + + $hostVars = array(); + + if ($connection instanceof Db) { + foreach ($connection->fetchDistinctHostVars() as $var) { + if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) { + if ($var->datatype) { + $hostVars[$prefix . 'vars.' . $var->varname] = sprintf( + '%s (%s)', + $var->varname, + $var->caption + ); + } else { + $hostVars[$prefix . 'vars.' . $var->varname] = $var->varname; + } + } + } + } + + //$properties['vars.*'] = 'Other custom variable'; + ksort($hostVars); + + + $props = mt('director', 'Host properties'); + $vars = mt('director', 'Custom variables'); + + $properties = array(); + if (!empty($hostProperties)) { + $properties[$props] = $hostProperties; + $properties[$props][$prefix . 'groups'] = 'Groups'; + } + + if (!empty($hostVars)) { + $properties[$vars] = $hostVars; + } + + return $properties; + } + + public function getCheckCommand() + { + $id = $this->getSingleResolvedProperty('check_command_id'); + return IcingaCommand::loadWithAutoIncId( + $id, + $this->getConnection() + ); + } + + public function hasCheckCommand() + { + return $this->getSingleResolvedProperty('check_command_id') !== null; + } + + public function renderToConfig(IcingaConfig $config) + { + parent::renderToConfig($config); + + // TODO: We might alternatively let the whole config fail in case we have + // used use_agent together with a legacy config + if (! $config->isLegacy()) { + $this->renderAgentZoneAndEndpoint($config); + } + } + + public function renderAgentZoneAndEndpoint(IcingaConfig $config = null) + { + if (!$this->isObject()) { + return; + } + + if ($this->isDisabled()) { + return; + } + + if ($this->getRenderingZone($config) === self::RESOLVE_ERROR) { + return; + } + + if ($this->getSingleResolvedProperty('has_agent') !== 'y') { + return; + } + + $name = $this->getEndpointName(); + + if (IcingaEndpoint::exists($name, $this->connection)) { + return; + } + + $props = array( + 'object_name' => $name, + 'object_type' => 'object', + 'log_duration' => 0 + ); + + if ($this->getSingleResolvedProperty('master_should_connect') === 'y') { + $props['host'] = $this->getSingleResolvedProperty('address'); + } + + $props['zone_id'] = $this->getSingleResolvedProperty('zone_id'); + + $endpoint = IcingaEndpoint::create($props, $this->connection); + + $zone = IcingaZone::create(array( + 'object_name' => $name, + ), $this->connection)->setEndpointList(array($name)); + + if ($props['zone_id']) { + $zone->parent_id = $props['zone_id']; + } else { + $zone->parent = $this->connection->getMasterZoneName(); + } + + $pre = 'zones.d/' . $this->getRenderingZone($config) . '/'; + $config->configFile($pre . 'agent_endpoints')->addObject($endpoint); + $config->configFile($pre . 'agent_zones')->addObject($zone); + } + + /** + // @codingStandardsIgnoreStart + * @param $value + * @return string + */ + protected function renderCustom_endpoint_name($value) + { + // @codingStandardsIgnoreEnd + // When feature flag feature_custom_endpoint is enabled, render custom var + if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') { + return c::renderKeyValue('vars._director_custom_endpoint_name', c::renderString($value)); + } + + return ''; + } + + /** + * Returns the hostname or custom endpoint name of the Icinga agent + * + * @return string + */ + public function getEndpointName() + { + $name = $this->getObjectName(); + + if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') { + if (($customName = $this->get('custom_endpoint_name')) !== null) { + $name = $customName; + } + } + + return $name; + } + + public function getAgentListenPort() + { + $conn = $this->connection; + $name = $this->getObjectName(); + if (IcingaEndpoint::exists($name, $conn)) { + return IcingaEndpoint::load($name, $conn)->getResolvedPort(); + } else { + return 5665; + } + } + + public function getUniqueIdentifier() + { + if ($this->isTemplate()) { + return $this->getObjectName(); + } else { + throw new RuntimeException( + 'getUniqueIdentifier() is supported by Host Templates only' + ); + } + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + // TODO: ksort in toPlainObject? + $props = (array) $this->toPlainObject(); + $props['fields'] = $this->loadFieldReferences(); + ksort($props); + + return (object) $props; + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaHost + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + $name = $properties['object_name']; + if ($properties['object_type'] !== 'template') { + throw new InvalidArgumentException(sprintf( + 'Can import only Templates, got "%s" for "%s"', + $properties['object_type'], + $name + )); + } + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Service Template "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + // $object->newFields = $properties['fields']; + unset($properties['fields']); + $object->setProperties($properties); + + return $object; + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ + protected function loadFieldReferences() + { + $db = $this->getDb(); + + $res = $db->fetchAll( + $db->select()->from([ + 'hf' => 'icinga_host_field' + ], [ + 'hf.datafield_id', + 'hf.is_required', + 'hf.var_filter', + ])->join(['df' => 'director_datafield'], 'df.id = hf.datafield_id', []) + ->where('host_id = ?', $this->get('id')) + ->order('varname ASC') + ); + + if (empty($res)) { + return []; + } else { + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + return $res; + } + } + + public function beforeDelete() + { + foreach ($this->fetchServices() as $service) { + $service->delete(); + } + foreach ($this->fetchServiceSets() as $set) { + $set->delete(); + } + + parent::beforeDelete(); + } + + public function hasAnyOverridenServiceVars() + { + $varname = $this->getServiceOverrivesVarname(); + return isset($this->vars()->$varname); + } + + public function getAllOverriddenServiceVars() + { + if ($this->hasAnyOverridenServiceVars()) { + $varname = $this->getServiceOverrivesVarname(); + return $this->vars()->$varname->getValue(); + } else { + return (object) array(); + } + } + + public function hasOverriddenServiceVars($service) + { + $all = $this->getAllOverriddenServiceVars(); + return property_exists($all, $service); + } + + public function getOverriddenServiceVars($service) + { + if ($this->hasOverriddenServiceVars($service)) { + $all = $this->getAllOverriddenServiceVars(); + return $all->$service; + } else { + return (object) array(); + } + } + + public function overrideServiceVars($service, $vars) + { + // For PHP < 5.5.0: + $array = (array) $vars; + if (empty($array)) { + return $this->unsetOverriddenServiceVars($service); + } + + $all = $this->getAllOverriddenServiceVars(); + $all->$service = $vars; + $varname = $this->getServiceOverrivesVarname(); + $this->vars()->$varname = $all; + + return $this; + } + + public function unsetOverriddenServiceVars($service) + { + if ($this->hasOverriddenServiceVars($service)) { + $all = (array) $this->getAllOverriddenServiceVars(); + unset($all[$service]); + + $varname = $this->getServiceOverrivesVarname(); + if (empty($all)) { + unset($this->vars()->$varname); + } else { + $this->vars()->$varname = (object) $all; + } + } + + return $this; + } + + protected function notifyResolvers() + { + $resolver = $this->getHostGroupMembershipResolver(); + $resolver->addObject($this); + $resolver->refreshDb(); + + return $this; + } + + protected function getHostGroupMembershipResolver() + { + if ($this->hostgroupMembershipResolver === null) { + $this->hostgroupMembershipResolver = new HostGroupMembershipResolver( + $this->getConnection() + ); + } + + return $this->hostgroupMembershipResolver; + } + + public function setHostGroupMembershipResolver(HostGroupMembershipResolver $resolver) + { + $this->hostgroupMembershipResolver = $resolver; + return $this; + } + + protected function getServiceOverrivesVarname() + { + return $this->connection->settings()->override_services_varname; + } + + /** + * Internal property, will not be rendered + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + protected function renderHas_Agent() + { + return ''; + } + + /** + * Internal property, will not be rendered + * + * @return string + */ + protected function renderMaster_should_connect() + { + return ''; + } + + /** + * Internal property, will not be rendered + * + * @return string + */ + protected function renderApi_key() + { + return ''; + } + + /** + * Internal property, will not be rendered + * + * @return string + */ + protected function renderTemplate_choice_id() + { + return ''; + } + + /** + * Internal property, will not be rendered + * + * @return string + */ + protected function renderAccept_config() + { + // @codingStandardsIgnoreEnd + return ''; + } + + /** + * @codingStandardsIgnoreStart + * + * @return string + */ + protected function renderLegacyDisplay_Name() + { + // @codingStandardsIgnoreEnd + return c1::renderKeyValue('display_name', $this->display_name); + } + + protected function renderLegacyVolatile() + { + // not available for hosts in Icinga 1.x + return; + } + + protected function renderLegacyCustomExtensions() + { + $str = parent::renderLegacyCustomExtensions(); + + if (($alias = $this->vars()->get('alias')) !== null) { + $str .= c1::renderKeyValue('alias', $alias->getValue()); + } + + return $str; + } + + /** + * @return IcingaService[] + */ + public function fetchServices() + { + $connection = $this->getConnection(); + $db = $connection->getDbAdapter(); + + /** @var IcingaService[] $services */ + $services = IcingaService::loadAll( + $connection, + $db->select()->from('icinga_service') + ->where('host_id = ?', $this->get('id')) + ); + + return $services; + } + + /** + * @return IcingaServiceSet[] + */ + public function fetchServiceSets() + { + $connection = $this->getConnection(); + $db = $connection->getDbAdapter(); + + /** @var IcingaServiceSet[] $sets */ + $sets = IcingaServiceSet::loadAll( + $connection, + $db->select()->from('icinga_service_set') + ->where('host_id = ?', $this->get('id')) + ); + + return $sets; + } + + /** + * @return string + */ + public function generateApiKey() + { + $key = sha1( + (string) microtime(false) + . $this->getObjectName() + . rand(1, 1000000) + ); + + if ($this->dbHasApiKey($key)) { + $key = $this->generateApiKey(); + } + + $this->set('api_key', $key); + + return $key; + } + + protected function dbHasApiKey($key) + { + $db = $this->getDb(); + $query = $db->select()->from( + ['o' => $this->getTableName()], + 'o.api_key' + )->where('api_key = ?', $key); + + return $db->fetchOne($query) === $key; + } + + public static function loadWithApiKey($key, Db $db) + { + $query = $db->getDbAdapter() + ->select() + ->from('icinga_host') + ->where('api_key = ?', $key); + + $result = self::loadAll($db, $query); + if (count($result) !== 1) { + throw new NotFoundError('Got invalid API key "%s"', $key); + } + + return current($result); + } +} diff --git a/library/Director/Objects/IcingaHostField.php b/library/Director/Objects/IcingaHostField.php new file mode 100644 index 0000000..b68c9d4 --- /dev/null +++ b/library/Director/Objects/IcingaHostField.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaHostField extends IcingaObjectField +{ + protected $keyName = array('host_id', 'datafield_id'); + + protected $table = 'icinga_host_field'; + + protected $defaultProperties = array( + 'host_id' => null, + 'datafield_id' => null, + 'is_required' => null, + 'var_filter' => null, + ); +} diff --git a/library/Director/Objects/IcingaHostGroup.php b/library/Director/Objects/IcingaHostGroup.php new file mode 100644 index 0000000..e11f672 --- /dev/null +++ b/library/Director/Objects/IcingaHostGroup.php @@ -0,0 +1,42 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaHostGroup extends IcingaObjectGroup +{ + protected $table = 'icinga_hostgroup'; + + /** @var HostGroupMembershipResolver */ + protected $hostgroupMembershipResolver; + + public function supportsAssignments() + { + return true; + } + + protected function getHostGroupMembershipResolver() + { + if ($this->hostgroupMembershipResolver === null) { + $this->hostgroupMembershipResolver = new HostGroupMembershipResolver( + $this->getConnection() + ); + } + + return $this->hostgroupMembershipResolver; + } + + public function setHostGroupMembershipResolver(HostGroupMembershipResolver $resolver) + { + $this->hostgroupMembershipResolver = $resolver; + return $this; + } + + protected function notifyResolvers() + { + $resolver = $this->getHostGroupMembershipResolver(); + $resolver->addGroup($this); + $resolver->refreshDb(); + + return $this; + } +} diff --git a/library/Director/Objects/IcingaHostGroupAssignment.php b/library/Director/Objects/IcingaHostGroupAssignment.php new file mode 100644 index 0000000..4e0e5a2 --- /dev/null +++ b/library/Director/Objects/IcingaHostGroupAssignment.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaHostGroupAssignment extends IcingaObject +{ + protected $table = 'icinga_hostgroup_assignment'; + + protected $keyName = 'id'; + + protected $defaultProperties = array( + 'id' => null, + 'service_id' => null, + 'filter_string' => null, + ); + + protected $relations = array( + 'service' => 'IcingaHostGroup', + ); +} diff --git a/library/Director/Objects/IcingaHostVar.php b/library/Director/Objects/IcingaHostVar.php new file mode 100644 index 0000000..45656d5 --- /dev/null +++ b/library/Director/Objects/IcingaHostVar.php @@ -0,0 +1,29 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaHostVar extends IcingaObject +{ + protected $keyName = array('host_id', 'varname'); + + protected $table = 'icinga_host_var'; + + protected $defaultProperties = array( + 'host_id' => null, + 'varname' => null, + 'varvalue' => null, + 'format' => null, + ); + + public function onInsert() + { + } + + public function onUpdate() + { + } + + public function onDelete() + { + } +} diff --git a/library/Director/Objects/IcingaNotification.php b/library/Director/Objects/IcingaNotification.php new file mode 100644 index 0000000..9c5d08d --- /dev/null +++ b/library/Director/Objects/IcingaNotification.php @@ -0,0 +1,254 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use RuntimeException; + +class IcingaNotification extends IcingaObject implements ExportInterface +{ + protected $table = 'icinga_notification'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'apply_to' => null, + 'host_id' => null, + 'service_id' => null, + // 'users' => null, + // 'user_groups' => null, + 'times_begin' => null, + 'times_end' => null, + 'command_id' => null, + 'notification_interval' => null, + 'period_id' => null, + 'zone_id' => null, + 'assign_filter' => null, + ]; + + protected $uuidColumn = 'uuid'; + + protected $supportsCustomVars = true; + + protected $supportsFields = true; + + protected $supportsImports = true; + + protected $supportsApplyRules = true; + + protected $relatedSets = [ + 'states' => 'StateFilterSet', + 'types' => 'TypeFilterSet', + ]; + + protected $multiRelations = [ + 'users' => 'IcingaUser', + 'user_groups' => 'IcingaUserGroup', + ]; + + protected $relations = [ + 'zone' => 'IcingaZone', + 'host' => 'IcingaHost', + 'service' => 'IcingaService', + 'command' => 'IcingaCommand', + 'period' => 'IcingaTimePeriod', + ]; + + protected $intervalProperties = [ + 'notification_interval' => 'interval', + 'times_begin' => 'times_begin', + 'times_end' => 'times_end', + ]; + + protected function prefersGlobalZone() + { + return false; + } + + /** + * @codingStandardsIgnoreStart + * @return string + */ + protected function renderTimes_begin() + { + // @codingStandardsIgnoreEnd + return c::renderKeyValue('times.begin', c::renderInterval($this->get('times_begin'))); + } + + /** + * @codingStandardsIgnoreStart + * @return string + */ + protected function renderTimes_end() + { + // @codingStandardsIgnoreEnd + return c::renderKeyValue('times.end', c::renderInterval($this->get('times_end'))); + } + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @return \stdClass + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + // TODO: ksort in toPlainObject? + $props = (array) $this->toPlainObject(); + $props['fields'] = $this->loadFieldReferences(); + ksort($props); + + return (object) $props; + } + + /** + * @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['object_name']; + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Notification "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + // $object->newFields = $properties['fields']; + unset($properties['fields']); + $object->setProperties($properties); + + return $object; + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ + protected function loadFieldReferences() + { + $db = $this->getDb(); + + $res = $db->fetchAll( + $db->select()->from([ + 'nf' => 'icinga_notification_field' + ], [ + 'nf.datafield_id', + 'nf.is_required', + 'nf.var_filter', + ])->join(['df' => 'director_datafield'], 'df.id = nf.datafield_id', []) + ->where('notification_id = ?', $this->get('id')) + ->order('varname ASC') + ); + + if (empty($res)) { + return []; + } else { + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + return $res; + } + } + + /** + * Do not render internal property apply_to + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderApply_to() + { + // @codingStandardsIgnoreEnd + return ''; + } + + protected function renderObjectHeader() + { + if ($this->isApplyRule()) { + if (($to = $this->get('apply_to')) === null) { + throw new RuntimeException(sprintf( + 'No "apply_to" object type has been set for Applied notification "%s"', + $this->getObjectName() + )); + } + + return sprintf( + "%s %s %s to %s {\n", + $this->getObjectTypeName(), + $this->getType(), + c::renderString($this->getObjectName()), + ucfirst($to) + ); + } else { + return parent::renderObjectHeader(); + } + } + + /** + * Render host_id as host_name + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderHost_id() + { + // @codingStandardsIgnoreEnd + return $this->renderRelationProperty('host', $this->get('host_id'), 'host_name'); + } + + /** + * Render service_id as service_name + * + * @codingStandardsIgnoreStart + * @return string + */ + public function renderService_id() + { + // @codingStandardsIgnoreEnd + return $this->renderRelationProperty('service', $this->get('service_id'), 'service_name'); + } + + protected function setKey($key) + { + if (is_int($key)) { + $this->id = $key; + } elseif (is_array($key)) { + foreach (['id', 'host_id', 'service_id', 'object_name'] as $k) { + if (array_key_exists($k, $key)) { + $this->set($k, $key[$k]); + } + } + } else { + return parent::setKey($key); + } + + return $this; + } +} diff --git a/library/Director/Objects/IcingaNotificationField.php b/library/Director/Objects/IcingaNotificationField.php new file mode 100644 index 0000000..d51f9e6 --- /dev/null +++ b/library/Director/Objects/IcingaNotificationField.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaNotificationField extends IcingaObjectField +{ + protected $keyName = array('notification_id', 'datafield_id'); + + protected $table = 'icinga_notification_field'; + + protected $defaultProperties = array( + 'notification_id' => null, + 'datafield_id' => null, + 'is_required' => null, + 'var_filter' => null, + ); +} diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php new file mode 100644 index 0000000..04ae32b --- /dev/null +++ b/library/Director/Objects/IcingaObject.php @@ -0,0 +1,3258 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Exception; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterChain; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\CustomVariable\CustomVariables; +use Icinga\Module\Director\Data\Db\DbDataFormatter; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\IcingaConfig\AssignRenderer; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Exception\NestingError; +use Icinga\Module\Director\IcingaConfig\ExtensibleSet; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use LogicException; +use RuntimeException; + +abstract class IcingaObject extends DbObject implements IcingaConfigRenderer +{ + const RESOLVE_ERROR = '(unable to resolve)'; + + protected $keyName = 'object_name'; + + protected $autoincKeyName = 'id'; + + /** @var bool Whether this Object supports custom variables */ + protected $supportsCustomVars = false; + + /** @var bool Whether there exist Groups for this object type */ + protected $supportsGroups = false; + + /** @var bool Whether this Object makes use of (time) ranges */ + protected $supportsRanges = false; + + /** @var bool Whether inheritance via "imports" property is supported */ + protected $supportsImports = false; + + /** @var bool Allows controlled custom var access through Fields */ + protected $supportsFields = false; + + /** @var bool Whether this object can be rendered as 'apply Object' */ + protected $supportsApplyRules = false; + + /** @var bool Whether Sets of object can be defined */ + protected $supportsSets = false; + + /** @var bool Whether this Object supports template-based Choices */ + protected $supportsChoices = false; + + /** @var bool If the object is rendered in legacy config */ + protected $supportedInLegacy = false; + + protected $rangeClass; + + protected $type; + + /* key/value!! */ + protected $booleans = []; + + // Property suffixed with _id must exist + protected $relations = [ + // property => PropertyClass + ]; + + protected $relatedSets = [ + // property => ExtensibleSetClass + ]; + + protected $multiRelations = [ + // property => IcingaObjectClass + ]; + + /** @var IcingaObjectMultiRelations[] */ + protected $loadedMultiRelations = []; + + /** + * Allows to set properties pointing to related objects by name without + * loading the related object. + * + * @var array + */ + protected $unresolvedRelatedProperties = []; + + protected $loadedRelatedSets = []; + + // Will be rendered first, before imports + protected $prioritizedProperties = []; + + protected $propertiesNotForRendering = [ + 'id', + 'object_name', + 'object_type', + ]; + + /** + * Array of interval property names + * + * Those will be automagically munged to integers (seconds) and rendered + * as durations (e.g. 2m 10s). Array expects (propertyName => renderedKey) + * + * @var array + */ + protected $intervalProperties = []; + + /** @var Db */ + protected $connection; + + private $vars; + + /** @var IcingaObjectGroups */ + private $groups; + + private $imports; + + /** @var IcingaTimePeriodRanges - TODO: generic ranges */ + private $ranges; + + private $shouldBeRemoved = false; + + private $resolveCache = []; + + private $cachedPlainUnmodified; + + private $templateResolver; + + protected static $tree; + + /** + * @return Db + */ + public function getConnection() + { + return $this->connection; + } + + public function propertyIsBoolean($property) + { + return array_key_exists($property, $this->booleans); + } + + public function propertyIsInterval($property) + { + return array_key_exists($property, $this->intervalProperties); + } + + /** + * Whether a property ends with _id and might refer another object + * + * @param $property string Property name, like zone_id + * + * @return bool + */ + public function propertyIsRelation($property) + { + if ($key = $this->stripIdSuffix($property)) { + return $this->hasRelation($key); + } + + return false; + } + + protected function stripIdSuffix($key) + { + $end = substr($key, -3); + + if ('_id' === $end) { + return substr($key, 0, -3); + } + + return false; + } + + public function propertyIsRelatedSet($property) + { + return array_key_exists($property, $this->relatedSets); + } + + public function propertyIsMultiRelation($property) + { + return array_key_exists($property, $this->multiRelations); + } + + public function listMultiRelations() + { + return array_keys($this->multiRelations); + } + + public function getMultiRelation($property) + { + if (! $this->hasLoadedMultiRelation($property)) { + $this->loadMultiRelation($property); + } + + return $this->loadedMultiRelations[$property]; + } + + public function setMultiRelation($property, $values) + { + $this->getMultiRelation($property)->set($values); + return $this; + } + + private function loadMultiRelation($property) + { + if ($this->hasBeenLoadedFromDb()) { + $rel = IcingaObjectMultiRelations::loadForStoredObject( + $this, + $property, + $this->multiRelations[$property] + ); + } else { + $rel = new IcingaObjectMultiRelations( + $this, + $property, + $this->multiRelations[$property] + ); + } + + $this->loadedMultiRelations[$property] = $rel; + } + + private function hasLoadedMultiRelation($property) + { + return array_key_exists($property, $this->loadedMultiRelations); + } + + private function loadAllMultiRelations() + { + foreach (array_keys($this->multiRelations) as $key) { + if (! $this->hasLoadedMultiRelation($key)) { + $this->loadMultiRelation($key); + } + } + + ksort($this->loadedMultiRelations); + return $this->loadedMultiRelations; + } + + protected function getRelatedSetClass($property) + { + $prefix = '\\Icinga\\Module\\Director\\IcingaConfig\\'; + return $prefix . $this->relatedSets[$property]; + } + + /** + * @param $property + * @return ExtensibleSet + */ + protected function getRelatedSet($property) + { + if (! array_key_exists($property, $this->loadedRelatedSets)) { + /** @var ExtensibleSet $class */ + $class = $this->getRelatedSetClass($property); + $this->loadedRelatedSets[$property] + = $class::forIcingaObject($this, $property); + } + + return $this->loadedRelatedSets[$property]; + } + + /** + * @return ExtensibleSet[] + */ + protected function relatedSets() + { + $sets = []; + foreach ($this->relatedSets as $key => $class) { + $sets[$key] = $this->getRelatedSet($key); + } + + return $sets; + } + + /** + * Whether the given property name is a short name for a relation + * + * This might be 'zone' for 'zone_id' + * + * @param string $property Property name + * + * @return bool + */ + public function hasRelation($property) + { + return array_key_exists($property, $this->relations); + } + + protected function getRelationClass($property) + { + return __NAMESPACE__ . '\\' . $this->relations[$property]; + } + + protected function getRelationObjectClass($property) + { + return $this->relations[$property]; + } + + /** + * @param $property + * @return IcingaObject + */ + public function getRelated($property) + { + return $this->getRelatedObject($property, $this->{$property . '_id'}); + } + + /** + * @param $property + * @param $id + * @return string + */ + public function getRelatedObjectName($property, $id) + { + return $this->getRelatedObject($property, $id)->getObjectName(); + } + + /** + * @param $property + * @param $id + * @return IcingaObject + */ + protected function getRelatedObject($property, $id) + { + /** @var IcingaObject $class */ + $class = $this->getRelationClass($property); + try { + $object = $class::loadWithAutoIncId($id, $this->connection); + } catch (NotFoundError $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + + return $object; + } + + /** + * @param $property + * @return IcingaObject|null + */ + public function getResolvedRelated($property) + { + $id = $this->getSingleResolvedProperty($property . '_id'); + + if ($id) { + return $this->getRelatedObject($property, $id); + } + + return null; + } + + public function prefetchAllRelatedTypes() + { + foreach (array_unique(array_values($this->relations)) as $relClass) { + /** @var static $class */ + $class = __NAMESPACE__ . '\\' . $relClass; + $class::prefetchAll($this->getConnection()); + } + } + + public static function prefetchAllRelationsByType($type, Db $db) + { + /** @var static $class */ + $class = DbObjectTypeRegistry::classByType($type); + /** @var static $dummy */ + $dummy = $class::create([], $db); + $dummy->prefetchAllRelatedTypes(); + } + + /** + * Whether this Object supports custom variables + * + * @return bool + */ + public function supportsCustomVars() + { + return $this->supportsCustomVars; + } + + /** + * Whether there exist Groups for this object type + * + * @return bool + */ + public function supportsGroups() + { + return $this->supportsGroups; + } + + /** + * Whether this Object makes use of (time) ranges + * + * @return bool + */ + public function supportsRanges() + { + return $this->supportsRanges; + } + + /** + * Whether this object supports (command) Arguments + * + * @return bool + */ + public function supportsArguments() + { + return $this instanceof ObjectWithArguments; + } + + /** + * Whether this object supports inheritance through the "imports" property + * + * @return bool + */ + public function supportsImports() + { + return $this->supportsImports; + } + + /** + * Whether this object allows controlled custom var access through fields + * + * @return bool + */ + public function supportsFields() + { + return $this->supportsFields; + } + + /** + * Whether this object can be rendered as 'apply Object' + * + * @return bool + */ + public function supportsApplyRules() + { + return $this->supportsApplyRules; + } + + /** + * Whether this object supports 'assign' properties + * + * @return bool + */ + public function supportsAssignments() + { + return $this->isApplyRule(); + } + + /** + * Whether this object can be part of a 'set' + * + * @return bool + */ + public function supportsSets() + { + return $this->supportsSets; + } + + /** + * Whether this object supports template-based Choices + * + * @return bool + */ + public function supportsChoices() + { + return $this->supportsChoices; + } + + public function setAssignments($value) + { + return IcingaObjectLegacyAssignments::applyToObject($this, $value); + } + + /** + * @codingStandardsIgnoreStart + * + * @param Filter|string $filter + * + * @throws LogicException + * + * @return self + */ + public function setAssign_filter($filter) + { + if (! $this->supportsAssignments() && $filter !== null) { + if ($this->hasProperty('object_type')) { + $type = $this->get('object_type'); + } else { + $type = get_class($this); + } + + if ($type === null) { + throw new LogicException( + 'Cannot set assign_filter unless object_type has been set' + ); + } + throw new LogicException(sprintf( + 'I can only assign for applied objects or objects with native' + . ' support for assignments, got %s', + $type + )); + } + + // @codingStandardsIgnoreEnd + if ($filter instanceof Filter) { + $filter = $filter->toQueryString(); + } + + return $this->reallySet('assign_filter', $filter); + } + + /** + * It sometimes makes sense to defer lookups for related properties. This + * kind of lazy-loading allows us to for example set host = 'localhost' and + * render an object even when no such host exists. Think of the activity log, + * one might want to visualize a history host or service template even when + * the related command has been deleted in the meantime. + * + * @return self + */ + public function resolveUnresolvedRelatedProperties() + { + foreach ($this->unresolvedRelatedProperties as $name => $p) { + $this->resolveUnresolvedRelatedProperty($name); + } + + return $this; + } + + public function getUnresolvedRelated($property) + { + if ($this->hasRelation($property)) { + $property .= '_id'; + if (isset($this->unresolvedRelatedProperties[$property])) { + return $this->unresolvedRelatedProperties[$property]; + } + + return null; + } + + throw new RuntimeException(sprintf( + '%s "%s" has no %s reference', + $this->getShortTableName(), + $this->getObjectName(), + $property + )); + } + + /** + * @param $name + */ + protected function resolveUnresolvedRelatedProperty($name) + { + $short = substr($name, 0, -3); + /** @var IcingaObject $class */ + $class = $this->getRelationClass($short); + try { + $object = $class::load( + $this->unresolvedRelatedProperties[$name], + $this->connection + ); + } catch (NotFoundError $e) { + // Hint: eventually a NotFoundError would be better + throw new RuntimeException(sprintf( + 'Unable to load object (%s: %s) referenced from %s "%s", %s', + $short, + $this->unresolvedRelatedProperties[$name], + $this->getShortTableName(), + $this->getObjectName(), + lcfirst($e->getMessage()) + ), $e->getCode(), $e); + } + + $id = $object->get('id'); + // Happens when load() get's a branched object, created in the branch + if ($id !== null) { + $this->reallySet($name, $id); + unset($this->unresolvedRelatedProperties[$name]); + } + } + + /** + * @return bool + */ + public function hasBeenModified() + { + if (parent::hasBeenModified()) { + return true; + } + + if ($this->hasUnresolvedRelatedProperties()) { + $this->resolveUnresolvedRelatedProperties(); + + // Duplicates above code, but this makes it faster: + if (parent::hasBeenModified()) { + return true; + } + } + + if ($this->supportsCustomVars() && $this->vars !== null && $this->vars()->hasBeenModified()) { + return true; + } + + if ($this->supportsGroups() && $this->groups !== null && $this->groups()->hasBeenModified()) { + return true; + } + + if ($this->supportsImports() && $this->imports !== null && $this->imports()->hasBeenModified()) { + return true; + } + + if ($this->supportsRanges() && $this->ranges !== null && $this->ranges()->hasBeenModified()) { + return true; + } + + if ($this instanceof ObjectWithArguments + && $this->gotArguments() + && $this->arguments()->hasBeenModified() + ) { + return true; + } + + foreach ($this->loadedRelatedSets as $set) { + if ($set->hasBeenModified()) { + return true; + } + } + + foreach ($this->loadedMultiRelations as $rel) { + if ($rel->hasBeenModified()) { + return true; + } + } + + return false; + } + + protected function hasUnresolvedRelatedProperties() + { + return ! empty($this->unresolvedRelatedProperties); + } + + protected function hasUnresolvedRelatedProperty($name) + { + return array_key_exists($name, $this->unresolvedRelatedProperties); + } + + /** + * @param $key + * @return mixed + */ + protected function getRelationId($key) + { + if ($this->hasUnresolvedRelatedProperty($key)) { + $this->resolveUnresolvedRelatedProperty($key); + } + + return parent::get($key); + } + + /** + * @param $key + * @return string|null + */ + protected function getRelatedProperty($key) + { + $idKey = $key . '_id'; + if ($this->hasUnresolvedRelatedProperty($idKey)) { + return $this->unresolvedRelatedProperties[$idKey]; + } + + if ($id = $this->get($idKey)) { + /** @var IcingaObject $class */ + $class = $this->getRelationClass($key); + try { + $object = $class::loadWithAutoIncId($id, $this->connection); + } catch (NotFoundError $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + + return $object->getObjectName(); + } + + return null; + } + + /** + * @param string $key + * @return \Icinga\Module\Director\CustomVariable\CustomVariable|mixed|null + */ + public function get($key) + { + if (substr($key, 0, 5) === 'vars.') { + $var = $this->vars()->get(substr($key, 5)); + if ($var === null) { + return $var; + } + + return $var->getValue(); + } + + // e.g. zone_id + if ($this->propertyIsRelation($key)) { + return $this->getRelationId($key); + } + + // e.g. zone + if ($this->hasRelation($key)) { + return $this->getRelatedProperty($key); + } + + if ($this->propertyIsRelatedSet($key)) { + return $this->getRelatedSet($key)->toPlainObject(); + } + + if ($this->propertyIsMultiRelation($key)) { + return $this->getMultiRelation($key)->listRelatedNames(); + } + + return parent::get($key); + } + + public function setProperties($props) + { + if (is_array($props)) { + if (array_key_exists('object_type', $props) && key($props) !== 'object_type') { + $type = $props['object_type']; + unset($props['object_type']); + $props = ['object_type' => $type] + $props; + } + } + return parent::setProperties($props); + } + + public function set($key, $value) + { + if ($key === 'vars') { + $value = (array) $value; + $unset = []; + foreach ($this->vars() as $k => $f) { + if (! array_key_exists($k, $value)) { + $unset[] = $k; + } + } + foreach ($unset as $k) { + unset($this->vars()->$k); + } + foreach ($value as $k => $v) { + $this->vars()->set($k, $v); + } + return $this; + } + + if (substr($key, 0, 5) === 'vars.') { + //TODO: allow for deep keys + $this->vars()->set(substr($key, 5), $value); + return $this; + } + + if ($this instanceof ObjectWithArguments + && substr($key, 0, 10) === 'arguments.') { + $this->arguments()->set(substr($key, 10), $value); + return $this; + } + + if ($this->propertyIsBoolean($key)) { + return parent::set($key, DbDataFormatter::normalizeBoolean($value)); + } + + // e.g. zone_id + if ($this->propertyIsRelation($key)) { + return $this->setRelation($key, $value); + } + + // e.g. zone + if ($this->hasRelation($key)) { + return $this->setUnresolvedRelation($key, $value); + } + + if ($this->propertyIsMultiRelation($key)) { + $this->setMultiRelation($key, $value); + return $this; + } + + if ($this->propertyIsRelatedSet($key)) { + $this->getRelatedSet($key)->set($value); + return $this; + } + + if ($this->propertyIsInterval($key)) { + return parent::set($key, c::parseInterval($value)); + } + + return parent::set($key, $value); + } + + private function setRelation($key, $value) + { + if ((int) $key !== (int) $this->$key) { + unset($this->unresolvedRelatedProperties[$key]); + } + return parent::set($key, $value); + } + + private function setUnresolvedRelation($key, $value) + { + if ($value === null || strlen($value) === 0) { + unset($this->unresolvedRelatedProperties[$key . '_id']); + return parent::set($key . '_id', null); + } + + $this->unresolvedRelatedProperties[$key . '_id'] = $value; + return $this; + } + + protected function setRanges($ranges) + { + $this->ranges()->set((array) $ranges); + return $this; + } + + protected function getRanges() + { + return $this->ranges()->getValues(); + } + + protected function setDisabled($disabled) + { + return $this->reallySet('disabled', DbDataFormatter::normalizeBoolean($disabled)); + } + + public function isDisabled() + { + return $this->get('disabled') === 'y'; + } + + public function markForRemoval($remove = true) + { + $this->shouldBeRemoved = $remove; + return $this; + } + + public function shouldBeRemoved() + { + return $this->shouldBeRemoved; + } + + public function shouldBeRenamed() + { + return $this->hasBeenLoadedFromDb() + && $this->getOriginalProperty('object_name') !== $this->getObjectName(); + } + + /** + * @return IcingaObjectGroups + */ + public function groups() + { + $this->assertGroupsSupport(); + if ($this->groups === null) { + if ($this->hasBeenLoadedFromDb() && $this->get('id')) { + $this->groups = IcingaObjectGroups::loadForStoredObject($this); + } else { + $this->groups = new IcingaObjectGroups($this); + } + } + + return $this->groups; + } + + public function hasModifiedGroups() + { + $this->assertGroupsSupport(); + if ($this->groups === null) { + return false; + } + + return $this->groups->hasBeenModified(); + } + + public function getAppliedGroups() + { + $this->assertGroupsSupport(); + if (! $this instanceof IcingaHost) { + throw new RuntimeException('getAppliedGroups is only available for hosts currently!'); + } + if (! $this->hasBeenLoadedFromDb()) { + // There are no stored related/resolved groups. We'll also not resolve + // them here on demand. + return []; + } + $id = $this->get('id'); + if ($id === null) { + // Do not fail for branches. Should be handled otherwise + // TODO: throw an Exception, once we are able to deal with this + return []; + } + + $type = strtolower($this->getType()); + $query = $this->db->select()->from( + ['gr' => "icinga_${type}group_${type}_resolved"], + ['g.object_name'] + )->join( + ['g' => "icinga_${type}group"], + "g.id = gr.${type}group_id", + [] + )->joinLeft( + ['go' => "icinga_${type}group_${type}"], + "go.${type}group_id = gr.${type}group_id AND go.${type}_id = " . (int) $id, + [] + )->where( + "gr.${type}_id = ?", + (int) $id + )->where("go.${type}_id IS NULL")->order('g.object_name'); + + return $this->db->fetchCol($query); + } + + /** + * @return IcingaTimePeriodRanges + */ + public function ranges() + { + $this->assertRangesSupport(); + if ($this->ranges === null) { + /** @var IcingaTimePeriodRanges $class */ + $class = $this->getRangeClass(); + if ($this->hasBeenLoadedFromDb()) { + $this->ranges = $class::loadForStoredObject($this); + } else { + $this->ranges = new $class($this); + } + } + + return $this->ranges; + } + + protected function getRangeClass() + { + if ($this->rangeClass === null) { + $this->rangeClass = get_class($this) . 'Ranges'; + } + + return $this->rangeClass; + } + + /** + * @return IcingaObjectImports + */ + public function imports() + { + $this->assertImportsSupport(); + if ($this->imports === null) { + // can not use hasBeenLoadedFromDb() when in onStore() + if ($this->getProperty('id') !== null) { + $this->imports = IcingaObjectImports::loadForStoredObject($this); + } else { + $this->imports = new IcingaObjectImports($this); + } + } + + return $this->imports; + } + + public function gotImports() + { + return $this->imports !== null; + } + + public function setImports($imports) + { + if (! is_array($imports) && $imports !== null) { + $imports = [$imports]; + } + + try { + $this->imports()->set($imports); + } catch (NestingError $e) { + $this->imports = new IcingaObjectImports($this); + // Force modification, otherwise it won't be stored when empty + $this->imports->setModified()->set($imports); + } + + if ($this->imports()->hasBeenModified()) { + $this->invalidateResolveCache(); + } + } + + public function getImports() + { + return $this->listImportNames(); + } + + /** + * @deprecated This should no longer be in use + * @return IcingaTemplateResolver + */ + public function templateResolver() + { + if ($this->templateResolver === null) { + $this->templateResolver = new IcingaTemplateResolver($this); + } + + return $this->templateResolver; + } + + public function getResolvedProperty($key, $default = null) + { + if (array_key_exists($key, $this->unresolvedRelatedProperties)) { + $this->resolveUnresolvedRelatedProperty($key); + $this->invalidateResolveCache(); + } + + $properties = $this->getResolvedProperties(); + if (property_exists($properties, $key)) { + return $properties->$key; + } + + return $default; + } + + public function getInheritedProperty($key, $default = null) + { + if (array_key_exists($key, $this->unresolvedRelatedProperties)) { + $this->resolveUnresolvedRelatedProperty($key); + $this->invalidateResolveCache(); + } + + $properties = $this->getInheritedProperties(); + if (property_exists($properties, $key)) { + return $properties->$key; + } + + return $default; + } + + public function getInheritedVar($varname) + { + try { + $vars = $this->getInheritedVars(); + } catch (NestingError $e) { + return null; + } + + if (property_exists($vars, $varname)) { + return $vars->$varname; + } + + return null; + } + + public function getResolvedVar($varName) + { + try { + $vars = $this->getResolvedVars(); + } catch (NestingError $e) { + return null; + } + + if (property_exists($vars, $varName)) { + return $vars->$varName; + } + + return null; + } + + public function getOriginForVar($varName) + { + try { + $origins = $this->getOriginsVars(); + } catch (NestingError $e) { + return null; + } + + if (property_exists($origins, $varName)) { + return $origins->$varName; + } + + return null; + } + + public function getResolvedProperties() + { + return $this->getResolved('Properties'); + } + + public function getInheritedProperties() + { + return $this->getInherited('Properties'); + } + + public function getOriginsProperties() + { + return $this->getOrigins('Properties'); + } + + public function resolveProperties() + { + return $this->resolve('Properties'); + } + + public function getResolvedVars() + { + return $this->getResolved('Vars'); + } + + public function getInheritedVars() + { + return $this->getInherited('Vars'); + } + + public function resolveVars() + { + return $this->resolve('Vars'); + } + + public function getOriginsVars() + { + return $this->getOrigins('Vars'); + } + + public function getVars() + { + $vars = []; + foreach ($this->vars() as $key => $var) { + if ($var->hasBeenDeleted()) { + continue; + } + + $vars[$key] = $var->getValue(); + } + ksort($vars); + + return (object) $vars; + } + + /** + * This is mostly for magic getters + * @return array + */ + public function getGroups() + { + return $this->groups()->listGroupNames(); + } + + /** + * @return array + * @throws NotFoundError + */ + public function listInheritedGroupNames() + { + $parents = $this->imports()->getObjects(); + /** @var IcingaObject $parent */ + foreach (array_reverse($parents) as $parent) { + $inherited = $parent->getGroups(); + if (! empty($inherited)) { + return $inherited; + } + } + + return []; + } + + public function setGroups($groups) + { + $this->groups()->set($groups); + return $this; + } + + /** + * @return array + * @throws NotFoundError + */ + public function listResolvedGroupNames() + { + $groups = $this->groups()->listGroupNames(); + if (empty($groups)) { + return $this->listInheritedGroupNames(); + } + + return $groups; + } + + /** + * @param $group + * @return bool + * @throws NotFoundError + */ + public function hasGroup($group) + { + if ($group instanceof static) { + $group = $group->getObjectName(); + } + + return in_array($group, $this->listResolvedGroupNames()); + } + + protected function getResolved($what) + { + $func = 'resolve' . $what; + $res = $this->$func(); + return $res['_MERGED_']; + } + + protected function getInherited($what) + { + $func = 'resolve' . $what; + $res = $this->$func(); + return $res['_INHERITED_']; + } + + protected function getOrigins($what) + { + $func = 'resolve' . $what; + $res = $this->$func(); + return $res['_ORIGINS_']; + } + + protected function hasResolveCached($what) + { + return array_key_exists($what, $this->resolveCache); + } + + protected function & getResolveCached($what) + { + return $this->resolveCache[$what]; + } + + protected function storeResolvedCache($what, $vals) + { + $this->resolveCache[$what] = $vals; + } + + public function invalidateResolveCache() + { + $this->resolveCache = []; + return $this; + } + + public function countDirectDescendants() + { + $db = $this->getDb(); + $table = $this->getTableName(); + $type = $this->getShortTableName(); + + $query = $db->select()->from( + ['oi' => $table . '_inheritance'], + ['cnt' => 'COUNT(*)'] + )->where('oi.parent_' . $type . '_id = ?', (int) $this->get('id')); + + return $db->fetchOne($query); + } + + protected function triggerLoopDetection() + { + // $this->templateResolver()->listResolvedParentIds(); + } + + public function getSingleResolvedProperty($key, $default = null) + { + if (array_key_exists($key, $this->unresolvedRelatedProperties)) { + $this->resolveUnresolvedRelatedProperty($key); + $this->invalidateResolveCache(); + } + + if ($my = $this->get($key)) { + if ($my !== null) { + return $my; + } + } + + /** @var IcingaObject[] $imports */ + try { + $imports = array_reverse($this->imports()->getObjects()); + } catch (NotFoundError $e) { + throw new RuntimeException($e->getMessage(), 0, $e); + } + + // Eventually trigger loop detection + $this->listAncestorIds(); + + foreach ($imports as $object) { + $v = $object->getSingleResolvedProperty($key); + if (null !== $v) { + return $v; + } + } + + return $default; + } + + protected function resolve($what) + { + if ($this->hasResolveCached($what)) { + return $this->getResolveCached($what); + } + + // Force exception + if ($this->hasBeenLoadedFromDb()) { + $this->triggerLoopDetection(); + } + + $vals = []; + $vals['_MERGED_'] = (object) []; + $vals['_INHERITED_'] = (object) []; + $vals['_ORIGINS_'] = (object) []; + // $objects = $this->imports()->getObjects(); + $objects = IcingaTemplateRepository::instanceByObject($this) + ->getTemplatesIndexedByNameFor($this, true); + + $get = 'get' . $what; + $getInherited = 'getInherited' . $what; + $getOrigins = 'getOrigins' . $what; + + $blacklist = ['id', 'uuid', 'object_type', 'object_name', 'disabled']; + foreach ($objects as $name => $object) { + $origins = $object->$getOrigins(); + + foreach ($object->$getInherited() as $key => $value) { + if (in_array($key, $blacklist)) { + continue; + } + + if (! property_exists($origins, $key)) { + // TODO: Introduced with group membership resolver or + // choices - this should not be required. Check this! + continue; + } + + // $vals[$name]->$key = $value; + $vals['_MERGED_']->$key = $value; + $vals['_INHERITED_']->$key = $value; + $vals['_ORIGINS_']->$key = $origins->$key; + } + + foreach ($object->$get() as $key => $value) { + // TODO: skip if default value? + if ($value === null) { + continue; + } + if (in_array($key, $blacklist)) { + continue; + } + $vals['_MERGED_']->$key = $value; + $vals['_INHERITED_']->$key = $value; + $vals['_ORIGINS_']->$key = $name; + } + } + + foreach ($this->$get() as $key => $value) { + if ($value === null) { + continue; + } + + $vals['_MERGED_']->$key = $value; + } + + $this->storeResolvedCache($what, $vals); + + return $vals; + } + + public function matches(Filter $filter) + { + // TODO: speed up by passing only desired properties (filter columns) to + // toPlainObject method + /** @var FilterChain|FilterExpression $filter */ + return $filter->matches($this->toPlainObject()); + } + + protected function assertCustomVarsSupport() + { + if (! $this->supportsCustomVars()) { + throw new LogicException(sprintf( + 'Objects of type "%s" have no custom vars', + $this->getType() + )); + } + + return $this; + } + + protected function assertGroupsSupport() + { + if (! $this->supportsGroups()) { + throw new LogicException(sprintf( + 'Objects of type "%s" have no groups', + $this->getType() + )); + } + + return $this; + } + + protected function assertRangesSupport() + { + if (! $this->supportsRanges()) { + throw new LogicException(sprintf( + 'Objects of type "%s" have no ranges', + $this->getType() + )); + } + + return $this; + } + + protected function assertImportsSupport() + { + if (! $this->supportsImports()) { + throw new LogicException(sprintf( + 'Objects of type "%s" have no imports', + $this->getType() + )); + } + + return $this; + } + + /** + * @return CustomVariables + */ + public function vars() + { + $this->assertCustomVarsSupport(); + if ($this->vars === null) { + if ($this->hasBeenLoadedFromDb()) { + if (PrefetchCache::shouldBeUsed()) { + $this->vars = PrefetchCache::instance()->vars($this); + } else { + if ($this->get('id')) { + $this->vars = CustomVariables::loadForStoredObject($this); + } else { + $this->vars = new CustomVariables(); + } + } + + if ($this->getShortTableName() === 'host') { + $this->vars->setOverrideKeyName( + $this->getConnection()->settings()->override_services_varname + ); + } + } else { + $this->vars = new CustomVariables(); + } + } + + return $this->vars; + } + + /** + * @return bool + */ + public function hasInitializedVars() + { + $this->assertCustomVarsSupport(); + + return $this->vars !== null; + } + + public function getVarsTableName() + { + return $this->getTableName() . '_var'; + } + + public function getShortTableName() + { + // strlen('icinga_') = 7 + return substr($this->getTableName(), 7); + } + + public function getVarsIdColumn() + { + return $this->getShortTableName() . '_id'; + } + + public function hasProperty($key) + { + if ($this->propertyIsRelatedSet($key)) { + return true; + } + + if ($this->propertyIsMultiRelation($key)) { + return true; + } + + return parent::hasProperty($key); + } + + public function isObject() + { + return $this->hasProperty('object_type') + && $this->get('object_type') === 'object'; + } + + public function isTemplate() + { + return $this->hasProperty('object_type') + && $this->get('object_type') === 'template'; + } + + public function isExternal() + { + return $this->hasProperty('object_type') + && $this->get('object_type') === 'external_object'; + } + + public function isApplyRule() + { + return $this->hasProperty('object_type') + && $this->get('object_type') === 'apply'; + } + + public function setBeingLoadedFromDb() + { + if ($this instanceof ObjectWithArguments && $this->gotArguments()) { + $this->arguments()->setBeingLoadedFromDb(); + } + if ($this->supportsImports() && $this->gotImports()) { + $this->imports()->setBeingLoadedFromDb(); + } + if ($this->supportsCustomVars() && $this->vars !== null) { + $this->vars()->setBeingLoadedFromDb(); + } + if ($this->supportsGroups() && $this->groups !== null) { + $this->groups()->setBeingLoadedFromDb(); + } + if ($this->supportsRanges() && $this->ranges !== null) { + $this->ranges()->setBeingLoadedFromDb(); + } + + foreach ($this->loadedRelatedSets as $set) { + $set->setBeingLoadedFromDb(); + } + + foreach ($this->loadedMultiRelations as $multiRelation) { + $multiRelation->setBeingLoadedFromDb(); + } + // This might trigger DB requests and 404's. We might want to defer this, but a call to + // hasBeenModified triggers anyway: + $this->resolveUnresolvedRelatedProperties(); + + parent::setBeingLoadedFromDb(); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + * @throws \Zend_Db_Adapter_Exception + */ + protected function storeRelatedObjects() + { + $this + ->storeCustomVars() + ->storeGroups() + ->storeMultiRelations() + ->storeImports() + ->storeRanges() + ->storeRelatedSets() + ->storeArguments(); + } + + /** + * @throws NotFoundError + */ + protected function beforeStore() + { + $this->resolveUnresolvedRelatedProperties(); + if ($this->gotImports()) { + $this->imports()->getObjects(); + } + } + + /** + * @throws NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + * @throws \Zend_Db_Adapter_Exception + */ + public function onInsert() + { + DirectorActivityLog::logCreation($this, $this->connection); + $this->storeRelatedObjects(); + } + + /** + * @throws NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + * @throws \Zend_Db_Adapter_Exception + */ + public function onUpdate() + { + DirectorActivityLog::logModification($this, $this->connection); + $this->storeRelatedObjects(); + } + + public function onStore() + { + $this->notifyResolvers(); + } + + /** + * @return self + */ + protected function storeCustomVars() + { + if ($this->supportsCustomVars()) { + $this->vars !== null && $this->vars()->storeToDb($this); + } + + return $this; + } + + /** + * @return self + */ + protected function storeGroups() + { + if ($this->supportsGroups()) { + $this->groups !== null && $this->groups()->store(); + } + + return $this; + } + + /** + * @return self + */ + protected function storeMultiRelations() + { + foreach ($this->loadedMultiRelations as $rel) { + $rel->store(); + } + + return $this; + } + + /** + * @return self + */ + protected function storeRanges() + { + if ($this->supportsRanges()) { + $this->ranges !== null && $this->ranges()->store(); + } + + return $this; + } + + /** + * @return $this + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + protected function storeArguments() + { + if ($this instanceof ObjectWithArguments) { + $this->gotArguments() && $this->arguments()->store(); + } + + return $this; + } + + protected function notifyResolvers() + { + } + + /** + * @return $this + */ + protected function storeRelatedSets() + { + foreach ($this->loadedRelatedSets as $set) { + if ($set->hasBeenModified()) { + $set->store(); + } + } + + return $this; + } + + /** + * @return $this + * @throws NotFoundError + * @throws \Zend_Db_Adapter_Exception + */ + protected function storeImports() + { + if ($this->supportsImports()) { + $this->imports !== null && $this->imports()->store(); + } + + return $this; + } + + public function beforeDelete() + { + $this->cachedPlainUnmodified = $this->getPlainUnmodifiedObject(); + } + + public function getCachedUnmodifiedObject() + { + return $this->cachedPlainUnmodified; + } + + public function onDelete() + { + DirectorActivityLog::logRemoval($this, $this->connection); + } + + public function toSingleIcingaConfig() + { + $config = new IcingaConfig($this->connection); + $object = $this; + if ($object->isExternal()) { + $object->set('object_type', 'object'); + $wasExternal = true; + } else { + $wasExternal = false; + } + + try { + $object->renderToConfig($config); + } catch (Exception $e) { + $message = $e->getMessage(); + $showTrace = false; + if ($showTrace) { + $message .= "\n" . $e->getTraceAsString(); + } + $config->configFile( + 'failed-to-render' + )->prepend( + "/** Failed to render this object **/\n" + . '/* ' . $message . ' */' + ); + } + if ($wasExternal) { + $object->set('object_type', 'external_object'); + } + + return $config; + } + + public function isSupportedInLegacy() + { + return $this->supportedInLegacy; + } + + public function renderToLegacyConfig(IcingaConfig $config) + { + if ($this->isExternal()) { + return; + } + + if (! $this->isSupportedInLegacy()) { + $config->configFile( + 'director/ignored-objects', + '.cfg' + )->prepend( + sprintf( + "# Not supported for legacy config: %s object_name=%s\n", + get_class($this), + $this->getObjectName() + ) + ); + return; + } + + $filename = $this->getRenderingFilename(); + + $deploymentMode = $config->getDeploymentMode(); + if ($deploymentMode === 'active-passive') { + if ($this->getSingleResolvedProperty('zone_id') + && array_key_exists('enable_active_checks', $this->defaultProperties) + ) { + $passive = clone($this); + $passive->set('enable_active_checks', false); + + $config->configFile( + 'director/master/' . $filename, + '.cfg' + )->addLegacyObject($passive); + } + } elseif ($deploymentMode === 'masterless') { + // no additional config + } else { + throw new LogicException(sprintf( + 'Unsupported deployment mode: %s', + $deploymentMode + )); + } + + $config->configFile( + 'director/' . $this->getRenderingZone($config) . '/' . $filename, + '.cfg' + )->addLegacyObject($this); + } + + public function renderToConfig(IcingaConfig $config) + { + if ($config->isLegacy()) { + $this->renderToLegacyConfig($config); + return; + } + + if ($this->isExternal()) { + return; + } + + $config->configFile( + 'zones.d/' . $this->getRenderingZone($config) . '/' . $this->getRenderingFilename() + )->addObject($this); + } + + public function getRenderingFilename() + { + $type = $this->getShortTableName(); + + if ($this->isTemplate()) { + $filename = strtolower($type) . '_templates'; + } elseif ($this->isApplyRule()) { + $filename = strtolower($type) . '_apply'; + } else { + $filename = strtolower($type) . 's'; + } + + return $filename; + } + + /** + * @param $zoneId + * @param IcingaConfig|null $config + * @return string + * @throws NotFoundError + */ + protected function getNameForZoneId($zoneId, IcingaConfig $config = null) + { + // TODO: this is still ugly. + if ($config === null) { + return IcingaZone::loadWithAutoIncId( + $zoneId, + $this->getConnection() + )->getObjectName(); + } + + // Config has a lookup cache, is faster: + return $config->getZoneName($zoneId); + } + + public function getRenderingZone(IcingaConfig $config = null) + { + if ($this->hasUnresolvedRelatedProperty('zone_id')) { + return $this->get('zone'); + } + + if ($this->hasProperty('zone_id')) { + try { + if (! $this->supportsImports()) { + if ($zoneId = $this->get('zone_id')) { + return $this->getNameForZoneId($zoneId, $config); + } + } + + if ($zoneId = $this->getSingleResolvedProperty('zone_id')) { + return $this->getNameForZoneId($zoneId, $config); + } + } catch (NestingError $e) { + throw $e; + } catch (Exception $e) { + return self::RESOLVE_ERROR; + } + } + + return $this->getDefaultZone($config); + } + + protected function getDefaultZone(IcingaConfig $config = null) + { + if ($this->prefersGlobalZone()) { + return $this->connection->getDefaultGlobalZoneName(); + } + + return $this->connection->getMasterZoneName(); + } + + protected function prefersGlobalZone() + { + return $this->isTemplate() || $this->isApplyRule(); + } + + protected function renderImports() + { + if (! $this->supportsImports()) { + return ''; + } + + $ret = ''; + foreach ($this->getImports() as $name) { + $ret .= ' import ' . c::renderString($name) . "\n"; + } + + if ($ret !== '') { + $ret .= "\n"; + } + + return $ret; + } + + protected function renderLegacyImports() + { + if ($this->supportsImports()) { + return $this->imports()->toLegacyConfigString(); + } + + return ''; + } + + protected function renderLegacyRelationProperty($propertyName, $id, $renderKey = null) + { + return $this->renderLegacyObjectProperty( + $renderKey ?: $propertyName, + c1::renderString($this->getRelatedObjectName($propertyName, $id)) + ); + } + + // Disabled is a virtual property + protected function renderDisabled() + { + return ''; + } + + /** + * @codingStandardsIgnoreStart + */ + protected function renderLegacyHost_id($value) + { + if (is_array($value)) { + return c1::renderKeyValue('host_name', c1::renderArray($value)); + } + + return $this->renderLegacyRelationProperty( + 'host', + $this->get('host_id'), + 'host_name' + ); + } + + /** + * Display Name only exists for host/service in Icinga 1 + * + * Render it as alias for everything by default. + * + * Alias does not exist in Icinga 2 currently! + * + * @return string + */ + protected function renderLegacyDisplay_Name() + { + return c1::renderKeyValue('alias', $this->display_name); + } + + protected function renderLegacyTimeout() + { + return ''; + } + + protected function renderLegacyEnable_active_checks() + { + return $this->renderLegacyBooleanProperty( + 'enable_active_checks', + 'active_checks_enabled' + ); + } + + protected function renderLegacyEnable_passive_checks() + { + return $this->renderLegacyBooleanProperty( + 'enable_passive_checks', + 'passive_checks_enabled' + ); + } + + protected function renderLegacyEnable_event_handler() + { + return $this->renderLegacyBooleanProperty( + 'enable_active_checks', + 'event_handler_enabled' + ); + } + + protected function renderLegacyEnable_notifications() + { + return $this->renderLegacyBooleanProperty( + 'enable_notifications', + 'notifications_enabled' + ); + } + + protected function renderLegacyEnable_perfdata() + { + return $this->renderLegacyBooleanProperty( + 'enable_perfdata', + 'process_perf_data' + ); + } + + protected function renderLegacyVolatile() + { + // @codingStandardsIgnoreEnd + return $this->renderLegacyBooleanProperty( + 'volatile', + 'is_volatile' + ); + } + + protected function renderLegacyBooleanProperty($property, $legacyKey) + { + return c1::renderKeyValue( + $legacyKey, + c1::renderBoolean($this->get($property)) + ); + } + + protected function renderProperties() + { + $out = ''; + $blacklist = array_merge( + $this->propertiesNotForRendering, + $this->prioritizedProperties + ); + + foreach ($this->properties as $key => $value) { + if (in_array($key, $blacklist)) { + continue; + } + + $out .= $this->renderObjectProperty($key, $value); + } + + return $out; + } + + protected function renderLegacyProperties() + { + $out = ''; + $blacklist = array_merge( + $this->propertiesNotForRendering, + [] /* $this->prioritizedProperties */ + ); + + foreach ($this->properties as $key => $value) { + if (in_array($key, $blacklist)) { + continue; + } + + $out .= $this->renderLegacyObjectProperty($key, $value); + } + + return $out; + } + + protected function renderPrioritizedProperties() + { + $out = ''; + + foreach ($this->prioritizedProperties as $key) { + $out .= $this->renderObjectProperty($key, $this->properties[$key]); + } + + return $out; + } + + protected function renderObjectProperty($key, $value) + { + if (substr($key, -3) === '_id') { + $short = substr($key, 0, -3); + if ($this->hasUnresolvedRelatedProperty($key)) { + return c::renderKeyValue( + $short, // NOT + c::renderString($this->$short) + ); + } + } + + if ($value === null) { + return ''; + } + + $method = 'render' . ucfirst($key); + if (method_exists($this, $method)) { + return $this->$method($value); + } + + if ($this->propertyIsBoolean($key)) { + if ($value === $this->defaultProperties[$key]) { + return ''; + } + + return c::renderKeyValue( + $this->booleans[$key], + c::renderBoolean($value) + ); + } + + if ($this->propertyIsInterval($key)) { + return c::renderKeyValue( + $this->intervalProperties[$key], + c::renderInterval($value) + ); + } + + if (substr($key, -3) === '_id' + && $this->hasRelation($relKey = substr($key, 0, -3)) + ) { + return $this->renderRelationProperty($relKey, $value); + } + + return c::renderKeyValue( + $key, + $this->isApplyRule() ? + c::renderStringWithVariables($value) : + c::renderString($value) + ); + } + + protected function renderLegacyObjectProperty($key, $value) + { + if (substr($key, -3) === '_id') { + $short = substr($key, 0, -3); + if ($this->hasUnresolvedRelatedProperty($key)) { + return c1::renderKeyValue( + $short, // NOT + c1::renderString($this->$short) + ); + } + } + + if ($value === null) { + return ''; + } + + $method = 'renderLegacy' . ucfirst($key); + if (method_exists($this, $method)) { + return $this->$method($value); + } + + $method = 'render' . ucfirst($key); + if (method_exists($this, $method)) { + return $this->$method($value); + } + + if ($this->propertyIsBoolean($key)) { + if ($value === $this->defaultProperties[$key]) { + return ''; + } + + return c1::renderKeyValue( + $this->booleans[$key], + c1::renderBoolean($value) + ); + } + + if ($this->propertyIsInterval($key)) { + return c1::renderKeyValue( + $this->intervalProperties[$key], + c1::renderInterval($value) + ); + } + + if (substr($key, -3) === '_id' + && $this->hasRelation($relKey = substr($key, 0, -3)) + ) { + return $this->renderLegacyRelationProperty($relKey, $value); + } + + return c1::renderKeyValue($key, c1::renderString($value)); + } + + protected function renderBooleanProperty($key) + { + return c::renderKeyValue($key, c::renderBoolean($this->get($key))); + } + + protected function renderPropertyAsSeconds($key) + { + return c::renderKeyValue($key, c::renderInterval($this->get($key))); + } + + protected function renderSuffix() + { + return "}\n\n"; + } + + protected function renderLegacySuffix() + { + return "}\n\n"; + } + + /** + * @return string + */ + protected function renderCustomVars() + { + if ($this->supportsCustomVars()) { + return $this->vars()->toConfigString($this->isApplyRule()); + } + + return ''; + } + + /** + * @return string + */ + protected function renderLegacyCustomVars() + { + if ($this->supportsCustomVars()) { + return $this->vars()->toLegacyConfigString(); + } + + return ''; + } + + public function renderUuid() + { + return ''; + } + + /** + * @return string + */ + protected function renderGroups() + { + if ($this->supportsGroups()) { + return $this->groups()->toConfigString(); + } + + return ''; + } + + /** + * @return string + */ + protected function renderLegacyGroups() + { + if ($this->supportsGroups() && $this->hasBeenLoadedFromDb()) { + $applied = []; + if ($this instanceof IcingaHost) { + $applied = $this->getAppliedGroups(); + } + return $this->groups()->toLegacyConfigString($applied); + } + + return ''; + } + + /** + * @return string + */ + protected function renderMultiRelations() + { + $out = ''; + foreach ($this->loadAllMultiRelations() as $rel) { + $out .= $rel->toConfigString(); + } + + return $out; + } + + /** + * @return string + */ + protected function renderLegacyMultiRelations() + { + $out = ''; + foreach ($this->loadAllMultiRelations() as $rel) { + $out .= $rel->toLegacyConfigString(); + } + + return $out; + } + + /** + * @return string + */ + protected function renderRanges() + { + if ($this->supportsRanges()) { + return $this->ranges()->toConfigString(); + } + + return ''; + } + + /** + * @return string + */ + protected function renderLegacyRanges() + { + if ($this->supportsRanges()) { + return $this->ranges()->toLegacyConfigString(); + } + + return ''; + } + + /** + * @return string + */ + protected function renderArguments() + { + return ''; + } + + protected function renderRelatedSets() + { + $config = ''; + foreach ($this->relatedSets as $property => $class) { + $config .= $this->getRelatedSet($property)->renderAs($property); + } + return $config; + } + + protected function renderRelationProperty($propertyName, $id, $renderKey = null) + { + return c::renderKeyValue( + $renderKey ?: $propertyName, + c::renderString($this->getRelatedObjectName($propertyName, $id)) + ); + } + + protected function renderCommandProperty($commandId, $propertyName = 'check_command') + { + return c::renderKeyValue( + $propertyName, + c::renderString($this->connection->getCommandName($commandId)) + ); + } + + /** + * @param $value + * @return string + * @codingStandardsIgnoreStart + */ + protected function renderLegacyCheck_command($value) + { + // @codingStandardsIgnoreEnd + $args = []; + foreach ($this->vars() as $k => $v) { + if (substr($k, 0, 3) === 'ARG') { + $args[] = $v->getValue(); + } + } + array_unshift($args, $value); + + return c1::renderKeyValue('check_command', implode('!', $args)); + } + + /** + * @param $value + * @return string + * @codingStandardsIgnoreStart + */ + protected function renderLegacyEvent_command($value) + { + // @codingStandardsIgnoreEnd + return c1::renderKeyValue('event_handler', $value); + } + + /** + * We do not render zone properties, objects are stored to zone dirs + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + protected function renderZone_id() + { + // @codingStandardsIgnoreEnd + return ''; + } + + protected function renderCustomExtensions() + { + return ''; + } + + protected function renderLegacyCustomExtensions() + { + $str = ''; + + // Set notification settings for the object to suppress warnings + if (array_key_exists('enable_notifications', $this->defaultProperties) + && $this->isTemplate() + ) { + $str .= c1::renderKeyValue('notification_period', 'notification_none'); + $str .= c1::renderKeyValue('notification_interval', '0'); + $str .= c1::renderKeyValue('contact_groups', 'icingaadmins'); + } + + // force rendering of check_command when ARG1 is set + if ($this->supportsCustomVars() && array_key_exists('check_command_id', $this->defaultProperties)) { + if ($this->get('check_command') === null + && $this->vars()->get('ARG1') !== null + ) { + $command = $this->getResolvedRelated('check_command'); + $str .= $this->renderLegacyCheck_command($command->getObjectName()); + } + } + + return $str; + } + + protected function renderObjectHeader() + { + return sprintf( + "%s %s %s {\n", + $this->getObjectTypeName(), + $this->getType(), + c::renderString($this->getObjectName()) + ); + } + + public function getLegacyObjectType() + { + return strtolower($this->getType()); + } + + protected function renderLegacyObjectHeader() + { + $type = $this->getLegacyObjectType(); + + if ($this->isTemplate()) { + $name = c1::renderKeyValue( + $this->getLegacyObjectKeyName(), + c1::renderString($this->getObjectName()) + ); + } else { + $name = c1::renderKeyValue( + $this->getLegacyObjectKeyName(), + c1::renderString($this->getObjectName()) + ); + } + + $str = "define $type {\n$name"; + if ($this->isTemplate()) { + $str .= c1::renderKeyValue('register', '0'); + } + + return $str; + } + + protected function getLegacyObjectKeyName() + { + if ($this->isTemplate()) { + return 'name'; + } + + return $this->getLegacyObjectType() . '_name'; + } + + /** + * @codingStandardsIgnoreStart + */ + public function renderAssign_Filter() + { + return ' ' . AssignRenderer::forFilter( + Filter::fromQueryString($this->get('assign_filter')) + )->renderAssign() . "\n"; + } + + public function renderLegacyAssign_Filter() + { + // @codingStandardsIgnoreEnd + if ($this instanceof IcingaHostGroup) { + $c = " # resolved memberships are set via the individual object\n"; + } elseif ($this instanceof IcingaService) { + $c = " # resolved objects are listed here\n"; + } else { + $c = " # assign is not supported for " . $this->type . "\n"; + } + $c .= ' #' . AssignRenderer::forFilter( + Filter::fromQueryString($this->get('assign_filter')) + )->renderAssign() . "\n"; + return $c; + } + + public function toLegacyConfigString() + { + $str = implode([ + $this->renderLegacyObjectHeader(), + $this->renderLegacyImports(), + $this->renderLegacyProperties(), + //$this->renderArguments(), + //$this->renderRelatedSets(), + $this->renderLegacyGroups(), + $this->renderLegacyMultiRelations(), + $this->renderLegacyRanges(), + $this->renderLegacyCustomExtensions(), + $this->renderLegacyCustomVars(), + $this->renderLegacySuffix() + ]); + + $str = $this->alignLegacyProperties($str); + + if ($this->isDisabled()) { + return + "# --- This object has been disabled ---\n" + . preg_replace('~^~m', '# ', trim($str)) + . "\n\n"; + } + + return $str; + } + + protected function alignLegacyProperties($configString) + { + $lines = explode("\n", $configString); + $len = 24; + + foreach ($lines as &$line) { + if (preg_match('/^\s{4}([^\t]+)\t+(.+)$/', $line, $m)) { + if ($len - strlen($m[1]) < 0) { + $fill = ' '; + } else { + $fill = str_repeat(' ', $len - strlen($m[1])); + } + + $line = ' ' . $m[1] . $fill . $m[2]; + } + } + + return implode("\n", $lines); + } + + public function toConfigString() + { + $str = implode([ + $this->renderObjectHeader(), + $this->renderPrioritizedProperties(), + $this->renderImports(), + $this->renderProperties(), + $this->renderArguments(), + $this->renderRelatedSets(), + $this->renderGroups(), + $this->renderMultiRelations(), + $this->renderRanges(), + $this->renderCustomExtensions(), + $this->renderCustomVars(), + $this->renderSuffix() + ]); + + if ($this->isDisabled()) { + return "/* --- This object has been disabled ---\n" + // Do not allow strings to break our comment + . str_replace('*/', "* /", $str) . "*/\n"; + } + + return $str; + } + + public function isGroup() + { + return substr($this->getType(), -5) === 'Group'; + } + + public function hasCheckCommand() + { + return false; + } + + protected function getType() + { + if ($this->type === null) { + $parts = explode('\\', get_class($this)); + // 6 = strlen('Icinga'); + $this->type = substr(end($parts), 6); + } + + return $this->type; + } + + protected function getObjectTypeName() + { + if ($this->isTemplate()) { + return 'template'; + } + if ($this->isApplyRule()) { + return 'apply'; + } + + return 'object'; + } + + public function getObjectName() + { + $property = static::getKeyColumnName(); + if ($this->hasProperty($property)) { + return $this->get($property); + } + + throw new LogicException(sprintf( + 'Trying to access "%s" for an instance of "%s"', + $property, + get_class($this) + )); + } + + /** + * @deprecated use DbObjectTypeRegistry::classByType() + * @param $type + * @return string + */ + public static function classByType($type) + { + return DbObjectTypeRegistry::classByType($type); + } + + /** + * @param $type + * @param array $properties + * @param Db|null $db + * + * @return IcingaObject + */ + public static function createByType($type, $properties = [], Db $db = null) + { + /** @var IcingaObject $class */ + $class = DbObjectTypeRegistry::classByType($type); + return $class::create($properties, $db); + } + + /** + * @param $type + * @param $id + * @param Db $db + * + * @return IcingaObject + * @throws NotFoundError + */ + public static function loadByType($type, $id, Db $db) + { + /** @var IcingaObject $class */ + $class = DbObjectTypeRegistry::classByType($type); + return $class::load($id, $db); + } + + /** + * @param $type + * @param $id + * @param Db $db + * + * @return bool + */ + public static function existsByType($type, $id, Db $db) + { + /** @var IcingaObject $class */ + $class = DbObjectTypeRegistry::classByType($type); + return $class::exists($id, $db); + } + + public static function getKeyColumnName() + { + return 'object_name'; + } + + public static function loadAllByType($type, Db $db, $query = null, $keyColumn = null) + { + /** @var DbObject $class */ + $class = DbObjectTypeRegistry::classByType($type); + + if ($keyColumn === null) { + if (method_exists($class, 'getKeyColumnName')) { + $keyColumn = $class::getKeyColumnName(); + } + } + + if (is_array($class::create()->getKeyName())) { + return $class::loadAll($db, $query); + } + + if (PrefetchCache::shouldBeUsed() + && $query === null + && $keyColumn === static::getKeyColumnName() + ) { + $result = []; + foreach ($class::prefetchAll($db) as $row) { + $result[$row->$keyColumn] = $row; + } + + return $result; + } + + return $class::loadAll($db, $query, $keyColumn); + } + + /** + * @param $type + * @param Db $db + * @return IcingaObject[] + */ + public static function loadAllExternalObjectsByType($type, Db $db) + { + /** @var IcingaObject $class */ + $class = DbObjectTypeRegistry::classByType($type); + $dummy = $class::create(); + + if (is_array($dummy->getKeyName())) { + throw new LogicException(sprintf( + 'There is no support for loading external objects of type "%s"', + $type + )); + } + + $query = $db->getDbAdapter() + ->select() + ->from($dummy->getTableName()) + ->where('object_type = ?', 'external_object'); + + return $class::loadAll($db, $query, 'object_name'); + } + + public static function fromJson($json, Db $connection = null) + { + return static::fromPlainObject(json_decode($json), $connection); + } + + public static function fromPlainObject($plain, Db $connection = null) + { + return static::create((array) $plain, $connection); + } + + /** + * @param IcingaObject $object + * @param null $preserve + * @return $this + * @throws NotFoundError + */ + public function replaceWith(IcingaObject $object, $preserve = []) + { + return $this->replaceWithProperties($object->toPlainObject(), $preserve); + } + + /** + * @param array|object $properties + * @param array $preserve + * @return $this + * @throws NotFoundError + */ + public function replaceWithProperties($properties, $preserve = []) + { + $properties = (array) $properties; + foreach ($preserve as $k) { + $v = $this->get($k); + if ($v !== null) { + $properties[$k] = $v; + } + } + $this->setProperties($properties); + + return $this; + } + + /** + * TODO: with rules? What if I want to override vars? Drop in favour of vars.x? + * + * @param IcingaObject $object + * @param bool $replaceVars + * @return $this + * @throws NotFoundError + */ + public function merge(IcingaObject $object, $replaceVars = false) + { + $object = clone($object); + + if ($object->supportsCustomVars()) { + $vars = $object->getVars(); + $object->set('vars', []); + } + + if ($object->supportsGroups()) { + $groups = $object->getGroups(); + $object->set('groups', []); + } + + if ($object->supportsImports()) { + $imports = $object->listImportNames(); + $object->set('imports', []); + } + + $plain = (array) $object->toPlainObject(false, false); + unset($plain['vars'], $plain['groups'], $plain['imports']); + foreach ($plain as $p => $v) { + if ($v === null) { + // We want default values, but no null values + continue; + } + + $this->set($p, $v); + } + + if ($object->supportsCustomVars()) { + $myVars = $this->vars(); + if ($replaceVars) { + $this->set('vars', $vars); + } else { + /** @var CustomVariables $vars */ + foreach ($vars as $key => $var) { + $myVars->set($key, $var); + } + } + } + + if ($object->supportsGroups()) { + if (! empty($groups)) { + $this->set('groups', $groups); + } + } + + if ($object->supportsImports()) { + if (! empty($imports)) { + $this->set('imports', $imports); + } + } + + return $this; + } + + /** + * @param bool $resolved + * @param bool $skipDefaults + * @param array|null $chosenProperties + * @param bool $resolveIds + * @param bool $keepId + * @return object + * @throws NotFoundError + */ + public function toPlainObject( + $resolved = false, + $skipDefaults = false, + array $chosenProperties = null, + $resolveIds = true, + $keepId = false + ) { + $props = []; + + if ($resolved) { + $p = $this->getInheritedProperties(); + foreach ($this->properties as $k => $v) { + if ($v === null && property_exists($p, $k)) { + continue; + } + $p->$k = $v; + } + } else { + $p = $this->properties; + } + + foreach ($p as $k => $v) { + // Do not ship ids for IcingaObjects: + if ($k === $this->getUuidColumn()) { + continue; + } + if ($resolveIds) { + if ($k === 'id' && $keepId === false && $this->hasProperty('object_name')) { + continue; + } + + if ('_id' === substr($k, -3)) { + $relKey = substr($k, 0, -3); + + if ($this->hasRelation($relKey)) { + if ($this->hasUnresolvedRelatedProperty($k)) { + $v = $this->$relKey; + } elseif ($v !== null) { + $v = $this->getRelatedObjectName($relKey, $v); + } + + $k = $relKey; + } else { + throw new LogicException(sprintf( + 'No such relation: %s', + $relKey + )); + } + } + } + + // TODO: Do not ship null properties based on flag? + if (!$skipDefaults || $this->differsFromDefaultValue($k, $v)) { + if ($k === 'disabled' || $this->propertyIsBoolean($k)) { + $props[$k] = $this->booleanForDbValue($v); + } else { + $props[$k] = $v; + } + } + } + + if ($this->supportsGroups()) { + // TODO: resolve + $groups = $this->groups()->listGroupNames(); + if ($resolved && empty($groups)) { + $groups = $this->listInheritedGroupNames(); + } + + $props['groups'] = $groups; + } + + foreach ($this->loadAllMultiRelations() as $key => $rel) { + if (count($rel) || !$skipDefaults) { + $props[$key] = $rel->listRelatedNames(); + } + } + + if ($this instanceof ObjectWithArguments) { + $props['arguments'] = $this->arguments()->toPlainObject( + false, + $skipDefaults + ); + } + + if ($this->supportsCustomVars()) { + if ($resolved) { + $props['vars'] = $this->getResolvedVars(); + } else { + $props['vars'] = $this->getVars(); + } + } + + if ($this->supportsImports()) { + if ($resolved) { + $props['imports'] = []; + } else { + $props['imports'] = $this->listImportNames(); + } + } + + if ($this->supportsRanges()) { + // TODO: resolve + $props['ranges'] = $this->get('ranges'); + } + + if ($skipDefaults) { + foreach (['imports', 'ranges', 'arguments'] as $key) { + if (empty($props[$key])) { + unset($props[$key]); + } + } + + if (array_key_exists('vars', $props)) { + if (count((array) $props['vars']) === 0) { + unset($props['vars']); + } + } + if (empty($props['groups'])) { + unset($props['groups']); + } + } + + foreach ($this->relatedSets() as $property => $set) { + if ($resolved) { + if ($this->supportsImports()) { + $set = clone($set); + foreach ($this->imports()->getObjects() as $parent) { + $set->inheritFrom($parent->getRelatedSet($property)); + } + } + + $values = $set->getResolvedValues(); + if (empty($values)) { + if (!$skipDefaults) { + $props[$property] = null; + } + } else { + $props[$property] = $values; + } + } else { + if ($set->isEmpty()) { + if (!$skipDefaults) { + $props[$property] = null; + } + } else { + $props[$property] = $set->toPlainObject(); + } + } + } + + if ($chosenProperties !== null) { + $chosen = []; + foreach ($chosenProperties as $k) { + if (array_key_exists($k, $props)) { + $chosen[$k] = $props[$k]; + } + } + + $props = $chosen; + } + ksort($props); + + return (object) $props; + } + + protected function booleanForDbValue($value) + { + if ($value === 'y') { + return true; + } + if ($value === 'n') { + return false; + } + + return $value; // let this fail elsewhere, if not null + } + + public function listImportNames() + { + if ($this->gotImports()) { + return $this->imports()->listImportNames(); + } + + return $this->templateTree()->listParentNamesFor($this); + } + + public function listFlatResolvedImportNames() + { + return $this->templateTree()->getAncestorsFor($this); + } + + public function listImportIds() + { + return $this->templateTree()->listParentIdsFor($this); + } + + public function listAncestorIds() + { + return $this->templateTree()->listAncestorIdsFor($this); + } + + protected function templateTree() + { + return $this->templates()->tree(); + } + + protected function templates() + { + return IcingaTemplateRepository::instanceByObject($this, $this->getConnection()); + } + + protected function differsFromDefaultValue($key, $value) + { + if (array_key_exists($key, $this->defaultProperties)) { + return $value !== $this->defaultProperties[$key]; + } + + return $value !== null; + } + + protected function mapHostsToZones($names) + { + $map = []; + + foreach ($names as $hostname) { + /** @var IcingaHost $host */ + $host = IcingaHost::load($hostname, $this->connection); + + $zone = $host->getRenderingZone(); + if (! array_key_exists($zone, $map)) { + $map[$zone] = []; + } + + $map[$zone][] = $hostname; + } + + ksort($map); + + return $map; + } + + public function getUrlParams() + { + $params = []; + if ($column = $this->getUuidColumn()) { + return [$column => $this->getUniqueId()->toString()]; + } + + if ($this->isApplyRule() && ! $this instanceof IcingaScheduledDowntime) { + $params['id'] = $this->get('id'); + } else { + $params = ['name' => $this->getObjectName()]; + + if ($this->hasProperty('host_id') && $this->get('host_id')) { + $params['host'] = $this->get('host'); + } + + if ($this->hasProperty('service_id') && $this->get('service_id')) { + $params['service'] = $this->get('service'); + } + + if ($this->hasProperty('service_set_id') && $this->get('service_set_id')) { + $params['set'] = $this->get('service_set'); + } + } + + return $params; + } + + public function getOnDeleteUrl() + { + $plural= preg_replace('/cys$/', 'cies', strtolower($this->getShortTableName()) . 's'); + return 'director/' . $plural; + } + + /** + * @param bool $resolved + * @param bool $skipDefaults + * @param array|null $chosenProperties + * @return string + * @throws NotFoundError + */ + public function toJson( + $resolved = false, + $skipDefaults = false, + array $chosenProperties = null + ) { + + return json_encode($this->toPlainObject($resolved, $skipDefaults, $chosenProperties)); + } + + public function getPlainUnmodifiedObject() + { + $props = []; + + foreach ($this->getOriginalProperties() as $k => $v) { + // Do not ship ids for IcingaObjects: + if ($k === 'id' && $this->hasProperty('object_name')) { + continue; + } + if ($k === $this->getUuidColumn()) { + continue; + } + if ($k === 'disabled' && $v === null) { + continue; + } + + if ('_id' === substr($k, -3)) { + $relKey = substr($k, 0, -3); + + if ($this->hasRelation($relKey)) { + if ($v !== null) { + $v = $this->getRelatedObjectName($relKey, $v); + } + + $k = $relKey; + } + } + + if ($this->differsFromDefaultValue($k, $v)) { + if ($k === 'disabled' || $this->propertyIsBoolean($k)) { + $props[$k] = $this->booleanForDbValue($v); + } else { + $props[$k] = $v; + } + } + } + + if ($this->supportsCustomVars()) { + $originalVars = $this->vars()->getOriginalVars(); + if (! empty($originalVars)) { + $props['vars'] = (object) []; + foreach ($originalVars as $name => $var) { + $props['vars']->$name = $var->getValue(); + } + } + } + if ($this->supportsGroups()) { + $groups = $this->groups()->listOriginalGroupNames(); + if (! empty($groups)) { + $props['groups'] = $groups; + } + } + if ($this->supportsImports()) { + $imports = $this->imports()->listOriginalImportNames(); + if (! empty($imports)) { + $props['imports'] = $imports; + } + } + + if ($this instanceof ObjectWithArguments) { + $args = $this->arguments()->toUnmodifiedPlainObject(); + if (! empty($args)) { + $props['arguments'] = $args; + } + } + + if ($this->supportsRanges()) { + $ranges = $this->ranges()->getOriginalValues(); + if (!empty($ranges)) { + $props['ranges'] = $ranges; + } + } + + foreach ($this->relatedSets() as $property => $set) { + if ($set->isEmpty()) { + continue; + } + + $props[$property] = $set->getPlainUnmodifiedObject(); + } + + foreach ($this->loadAllMultiRelations() as $key => $rel) { + $old = $rel->listOriginalNames(); + if (! empty($old)) { + $props[$key] = $old; + } + } + + return (object) $props; + } + + public function __toString() + { + try { + return $this->toConfigString(); + } catch (Exception $e) { + trigger_error($e); + $previousHandler = set_exception_handler( + function () { + } + ); + restore_error_handler(); + if ($previousHandler !== null) { + call_user_func($previousHandler, $e); + die(); + } + + die($e->getMessage()); + } + } + + public function __destruct() + { + unset($this->resolveCache); + unset($this->vars); + unset($this->groups); + unset($this->imports); + unset($this->ranges); + if ($this instanceof ObjectWithArguments) { + $this->unsetArguments(); + } + + parent::__destruct(); + } +} diff --git a/library/Director/Objects/IcingaObjectField.php b/library/Director/Objects/IcingaObjectField.php new file mode 100644 index 0000000..e18965b --- /dev/null +++ b/library/Director/Objects/IcingaObjectField.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Data\Db\DbObject; + +abstract class IcingaObjectField extends DbObject +{ + /** + * + * @param Filter|string $filter + * + * @return $this + * @codingStandardsIgnoreStart + */ + protected function setVar_filter($value) + { + // @codingStandardsIgnoreEnd + if ($value instanceof Filter) { + $value = $value->toQueryString(); + } + + return $this->reallySet('var_filter', $value); + } +} diff --git a/library/Director/Objects/IcingaObjectGroup.php b/library/Director/Objects/IcingaObjectGroup.php new file mode 100644 index 0000000..c0bec54 --- /dev/null +++ b/library/Director/Objects/IcingaObjectGroup.php @@ -0,0 +1,76 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; + +abstract class IcingaObjectGroup extends IcingaObject implements ExportInterface +{ + protected $supportsImports = true; + + protected $supportedInLegacy = true; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'display_name' => null, + 'assign_filter' => null, + ]; + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + return $this->toPlainObject(); + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaObjectGroup + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + $name = $properties['object_name']; + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Group "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + $object->setProperties($properties); + + return $object; + } + + protected function prefersGlobalZone() + { + return true; + } +} diff --git a/library/Director/Objects/IcingaObjectGroups.php b/library/Director/Objects/IcingaObjectGroups.php new file mode 100644 index 0000000..8bef1b1 --- /dev/null +++ b/library/Director/Objects/IcingaObjectGroups.php @@ -0,0 +1,408 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Countable; +use Exception; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; +use Iterator; +use RuntimeException; + +class IcingaObjectGroups implements Iterator, Countable, IcingaConfigRenderer +{ + protected $storedGroups = array(); + + protected $groups = array(); + + protected $modified = false; + + protected $object; + + private $position = 0; + + protected $idx = array(); + + public function __construct(IcingaObject $object) + { + $this->object = $object; + + if (! $object->hasBeenLoadedFromDb() && PrefetchCache::shouldBeUsed()) { + /** @var IcingaObjectGroup $class */ + $class = $this->getGroupClass(); + $class::prefetchAll($this->object->getConnection()); + } + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->groups); + } + + #[\ReturnTypeWillChange] + public function rewind() + { + $this->position = 0; + } + + public function hasBeenModified() + { + return $this->modified; + } + + #[\ReturnTypeWillChange] + public function current() + { + if (! $this->valid()) { + return null; + } + + return $this->groups[$this->idx[$this->position]]; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->idx[$this->position]; + } + + #[\ReturnTypeWillChange] + public function next() + { + ++$this->position; + } + + #[\ReturnTypeWillChange] + public function valid() + { + return array_key_exists($this->position, $this->idx); + } + + public function get($key) + { + if (array_key_exists($key, $this->groups)) { + return $this->groups[$key]; + } + + return null; + } + + /** + * @param $group + * @return $this + * @throws NotFoundError + */ + public function set($group) + { + if (! is_array($group)) { + $group = array($group); + } + + $existing = array_keys($this->groups); + $new = array(); + $class = $this->getGroupClass(); + $unset = array(); + + foreach ($group as $k => $g) { + if ($g instanceof $class) { + $new[] = $g->object_name; + } else { + if (empty($g)) { + $unset[] = $k; + continue; + } + + $new[] = $g; + } + } + + foreach ($unset as $k) { + unset($group[$k]); + } + + sort($existing); + sort($new); + if ($existing === $new) { + return $this; + } + + $this->groups = array(); + if (empty($group)) { + $this->modified = true; + $this->refreshIndex(); + return $this; + } + + return $this->add($group); + } + + /** + * Magic isset check + * + * @return boolean + */ + public function __isset($group) + { + return array_key_exists($group, $this->groups); + } + + public function remove($group) + { + if (array_key_exists($group, $this->groups)) { + unset($this->groups[$group]); + } + + $this->modified = true; + $this->refreshIndex(); + } + + protected function refreshIndex() + { + ksort($this->groups); + $this->idx = array_keys($this->groups); + } + + /** + * @param $group + * @param string $onError + * @return $this + * @throws NotFoundError + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public function add($group, $onError = 'fail') + { + // TODO: only one query when adding array + if (is_array($group)) { + foreach ($group as $g) { + $this->add($g, $onError); + } + return $this; + } + + /** @var IcingaObjectGroup $class */ + $class = $this->getGroupClass(); + + if ($group instanceof $class) { + if (array_key_exists($group->getObjectName(), $this->groups)) { + return $this; + } + + $this->groups[$group->object_name] = $group; + } elseif (is_string($group)) { + if (array_key_exists($group, $this->groups)) { + return $this; + } + + $connection = $this->object->getConnection(); + + try { + $this->groups[$group] = $class::load($group, $connection); + } catch (NotFoundError $e) { + switch ($onError) { + case 'autocreate': + $newGroup = $class::create(array( + 'object_type' => 'object', + 'object_name' => $group + )); + $newGroup->store($connection); + $this->groups[$group] = $newGroup; + break; + case 'fail': + throw new NotFoundError( + 'The group "%s" doesn\'t exist.', + $group + ); + break; + case 'ignore': + return $this; + } + } + } else { + throw new RuntimeException( + 'Invalid group object: %s', + var_export($group, 1) + ); + } + + $this->modified = true; + $this->refreshIndex(); + + return $this; + } + + protected function getGroupTableName() + { + return $this->object->getTableName() . 'group'; + } + + + protected function getGroupMemberTableName() + { + return $this->object->getTableName() . 'group_' . $this->getType(); + } + + public function listGroupNames() + { + return array_keys($this->groups); + } + + public function listOriginalGroupNames() + { + return array_keys($this->storedGroups); + } + + public function getType() + { + return $this->object->getShortTableName(); + } + + protected function loadFromDb() + { + $db = $this->object->getDb(); + $connection = $this->object->getConnection(); + + $type = $this->getType(); + + $table = $this->object->getTableName(); + $query = $db->select()->from( + array('go' => $table . 'group_' . $type), + array() + )->join( + array('g' => $table . 'group'), + 'go.' . $type . 'group_id = g.id', + '*' + )->where('go.' . $type . '_id = ?', $this->object->id) + ->order('g.object_name'); + + $class = $this->getGroupClass(); + $this->groups = $class::loadAll($connection, $query, 'object_name'); + $this->setBeingLoadedFromDb(); + + return $this; + } + + public function store() + { + $storedGroups = array_keys($this->storedGroups); + $groups = array_keys($this->groups); + + $objectId = $this->object->id; + $type = $this->getType(); + + $objectCol = $type . '_id'; + $groupCol = $type . 'group_id'; + + $toDelete = array_diff($storedGroups, $groups); + foreach ($toDelete as $group) { + $where = sprintf( + $objectCol . ' = %d AND ' . $groupCol . ' = %d', + $objectId, + $this->storedGroups[$group]->id + ); + + $this->object->db->delete( + $this->getGroupMemberTableName(), + $where + ); + } + + $toAdd = array_diff($groups, $storedGroups); + foreach ($toAdd as $group) { + $this->object->db->insert( + $this->getGroupMemberTableName(), + array( + $objectCol => $objectId, + $groupCol => $this->groups[$group]->id + ) + ); + } + $this->setBeingLoadedFromDb(); + + return true; + } + + public function setBeingLoadedFromDb() + { + $this->storedGroups = array(); + foreach ($this->groups as $k => $v) { + $this->storedGroups[$k] = clone($v); + $this->storedGroups[$k]->id = $v->id; + } + + $this->modified = false; + } + + protected function getGroupClass() + { + return __NAMESPACE__ . '\\Icinga' .ucfirst($this->object->getShortTableName()) . 'Group'; + } + + public static function loadForStoredObject(IcingaObject $object) + { + $groups = new static($object); + + if (PrefetchCache::shouldBeUsed()) { + $groups->groups = PrefetchCache::instance()->groups($object); + $groups->setBeingLoadedFromDb(); + } else { + $groups->loadFromDb(); + } + + return $groups; + } + + public function toConfigString() + { + $groups = array_keys($this->groups); + + if (empty($groups)) { + return ''; + } + + return c::renderKeyValue('groups', c::renderArray($groups)); + } + + public function toLegacyConfigString($additionalGroups = array()) + { + $groups = array_merge(array_keys($this->groups), $additionalGroups); + $groups = array_unique($groups); + + if (empty($groups)) { + return ''; + } + + $type = $this->object->getLegacyObjectType(); + return c1::renderKeyValue($type.'groups', c1::renderArray($groups)); + } + + public function __toString() + { + try { + return $this->toConfigString(); + } catch (Exception $e) { + trigger_error($e); + $previousHandler = set_exception_handler( + function () { + } + ); + restore_error_handler(); + if ($previousHandler !== null) { + call_user_func($previousHandler, $e); + die(); + } else { + die($e->getMessage()); + } + } + } + + public function __destruct() + { + unset($this->storedGroups); + unset($this->groups); + unset($this->object); + } +} diff --git a/library/Director/Objects/IcingaObjectImports.php b/library/Director/Objects/IcingaObjectImports.php new file mode 100644 index 0000000..384fa1c --- /dev/null +++ b/library/Director/Objects/IcingaObjectImports.php @@ -0,0 +1,439 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Countable; +use Exception; +use Icinga\Exception\NotFoundError; +use Iterator; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; +use Icinga\Module\Director\Repository\IcingaTemplateRepository; +use RuntimeException; + +class IcingaObjectImports implements Iterator, Countable, IcingaConfigRenderer +{ + protected $storedNames = []; + + /** @var array A list of our imports, key and value are the import name */ + protected $imports = []; + + /** @var IcingaObject[] A list of all objects we have seen, referred by name */ + protected $objects = []; + + protected $modified = false; + + /** @var IcingaObject The parent object */ + protected $object; + + private $position = 0; + + protected $idx = []; + + public function __construct(IcingaObject $object) + { + $this->object = $object; + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->imports); + } + + #[\ReturnTypeWillChange] + public function rewind() + { + $this->position = 0; + } + + public function setModified() + { + $this->modified = true; + return $this; + } + + public function hasBeenModified() + { + return $this->modified; + } + + /** + * @return IcingaObject|null + * @throws \Icinga\Exception\NotFoundError + */ + #[\ReturnTypeWillChange] + public function current() + { + if (! $this->valid()) { + return null; + } + + return $this->getObject( + $this->imports[$this->idx[$this->position]] + ); + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->idx[$this->position]; + } + + #[\ReturnTypeWillChange] + public function next() + { + ++$this->position; + } + + #[\ReturnTypeWillChange] + public function valid() + { + return array_key_exists($this->position, $this->idx); + } + + /** + * @param $key + * @return IcingaObject|null + * @throws \Icinga\Exception\NotFoundError + */ + public function get($key) + { + if (array_key_exists($key, $this->imports)) { + return $this->getObject($this->imports[$key]); + } + + return null; + } + + public function set($import) + { + if (empty($import)) { + if (empty($this->imports)) { + return $this; + } else { + return $this->clear(); + } + } + + if (! is_array($import)) { + $import = [$import]; + } + + $existing = $this->listImportNames(); + $new = $this->listNamesForGivenImports($import); + + if ($existing === $new) { + return $this; + } + + $this->imports = []; + return $this->add($import); + } + + protected function listNamesForGivenImports($imports) + { + $list = []; + $class = $this->getImportClass(); + + foreach ($imports as $i) { + if ($i instanceof $class) { + $list[] = $i->object_name; + } else { + $list[] = $i; + } + } + + return $list; + } + + /** + * Magic isset check + * + * @param string $import + * + * @return boolean + */ + public function __isset($import) + { + return array_key_exists($import, $this->imports); + } + + public function clear() + { + if ($this->imports === []) { + return $this; + } + + $this->imports = []; + $this->modified = true; + + return $this->refreshIndex(); + } + + public function remove($import) + { + if (array_key_exists($import, $this->imports)) { + unset($this->imports[$import]); + } + + $this->modified = true; + + return $this->refreshIndex(); + } + + protected function refreshIndex() + { + $this->idx = array_keys($this->imports); + // $this->object->templateResolver()->refreshObject($this->object); + + return $this; + } + + public function add($import) + { + $class = $this->getImportClass(); + + if (is_array($import)) { + foreach ($import as $i) { + // Gracefully ignore null members or empty strings + if (! $i instanceof $class && ($i === null || strlen($i) === 0)) { + continue; + } + + $this->add($i); + } + + return $this; + } + + if ($import instanceof $class) { + $name = $import->object_name; + if (array_key_exists($name, $this->imports)) { + return $this; + } + + $this->imports[$name] = $name; + $this->objects[$name] = $import; + } elseif (is_string($import)) { + if (array_key_exists($import, $this->imports)) { + return $this; + } + + $this->imports[$import] = $import; + } + + $this->modified = true; + $this->refreshIndex(); + + return $this; + } + + /** + * @return IcingaObject[] + * @throws \Icinga\Exception\NotFoundError + */ + public function getObjects() + { + $list = []; + foreach ($this->listImportNames() as $name) { + $name = (string) $name; + $list[$name] = $this->getObject($name); + } + + return $list; + } + + /** + * @param $name + * @return IcingaObject + * @throws \Icinga\Exception\NotFoundError + */ + protected function getObject($name) + { + if (array_key_exists($name, $this->objects)) { + return $this->objects[$name]; + } + + $connection = $this->object->getConnection(); + /** @var IcingaObject $class */ + $class = $this->getImportClass(); + try { + if (is_array($this->object->getKeyName())) { + // Services only + $import = $class::load([ + 'object_name' => $name, + 'object_type' => 'template' + ], $connection); + } else { + $import = $class::load($name, $connection); + } + } catch (NotFoundError $e) { + throw new NotFoundError(sprintf( + 'Unable to load parent referenced from %s "%s", %s', + $this->object->getShortTableName(), + $this->object->getObjectName(), + lcfirst($e->getMessage()) + ), $e->getCode(), $e); + } + + return $this->objects[$import->getObjectName()] = $import; + } + + protected function getImportTableName() + { + return $this->object->getTableName() . '_inheritance'; + } + + public function listImportNames() + { + return array_keys($this->imports); + } + + public function listOriginalImportNames() + { + return $this->storedNames; + } + + public function getType() + { + return $this->object->getShortTableName(); + } + + protected function loadFromDb() + { + // $resolver = $this->object->templateResolver(); + // $this->objects = $resolver->fetchParents(); + $this->objects = IcingaTemplateRepository::instanceByObject($this->object) + ->getTemplatesIndexedByNameFor($this->object); + if (empty($this->objects)) { + $this->imports = []; + } else { + $keys = array_keys($this->objects); + $this->imports = array_combine($keys, $keys); + } + + $this->setBeingLoadedFromDb(); + return $this; + } + + /** + * @return bool + * @throws \Zend_Db_Adapter_Exception + * @throws \Icinga\Exception\NotFoundError + */ + public function store() + { + if (! $this->hasBeenModified()) { + return true; + } + + $objectId = $this->object->get('id'); + if ($objectId === null) { + throw new RuntimeException( + 'Cannot store imports for unstored object with no ID' + ); + } else { + $objectId = (int) $objectId; + } + + $type = $this->getType(); + + $objectCol = $type . '_id'; + $importCol = 'parent_' . $type . '_id'; + $table = $this->getImportTableName(); + $db = $this->object->getDb(); + + if ($this->object->hasBeenLoadedFromDb()) { + $db->delete( + $table, + $objectCol . ' = ' . $objectId + ); + } + + $weight = 1; + foreach ($this->getObjects() as $import) { + $db->insert($table, [ + $objectCol => $objectId, + $importCol => $import->get('id'), + 'weight' => $weight++ + ]); + } + + $this->setBeingLoadedFromDb(); + + return true; + } + + public function setBeingLoadedFromDb() + { + $this->storedNames = $this->listImportNames(); + $this->modified = false; + } + + protected function getImportClass() + { + return get_class($this->object); + } + + public static function loadForStoredObject(IcingaObject $object) + { + $obj = new static($object); + return $obj->loadFromDb(); + } + + public function toConfigString() + { + $ret = ''; + + foreach ($this->listImportNames() as $name) { + $ret .= ' import ' . c::renderString($name) . "\n"; + } + + if ($ret !== '') { + $ret .= "\n"; + } + return $ret; + } + + public function toLegacyConfigString() + { + $ret = ''; + + foreach ($this->listImportNames() as $name) { + $ret .= c1::renderKeyValue('use', c1::renderString($name)) . "\n"; + } + + if ($ret !== '') { + $ret .= "\n"; + } + return $ret; + } + + public function __toString() + { + try { + return $this->toConfigString(); + } catch (Exception $e) { + trigger_error($e); + $previousHandler = set_exception_handler( + function () { + } + ); + restore_error_handler(); + if ($previousHandler !== null) { + call_user_func($previousHandler, $e); + die(); + } else { + die($e->getMessage()); + } + } + } + + public function __destruct() + { + unset($this->object); + unset($this->objects); + } +} diff --git a/library/Director/Objects/IcingaObjectLegacyAssignments.php b/library/Director/Objects/IcingaObjectLegacyAssignments.php new file mode 100644 index 0000000..6ab75c8 --- /dev/null +++ b/library/Director/Objects/IcingaObjectLegacyAssignments.php @@ -0,0 +1,79 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use LogicException; + +/** + * This class is required for historical reasons + * + * Objects with assignments in your activity log would otherwise not be able + * to render themselves + */ +class IcingaObjectLegacyAssignments +{ + public static function applyToObject(IcingaObject $object, $values) + { + if (! $object->supportsAssignments()) { + throw new LogicException(sprintf( + 'I can only assign for applied objects, got %s', + $object->object_type + )); + } + + if ($values === null) { + return $object; + } + + if (! is_array($values)) { + static::throwCompatError(); + } + + if (empty($values)) { + return $object; + } + + $assigns = array(); + $ignores = array(); + foreach ($values as $type => $value) { + if (strpos($value, '|') !== false || strpos($value, '&' !== false)) { + $value = '(' . $value . ')'; + } + + if ($type === 'assign') { + $assigns[] = $value; + } elseif ($type === 'ignore') { + $ignores[] = $value; + } else { + static::throwCompatError(); + } + } + + $assign = implode('|', $assigns); + $ignore = implode('&', $ignores); + if (empty($assign)) { + $filter = $ignore; + } elseif (empty($ignore)) { + $filter = $assign; + } else { + if (count($assigns) === 1) { + $filter = $assign . '&' . $ignore; + } else { + $filter = '(' . $assign . ')&(' . $ignore . ')'; + } + } + + $object->assign_filter = $filter; + + return $object; + } + + protected static function throwCompatError() + { + throw new LogicException( + 'You ran into an unexpected compatibility issue. Please report' + . ' this with details helping us to reproduce this to the' + . ' Icinga project' + ); + } +} diff --git a/library/Director/Objects/IcingaObjectMultiRelations.php b/library/Director/Objects/IcingaObjectMultiRelations.php new file mode 100644 index 0000000..a1ec9a2 --- /dev/null +++ b/library/Director/Objects/IcingaObjectMultiRelations.php @@ -0,0 +1,454 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Exception; +use Icinga\Exception\ProgrammingError; +use Iterator; +use Countable; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; + +class IcingaObjectMultiRelations implements Iterator, Countable, IcingaConfigRenderer +{ + protected $stored = array(); + + protected $relations = array(); + + protected $modified = false; + + protected $object; + + protected $propertyName; + + protected $relatedObjectClass; + + protected $relatedTableName; + + protected $relationIdColumn; + + protected $relatedShortName; + + protected $legacyPropertyName; + + private $position = 0; + + private $db; + + protected $idx = array(); + + public function __construct(IcingaObject $object, $propertyName, $config) + { + $this->object = $object; + $this->propertyName = $propertyName; + + if (is_object($config) || is_array($config)) { + foreach ($config as $k => $v) { + $this->$k = $v; + } + } else { + $this->relatedObjectClass = $config; + } + } + + public function getObjects() + { + return $this->relations; + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->relations); + } + + #[\ReturnTypeWillChange] + public function rewind() + { + $this->position = 0; + } + + public function hasBeenModified() + { + return $this->modified; + } + + #[\ReturnTypeWillChange] + public function current() + { + if (! $this->valid()) { + return null; + } + + return $this->relations[$this->idx[$this->position]]; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->idx[$this->position]; + } + + #[\ReturnTypeWillChange] + public function next() + { + ++$this->position; + } + + #[\ReturnTypeWillChange] + public function valid() + { + return array_key_exists($this->position, $this->idx); + } + + public function get($key) + { + if (array_key_exists($key, $this->relations)) { + return $this->relations[$key]; + } + + return null; + } + + public function set($relation) + { + if (! is_array($relation)) { + if ($relation === null) { + $relation = array(); + } else { + $relation = array($relation); + } + } + + $existing = array_keys($this->relations); + $new = array(); + $class = $this->getRelatedClassName(); + $unset = array(); + + foreach ($relation as $k => $ro) { + if ($ro instanceof $class) { + $new[] = $ro->object_name; + } else { + if (empty($ro)) { + $unset[] = $k; + continue; + } + + $new[] = $ro; + } + } + + foreach ($unset as $k) { + unset($relation[$k]); + } + + sort($existing); + sort($new); + if ($existing === $new) { + return $this; + } + + $this->relations = array(); + if (empty($relation)) { + $this->modified = true; + $this->refreshIndex(); + return $this; + } + + return $this->add($relation); + } + + /** + * Magic isset check + * + * @return boolean + */ + public function __isset($relation) + { + return array_key_exists($relation, $this->relations); + } + + public function remove($relation) + { + if (array_key_exists($relation, $this->relations)) { + unset($this->relations[$relation]); + } + + $this->modified = true; + $this->refreshIndex(); + } + + protected function refreshIndex() + { + ksort($this->relations); + $this->idx = array_keys($this->relations); + } + + public function add($relation, $onError = 'fail') + { + // TODO: only one query when adding array + if (is_array($relation)) { + foreach ($relation as $r) { + $this->add($r, $onError); + } + return $this; + } + + if (array_key_exists($relation, $this->relations)) { + return $this; + } + + $class = $this->getRelatedClassName(); + + if ($relation instanceof $class) { + $this->relations[$relation->object_name] = $relation; + } elseif (is_string($relation)) { + $connection = $this->object->getConnection(); + try { + // Related services can only be objects, used by ServiceSets + if ($class === 'Icinga\\Module\\Director\\Objects\\IcingaService') { + $relation = $class::load(array( + 'object_name' => $relation, + 'object_type' => 'template' + ), $connection); + } else { + $relation = $class::load($relation, $connection); + } + } catch (Exception $e) { + switch ($onError) { + case 'autocreate': + $relation = $class::create(array( + 'object_type' => 'object', + 'object_name' => $relation + )); + $relation->store($connection); + // TODO + case 'fail': + throw new ProgrammingError( + 'The related %s "%s" doesn\'t exists: %s', + $this->getRelatedTableName(), + $relation, + $e->getMessage() + ); + break; + case 'ignore': + return $this; + } + } + } else { + throw new ProgrammingError( + 'Invalid related object: %s', + var_export($relation, 1) + ); + } + + $this->relations[$relation->object_name] = $relation; + $this->modified = true; + $this->refreshIndex(); + + return $this; + } + + protected function getPropertyName() + { + return $this->propertyName; + } + + protected function getRelatedShortName() + { + if ($this->relatedShortName === null) { + /** @var IcingaObject $class */ + $class = $this->getRelatedClassName(); + $this->relatedShortName = $class::create()->getShortTableName(); + } + + return $this->relatedShortName; + } + + protected function getTableName() + { + return $this->object->getTableName() . '_' . $this->getRelatedShortName(); + } + + protected function getRelatedTableName() + { + if ($this->relatedTableName === null) { + /** @var IcingaObject $class */ + $class = $this->getRelatedClassName(); + $this->relatedTableName = $class::create()->getTableName(); + } + + return $this->relatedTableName; + } + + protected function getRelationIdColumn() + { + if ($this->relationIdColumn === null) { + $this->relationIdColumn = $this->getRelatedShortName(); + } + + return $this->relationIdColumn; + } + + public function listRelatedNames() + { + return array_keys($this->relations); + } + + public function listOriginalNames() + { + return array_keys($this->stored); + } + + public function getType() + { + return $this->object->getShortTableName(); + } + + protected function loadFromDb() + { + $db = $this->getDb(); + $connection = $this->object->getConnection(); + + $type = $this->getType(); + $objectIdCol = $type . '_id'; + $relationIdCol = $this->getRelationIdColumn() . '_id'; + + $query = $db->select()->from( + array('r' => $this->getTableName()), + array() + )->join( + array('ro' => $this->getRelatedTableName()), + sprintf('r.%s = ro.id', $relationIdCol), + '*' + )->where( + sprintf('r.%s = ?', $objectIdCol), + (int) $this->object->id + )->order('ro.object_name'); + + $class = $this->getRelatedClassName(); + $this->relations = $class::loadAll($connection, $query, 'object_name'); + $this->setBeingLoadedFromDb(); + + return $this; + } + + public function store() + { + $db = $this->getDb(); + $stored = array_keys($this->stored); + $relations = array_keys($this->relations); + + $objectId = $this->object->id; + $type = $this->getType(); + $objectCol = $type . '_id'; + $relationCol = $this->getRelationIdColumn() . '_id'; + + $toDelete = array_diff($stored, $relations); + foreach ($toDelete as $relation) { + // We work with cloned objects. (why?) + // As __clone drops the id, we need to access original properties + $orig = $this->stored[$relation]->getOriginalProperties(); + $where = sprintf( + $objectCol . ' = %d AND ' . $relationCol . ' = %d', + $objectId, + $orig['id'] + ); + + $db->delete( + $this->getTableName(), + $where + ); + } + + $toAdd = array_diff($relations, $stored); + foreach ($toAdd as $related) { + $db->insert( + $this->getTableName(), + array( + $objectCol => $objectId, + $relationCol => $this->relations[$related]->id + ) + ); + } + $this->setBeingLoadedFromDb(); + + return true; + } + + public function setBeingLoadedFromDb() + { + $this->stored = array(); + foreach ($this->relations as $k => $v) { + $this->stored[$k] = clone($v); + } + } + + protected function getRelatedClassName() + { + return __NAMESPACE__ . '\\' . $this->relatedObjectClass; + } + + protected function getDb() + { + if ($this->db === null) { + $this->db = $this->object->getDb(); + } + + return $this->db; + } + + public static function loadForStoredObject(IcingaObject $object, $propertyName, $relatedObjectClass) + { + $relations = new static($object, $propertyName, $relatedObjectClass); + return $relations->loadFromDb(); + } + + public function toConfigString() + { + $relations = array_keys($this->relations); + + if (empty($relations)) { + return ''; + } + + return c::renderKeyValue($this->propertyName, c::renderArray($relations)); + } + + public function __toString() + { + try { + return $this->toConfigString(); + } catch (Exception $e) { + trigger_error($e); + $previousHandler = set_exception_handler( + function () { + } + ); + restore_error_handler(); + if ($previousHandler !== null) { + call_user_func($previousHandler, $e); + die(); + } else { + die($e->getMessage()); + } + } + } + + public function toLegacyConfigString() + { + $relations = array_keys($this->relations); + + if (empty($relations)) { + return ''; + } + + if ($this->legacyPropertyName === null) { + return ' # not supported in legacy: ' . + c1::renderKeyValue($this->propertyName, c1::renderArray($relations), ''); + } + + return c1::renderKeyValue($this->legacyPropertyName, c1::renderArray($relations)); + } +} diff --git a/library/Director/Objects/IcingaRanges.php b/library/Director/Objects/IcingaRanges.php new file mode 100644 index 0000000..c14c588 --- /dev/null +++ b/library/Director/Objects/IcingaRanges.php @@ -0,0 +1,321 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Exception; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; + +abstract class IcingaRanges +{ + /** @var IcingaTimePeriodRange[]|IcingaScheduledDowntimeRange[] */ + protected $storedRanges = []; + + /** @var IcingaTimePeriodRange[]|IcingaScheduledDowntimeRange[] */ + protected $ranges = []; + + protected $modified = false; + + protected $object; + + private $position = 0; + + protected $idx = array(); + + public function __construct(IcingaObject $object) + { + $this->object = $object; + } + + #[\ReturnTypeWillChange] + public function count() + { + return count($this->ranges); + } + + #[\ReturnTypeWillChange] + public function rewind() + { + $this->position = 0; + } + + public function hasBeenModified() + { + return $this->modified; + } + + #[\ReturnTypeWillChange] + public function current() + { + if (! $this->valid()) { + return null; + } + + return $this->ranges[$this->idx[$this->position]]; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->idx[$this->position]; + } + + #[\ReturnTypeWillChange] + public function next() + { + ++$this->position; + } + + #[\ReturnTypeWillChange] + public function valid() + { + return array_key_exists($this->position, $this->idx); + } + + public function get($key) + { + if (array_key_exists($key, $this->ranges)) { + return $this->ranges[$key]; + } + + return null; + } + + public function getValues() + { + $res = array(); + foreach ($this->ranges as $key => $range) { + $res[$key] = $range->range_value; + } + + return (object) $res; + } + + public function getOriginalValues() + { + $res = array(); + foreach ($this->storedRanges as $key => $range) { + $res[$key] = $range->range_value; + } + + return (object) $res; + } + + public function getRanges() + { + return $this->ranges; + } + + protected function modify($range, $value) + { + $this->ranges[$range]->range_key = $value; + } + + public function set($ranges) + { + foreach ($ranges as $range => $value) { + $this->setRange($range, $value); + } + + $toDelete = array_diff(array_keys($this->ranges), array_keys($ranges)); + foreach ($toDelete as $range) { + $this->remove($range); + } + + return $this; + } + + public function setRange($range, $value) + { + if ($value === null && array_key_exists($range, $this->ranges)) { + $this->remove($range); + return $this; + } + + if (array_key_exists($range, $this->ranges)) { + if ($this->ranges[$range]->range_value === $value) { + return $this; + } else { + $this->ranges[$range]->range_value = $value; + $this->modified = true; + } + } else { + $class = $this->getRangeClass(); + $this->ranges[$range] = $class::create([ + $this->objectIdColumn => $this->object->get('id'), + 'range_key' => $range, + 'range_value' => $value, + ]); + $this->modified = true; + } + + return $this; + } + + /** + * Magic isset check + * + * @return boolean + */ + public function __isset($range) + { + return array_key_exists($range, $this->ranges); + } + + public function remove($range) + { + if (array_key_exists($range, $this->ranges)) { + unset($this->ranges[$range]); + } + + $this->modified = true; + $this->refreshIndex(); + } + + public function clear() + { + $this->ranges = []; + $this->modified = true; + $this->refreshIndex(); + } + + protected function refreshIndex() + { + ksort($this->ranges); + $this->idx = array_keys($this->ranges); + } + + public function listRangesNames() + { + return array_keys($this->ranges); + } + + public function getType() + { + return $this->object->getShortTableName(); + } + + public function getRangeTableName() + { + return $this->object->getTableName() . '_range'; + } + + protected function loadFromDb() + { + $db = $this->object->getDb(); + $connection = $this->object->getConnection(); + + $table = $this->getRangeTableName(); + + $query = $db->select() + ->from(['o' => $table]) + ->where('o.' . $this->objectIdColumn . ' = ?', (int) $this->object->get('id')) + ->order('o.range_key'); + + $class = $this->getRangeClass(); + $this->ranges = $class::loadAll($connection, $query, 'range_key'); + $this->setBeingLoadedFromDb(); + + return $this; + } + + public function setBeingLoadedFromDb() + { + $this->storedRanges = []; + + foreach ($this->ranges as $key => $range) { + $range->setBeingLoadedFromDb(); + $this->storedRanges[$key] = clone($range); + } + $this->refreshIndex(); + $this->modified = false; + } + + public function store() + { + $db = $this->object->getConnection(); + if (! $this->hasBeenModified()) { + return false; + } + + $table = $this->getRangeTableName(); + $objectId = (int) $this->object->get('id'); + $idColumn = $this->objectIdColumn; + foreach ($this->ranges as $range) { + if ($range->hasBeenModified()) { + $range->setConnection($db); + if ((int) $range->get($idColumn) !== $objectId) { + $range->set($idColumn, $objectId); + } + } + } + + foreach (array_diff(array_keys($this->storedRanges), array_keys($this->ranges)) as $delete) { + $range = $this->storedRanges[$delete]; + $range->setConnection($db); + $range->set($idColumn, $objectId); + $db->getDbAdapter()->delete($table, $range->createWhere()); + unset($this->ranges[$delete]); + } + foreach ($this->ranges as $range) { + $range->store(); + } + $this->setBeingLoadedFromDb(); + + return true; + } + + /** + * @return IcingaTimePeriodRange|IcingaScheduledDowntimeRange|string IDE hint + */ + protected function getRangeClass() + { + return $this->rangeClass; + } + + public static function loadForStoredObject(IcingaObject $object) + { + $ranges = new static($object); + return $ranges->loadFromDb(); + } + + public function toConfigString() + { + if (empty($this->ranges) && $this->object->object_type === 'template') { + return ''; + } + + $string = " ranges = {\n"; + + foreach ($this->ranges as $range) { + $string .= sprintf( + " %s\t= %s\n", + c::renderString($range->range_key), + c::renderString($range->range_value) + ); + } + + return $string . " }\n"; + } + + abstract public function toLegacyConfigString(); + + public function __toString() + { + try { + return $this->toConfigString(); + } catch (Exception $e) { + trigger_error($e); + $previousHandler = set_exception_handler( + function () { + } + ); + restore_error_handler(); + if ($previousHandler !== null) { + call_user_func($previousHandler, $e); + die(); + } else { + die($e->getMessage()); + } + } + } +} diff --git a/library/Director/Objects/IcingaRelatedObject.php b/library/Director/Objects/IcingaRelatedObject.php new file mode 100644 index 0000000..d35bcb0 --- /dev/null +++ b/library/Director/Objects/IcingaRelatedObject.php @@ -0,0 +1,211 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Objects\IcingaObject; + +/** + * Related Object + * + * This class comes in handy when working with simple foreign key references. In + * contrast to an ORM it helps to deal with lazy-loaded objects in a way allowing + * us to render objects with references which to no longer (or not yet) exist + */ +class IcingaRelatedObject +{ + /** @var IcingaObject Main object with (optional) relation */ + protected $owner; + + /** @var int Related object id */ + protected $id; + + /** @var int Related object name */ + protected $name; + + /** @var int Relation property name, e.g. 'host' */ + protected $key; + + /** @var int Relation property, e.g. 'host_id' */ + protected $idKey; + + /** @var IcingaObject Related object once loaded */ + protected $object; + + /** @var string Related class name */ + protected $className; + + /** + * IcingaRelatedObject constructor + * + * @param IcingaObject $owner Main object referring a related one + * @param string $key Main objects (short) property name for this + */ + public function __construct(IcingaObject $owner, $key) + { + $this->owner = $owner; + $this->key = $key; + $this->idKey = $key . '_id'; + } + + /** + * Set a specific id + * + * @param $id int + * + * @return self + */ + public function setId($id) + { + if (! is_int($id)) { + throw new ProgrammingError( + 'An id must be an integer' + ); + } + + if ($this->object !== null) { + if ($this->object->id === $id) { + return $this; + } else { + $this->object = null; + } + } + + if ($this->object === null) { + $this->name = null; + } + + $this->id = $id; + $this->owner->set($this->getRealPropertyName(), $id); + + return $this; + } + + /** + * Return the related objects id + * + * @return int + */ + public function getId() + { + if ($this->id === null) { + $this->id = $this->getObject()->id; + } + + return $this->id; + } + + /** + * Lazy-load the related object + * + * @return IcingaObject + */ + public function getObject() + { + // TODO: This is unfinished + + if ($this->object === null) { + $class = $this->getClassName(); + + if ($this->name === null) { + if ($id = $this->getId()) { + } + } else { + $this->object = $class::load($this->name, $this->owner->getConnection()); + } + } + return $this->object; + } + + /** + * The real property name pointing to this relation, e.g. 'host_id' + * + * @return string + */ + public function getRealPropertyName() + { + return $this->key . '_id'; + } + + /** + * Full related class name + * + * @return string + */ + public function getClassName() + { + if ($this->className === null) { + $this->className = __NAMESPACE__ . '\\' . $this->getShortClassName(); + } + + return $this->className; + } + + /** + * Related class name relative to Icinga\Module\Director\Objects + * + * @return string + */ + public function getShortClassName() + { + return $this->owner->getRelationObjectClass($this->key); + } + + /** + * Set a related property + * + * This might be a string or an object + * @param $related string|IcingaObject + * @throws ProgrammingError + * + * return self + */ + public function set($related) + { + if (is_string($related)) { + $this->name = $related; + } elseif (is_object($related)) { + $className = $this->getClassName(); + if ($related instanceof $className) { + $this->object = $related; + $this->name = $object->object_name; + $this->id = $object->id; + } else { + throw new ProgrammingError( + 'Trying to set a related "%s" while expecting "%s"', + get_class($related), + $this->getShortClassName() + ); + } + } else { + throw new ProgrammingError( + 'Related object can be name or object, got: %s', + var_export($related, 1) + ); + } + + return $this; + } + + /** + * Get the name of the related object + * + * @return string + */ + public function getName() + { + if ($this->name === null) { + return $this->owner->{$this->key}; + } else { + return $this->name; + } + } + + /** + * Conservative constructor to avoid issued with PHP GC + */ + public function __destruct() + { + unset($this->owner); + } +} diff --git a/library/Director/Objects/IcingaScheduledDowntime.php b/library/Director/Objects/IcingaScheduledDowntime.php new file mode 100644 index 0000000..7fc3f78 --- /dev/null +++ b/library/Director/Objects/IcingaScheduledDowntime.php @@ -0,0 +1,135 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use RuntimeException; + +class IcingaScheduledDowntime extends IcingaObject +{ + protected $table = 'icinga_scheduled_downtime'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'zone_id' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'author' => null, + 'comment' => null, + 'fixed' => null, + 'duration' => null, + 'apply_to' => null, + 'assign_filter' => null, + 'with_services' => null, + ]; + + protected $uuidColumn = 'uuid'; + + protected $supportsImports = true; + + protected $supportsRanges = true; + + protected $supportsApplyRules = true; + + protected $relations = [ + 'zone' => 'IcingaZone', + ]; + + protected $booleans = [ + 'fixed' => 'fixed', + ]; + + protected $intervalProperties = [ + 'duration' => 'duration', + ]; + + protected $propertiesNotForRendering = [ + 'id', + 'apply_to', + 'object_name', + 'object_type', + 'with_services', + ]; + + /** + * @return string + */ + protected function renderObjectHeader() + { + if ($this->isApplyRule()) { + if (($to = $this->get('apply_to')) === null) { + throw new RuntimeException(sprintf( + 'Applied notification "%s" has no valid object type', + $this->getObjectName() + )); + } + + return sprintf( + "%s %s %s to %s {\n", + $this->getObjectTypeName(), + $this->getType(), + c::renderString($this->getObjectName()), + ucfirst($to) + ); + } else { + return parent::renderObjectHeader(); + } + } + + public function getOnDeleteUrl() + { + if ($this->isApplyRule()) { + return 'director/scheduled-downtimes/applyrules'; + } elseif ($this->isTemplate()) { + return 'director/scheduled-downtimes/templates'; + } else { + return 'director/scheduled-downtimes'; + } + } + + public function isActive($now = null) + { + if ($now === null) { + $now = time(); + } + + foreach ($this->ranges()->getRanges() as $range) { + if ($range->isActive($now)) { + return true; + } + } + + // TODO: no range currently means (and renders) "never", Icinga behaves + // different. Figure out whether and how we should support this + return false; + } + + /** + * @return string + */ + protected function renderSuffix() + { + if ($this->get('with_services') === 'y' && $this->get('apply_to') === 'host') { + return parent::renderSuffix() . $this->renderCloneForServices(); + } else { + return parent::renderSuffix(); + } + } + + protected function prefersGlobalZone() + { + return false; + } + + protected function renderCloneForServices() + { + $services = clone($this); + $services + ->set('with_services', 'n') + ->set('apply_to', 'service'); + + return $services->toConfigString(); + } +} diff --git a/library/Director/Objects/IcingaScheduledDowntimeRange.php b/library/Director/Objects/IcingaScheduledDowntimeRange.php new file mode 100644 index 0000000..6280990 --- /dev/null +++ b/library/Director/Objects/IcingaScheduledDowntimeRange.php @@ -0,0 +1,88 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; + +class IcingaScheduledDowntimeRange extends DbObject +{ + protected $keyName = ['scheduled_downtime_id', 'range_key', 'range_type']; + + protected $table = 'icinga_scheduled_downtime_range'; + + protected $defaultProperties = [ + 'scheduled_downtime_id' => null, + 'range_key' => null, + 'range_value' => null, + 'range_type' => 'include', + 'merge_behaviour' => 'set', + ]; + + public function isActive($now = null) + { + if ($now === null) { + $now = time(); + } + + if (false === ($weekDay = $this->getWeekDay($this->get('range_key')))) { + // TODO, dates are not yet supported + return false; + } + + if ((int) date('w', $now) !== $weekDay) { + return false; + } + + $timeRanges = preg_split('/\s*,\s*/', $this->get('range_value'), -1, PREG_SPLIT_NO_EMPTY); + foreach ($timeRanges as $timeRange) { + if ($this->timeRangeIsActive($timeRange, $now)) { + return true; + } + } + + return false; + } + + protected function timeRangeIsActive($rangeString, $now) + { + $hBegin = $mBegin = $hEnd = $mEnd = null; + if (sscanf($rangeString, '%2d:%2d-%2d:%2d', $hBegin, $mBegin, $hEnd, $mEnd) === 4) { + if ($this->timeFromHourMin($hBegin, $mBegin, $now) <= $now + && $this->timeFromHourMin($hEnd, $mEnd, $now) >= $now + ) { + return true; + } + } else { + // TODO: throw exception? + } + + return false; + } + + protected function timeFromHourMin($hour, $min, $now) + { + return strtotime(sprintf('%s %02d:%02d:00', date('Y-m-d', $now), $hour, $min)); + } + + protected function getWeekDay($day) + { + switch ($day) { + case 'sunday': + return 0; + case 'monday': + return 1; + case 'tuesday': + return 2; + case 'wednesday': + return 3; + case 'thursday': + return 4; + case 'friday': + return 5; + case 'saturday': + return 6; + } + + return false; + } +} diff --git a/library/Director/Objects/IcingaScheduledDowntimeRanges.php b/library/Director/Objects/IcingaScheduledDowntimeRanges.php new file mode 100644 index 0000000..ac8483e --- /dev/null +++ b/library/Director/Objects/IcingaScheduledDowntimeRanges.php @@ -0,0 +1,18 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Countable; +use Iterator; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; + +class IcingaScheduledDowntimeRanges extends IcingaRanges implements Iterator, Countable, IcingaConfigRenderer +{ + protected $rangeClass = IcingaScheduledDowntimeRange::class; + protected $objectIdColumn = 'scheduled_downtime_id'; + + public function toLegacyConfigString() + { + return ''; + } +} diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php new file mode 100644 index 0000000..9479ef7 --- /dev/null +++ b/library/Director/Objects/IcingaService.php @@ -0,0 +1,828 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Data\Filter\Filter; +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\Data\PropertiesFilter; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; +use Icinga\Module\Director\Objects\Extension\FlappingSupport; +use Icinga\Module\Director\Resolver\HostServiceBlacklist; +use InvalidArgumentException; +use RuntimeException; + +class IcingaService extends IcingaObject implements ExportInterface +{ + use FlappingSupport; + + protected $table = 'icinga_service'; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'display_name' => null, + 'host_id' => null, + 'service_set_id' => null, + 'check_command_id' => null, + 'max_check_attempts' => null, + 'check_period_id' => null, + 'check_interval' => null, + 'retry_interval' => null, + 'check_timeout' => null, + 'enable_notifications' => null, + 'enable_active_checks' => null, + 'enable_passive_checks' => null, + 'enable_event_handler' => null, + 'enable_flapping' => null, + 'enable_perfdata' => null, + 'event_command_id' => null, + 'flapping_threshold_high' => null, + 'flapping_threshold_low' => null, + 'volatile' => null, + 'zone_id' => null, + 'command_endpoint_id' => null, + 'notes' => null, + 'notes_url' => null, + 'action_url' => null, + 'icon_image' => null, + 'icon_image_alt' => null, + 'use_agent' => null, + 'apply_for' => null, + 'use_var_overrides' => null, + 'assign_filter' => null, + 'template_choice_id' => null, + ]; + + protected $relations = [ + 'host' => 'IcingaHost', + 'service_set' => 'IcingaServiceSet', + 'check_command' => 'IcingaCommand', + 'event_command' => 'IcingaCommand', + 'check_period' => 'IcingaTimePeriod', + 'command_endpoint' => 'IcingaEndpoint', + 'zone' => 'IcingaZone', + 'template_choice' => 'IcingaTemplateChoiceService', + ]; + + protected $booleans = [ + 'enable_notifications' => 'enable_notifications', + 'enable_active_checks' => 'enable_active_checks', + 'enable_passive_checks' => 'enable_passive_checks', + 'enable_event_handler' => 'enable_event_handler', + 'enable_flapping' => 'enable_flapping', + 'enable_perfdata' => 'enable_perfdata', + 'volatile' => 'volatile', + 'use_agent' => 'use_agent', + 'use_var_overrides' => 'use_var_overrides', + ]; + + protected $intervalProperties = [ + 'check_interval' => 'check_interval', + 'check_timeout' => 'check_timeout', + 'retry_interval' => 'retry_interval', + ]; + + protected $supportsGroups = true; + + protected $supportsCustomVars = true; + + protected $supportsFields = true; + + protected $supportsImports = true; + + protected $supportsApplyRules = true; + + protected $supportsSets = true; + + protected $supportsChoices = true; + + protected $supportedInLegacy = true; + + protected $keyName = ['host_id', 'service_set_id', 'object_name']; + + protected $prioritizedProperties = ['host_id']; + + protected $propertiesNotForRendering = [ + 'id', + 'object_name', + 'object_type', + 'apply_for' + ]; + + /** @var ServiceGroupMembershipResolver */ + protected $servicegroupMembershipResolver; + + /** + * @return IcingaCommand + * @throws IcingaException + * @throws \Icinga\Exception\NotFoundError + */ + public function getCheckCommand() + { + $id = $this->getSingleResolvedProperty('check_command_id'); + return IcingaCommand::loadWithAutoIncId( + $id, + $this->getConnection() + ); + } + + /** + * @return bool + */ + public function isApplyRule() + { + if ($this->hasBeenAssignedToHostTemplate()) { + return true; + } + + return $this->hasProperty('object_type') + && $this->get('object_type') === 'apply'; + } + + /** + * @return bool + */ + public function usesVarOverrides() + { + return $this->get('use_var_overrides') === 'y'; + } + + public function getUniqueIdentifier() + { + if ($this->isTemplate()) { + return $this->getObjectName(); + } else { + throw new RuntimeException( + 'getUniqueIdentifier() is supported by Service Templates only' + ); + } + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + // TODO: ksort in toPlainObject? + $props = (array) $this->toPlainObject(); + $props['fields'] = $this->loadFieldReferences(); + ksort($props); + + return (object) $props; + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaService + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + $name = $properties['object_name']; + if ($properties['object_type'] !== 'template') { + throw new InvalidArgumentException(sprintf( + 'Can import only Templates, got "%s" for "%s"', + $properties['object_type'], + $name + )); + } + $key = [ + 'object_type' => 'template', + 'object_name' => $name + ]; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Service Template "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + // $object->newFields = $properties['fields']; + unset($properties['fields']); + $object->setProperties($properties); + + return $object; + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ + protected function loadFieldReferences() + { + $db = $this->getDb(); + + $res = $db->fetchAll( + $db->select()->from([ + 'sf' => 'icinga_service_field' + ], [ + 'sf.datafield_id', + 'sf.is_required', + 'sf.var_filter', + ])->join(['df' => 'director_datafield'], 'df.id = sf.datafield_id', []) + ->where('service_id = ?', $this->get('id')) + ->order('varname ASC') + ); + + if (empty($res)) { + return []; + } else { + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + + return $res; + } + } + + /** + * @param string $key + * @return $this + */ + protected function setKey($key) + { + if (is_int($key)) { + $this->set('id', $key); + } elseif (is_array($key)) { + foreach (['id', 'host_id', 'service_set_id', 'object_name'] as $k) { + if (array_key_exists($k, $key)) { + $this->set($k, $key[$k]); + } + } + } else { + parent::setKey($key); + } + + return $this; + } + + /** + * @param $name + * @return $this + * @codingStandardsIgnoreStart + */ + protected function setObject_Name($name) + { + // @codingStandardsIgnoreEnd + + if ($name === null && $this->isApplyRule()) { + $name = ''; + } + + return $this->reallySet('object_name', $name); + } + + /** + * Render host_id as host_name + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderHost_id() + { + if ($this->hasBeenAssignedToHostTemplate()) { + return ''; + } + + return $this->renderRelationProperty('host', $this->get('host_id'), 'host_name'); + } + + /** + * @codingStandardsIgnoreStart + */ + protected function renderLegacyHost_id($value) + { + // @codingStandardsIgnoreEnd + if (is_array($value)) { + $blacklisted = $this->getBlacklistedHostnames(); + $c = c1::renderKeyValue('host_name', c1::renderArray(array_diff($value, $blacklisted))); + + // blacklisted in this (zoned) scope? + $bl = array_intersect($blacklisted, $value); + if (! empty($bl)) { + $c .= c1::renderKeyValue('# ignored on', c1::renderArray($bl)); + } + + return $c; + } else { + return parent::renderLegacyHost_id($value); + } + } + + /** + * @param IcingaConfig $config + * @throws IcingaException + */ + public function renderToLegacyConfig(IcingaConfig $config) + { + if ($this->get('service_set_id') !== null) { + return; + } elseif ($this->isApplyRule()) { + $this->renderLegacyApplyToConfig($config); + } else { + parent::renderToLegacyConfig($config); + } + } + + /** + * @param IcingaConfig $config + * @throws IcingaException + */ + protected function renderLegacyApplyToConfig(IcingaConfig $config) + { + $conn = $this->getConnection(); + + $assign_filter = $this->get('assign_filter'); + $filter = Filter::fromQueryString($assign_filter); + $hostnames = HostApplyMatches::forFilter($filter, $conn); + + $this->set('object_type', 'object'); + + foreach ($this->mapHostsToZones($hostnames) as $zone => $names) { + $blacklisted = $this->getBlacklistedHostnames(); + $zoneNames = array_diff($names, $blacklisted); + + $disabled = []; + foreach ($zoneNames as $name) { + if (IcingaHost::load($name, $this->getConnection())->isDisabled()) { + $disabled[] = $name; + } + } + $zoneNames = array_diff($zoneNames, $disabled); + + if (empty($zoneNames)) { + continue; + } + + $this->set('host_id', $zoneNames); + + $config->configFile('director/' . $zone . '/service_apply', '.cfg') + ->addLegacyObject($this); + } + } + + /** + * @return string + */ + public function toLegacyConfigString() + { + if ($this->get('service_set_id') !== null) { + return ''; + } + + $str = parent::toLegacyConfigString(); + + if (! $this->isDisabled() + && $this->get('host_id') + && $this->getRelated('host')->isDisabled() + ) { + return "# --- This services host has been disabled ---\n" + . preg_replace('~^~m', '# ', trim($str)) + . "\n\n"; + } else { + return $str; + } + } + + /** + * @return string + */ + public function toConfigString() + { + if ($this->get('service_set_id')) { + return ''; + } + $str = parent::toConfigString(); + + if (! $this->isDisabled() + && $this->get('host_id') + && $this->getRelated('host')->isDisabled() + ) { + return "/* --- This services host has been disabled ---\n" + // Do not allow strings to break our comment + . str_replace('*/', "* /", $str) . "*/\n"; + } else { + return $str; + } + } + + /** + * @return string + */ + protected function renderObjectHeader() + { + if ($this->isApplyRule() + && !$this->hasBeenAssignedToHostTemplate() + && $this->get('apply_for') !== null + ) { + $name = $this->getObjectName(); + $extraName = ''; + + if (c::stringHasMacro($name)) { + $extraName = c::renderKeyValue('name', c::renderStringWithVariables($name)); + $name = ''; + } elseif ($name !== '') { + $name = ' ' . c::renderString($name); + } + + return sprintf( + "%s %s%s for (config in %s) {\n", + $this->getObjectTypeName(), + $this->getType(), + $name, + $this->get('apply_for') + ) . $extraName; + } + + return parent::renderObjectHeader(); + } + + /** + * @return string + */ + protected function getLegacyObjectKeyName() + { + if ($this->isTemplate()) { + return 'name'; + } else { + return 'service_description'; + } + } + + /** + * @return bool + */ + public function hasBeenAssignedToHostTemplate() + { + // Branches would fail + if ($this->properties['host_id'] === null) { + return null; + } + $hostId = $this->get('host_id'); + + return $hostId && $this->getRelatedObject( + 'host', + $hostId + )->isTemplate(); + } + + /** + * @return string + * @throws \Icinga\Exception\NotFoundError + * @throws \Icinga\Module\Director\Exception\NestingError + */ + protected function renderSuffix() + { + $suffix = ''; + if ($this->isApplyRule()) { + $zoneName = $this->getRenderingZone(); + if (!IcingaZone::zoneNameIsGlobal($zoneName, $this->connection)) { + $suffix .= c::renderKeyValue('zone', c::renderString($zoneName)); + } + } + + if ($this->isApplyRule() || $this->usesVarOverrides()) { + $suffix .= $this->renderImportHostVarOverrides(); + } + + return $suffix . parent::renderSuffix(); + } + + /** + * @return string + */ + protected function renderImportHostVarOverrides() + { + if (! $this->connection) { + throw new RuntimeException( + 'Cannot render services without an assigned DB connection' + ); + } + + return "\n import DirectorOverrideTemplate\n"; + } + + /** + * @return string + * @throws \Icinga\Exception\NotFoundError + */ + protected function renderCustomExtensions() + { + $output = ''; + + if ($this->hasBeenAssignedToHostTemplate()) { + // TODO: use assignment renderer? + $filter = sprintf( + 'assign where %s in host.templates', + c::renderString($this->get('host')) + ); + + $output .= "\n " . $filter . "\n"; + } + + $blacklist = $this->getBlacklistedHostnames(); + $blacklistedTemplates = []; + $blacklistedHosts = []; + foreach ($blacklist as $hostname) { + if (IcingaHost::load($hostname, $this->connection)->isTemplate()) { + $blacklistedTemplates[] = $hostname; + } else { + $blacklistedHosts[] = $hostname; + } + } + foreach ($blacklistedTemplates as $template) { + $output .= sprintf( + " ignore where %s in host.templates\n", + c::renderString($template) + ); + } + if (! empty($blacklistedHosts)) { + if (count($blacklistedHosts) === 1) { + $output .= sprintf( + " ignore where host.name == %s\n", + c::renderString($blacklistedHosts[0]) + ); + } else { + $output .= sprintf( + " ignore where host.name in %s\n", + c::renderArray($blacklistedHosts) + ); + } + } + + // A hand-crafted command endpoint overrides use_agent + if ($this->get('command_endpoint_id') !== null) { + return $output; + } + + if ($this->get('use_agent') === 'y') { + // When feature flag feature_custom_endpoint is enabled, render additional code + if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') { + return $output . " + // Set command_endpoint dynamically with Director + if (!host) { + var host = get_host(host_name) + } + if (host.vars._director_custom_endpoint_name) { + command_endpoint = host.vars._director_custom_endpoint_name + } else { + command_endpoint = host_name + } +"; + } else { + return $output . c::renderKeyValue('command_endpoint', 'host_name'); + } + } elseif ($this->get('use_agent') === 'n') { + return $output . c::renderKeyValue('command_endpoint', c::renderPhpValue(null)); + } else { + return $output; + } + } + + /** + * @return array + */ + public function getBlacklistedHostnames() + { + // Hint: if ($this->isApplyRule()) would be nice, but apply rules are + // not enough, one might want to blacklist single services from Sets + // assigned to single Hosts. + if (PrefetchCache::shouldBeUsed()) { + $lookup = PrefetchCache::instance()->hostServiceBlacklist(); + } else { + $lookup = new HostServiceBlacklist($this->getConnection()); + } + + return $lookup->getBlacklistedHostnamesForService($this); + } + + /** + * Do not render internal property + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderUse_agent() + { + return ''; + } + + public function renderUse_var_overrides() + { + return ''; + } + + protected function renderTemplate_choice_id() + { + return ''; + } + + protected function renderLegacyDisplay_Name() + { + // @codingStandardsIgnoreEnd + return c1::renderKeyValue('display_name', $this->get('display_name')); + } + + public function hasCheckCommand() + { + return $this->getSingleResolvedProperty('check_command_id') !== null; + } + + public function getOnDeleteUrl() + { + if ($this->get('host_id')) { + return 'director/host/services?name=' . rawurlencode($this->get('host')); + } elseif ($this->get('service_set_id')) { + return 'director/serviceset/services?name=' . rawurlencode($this->get('service_set')); + } else { + return parent::getOnDeleteUrl(); + } + } + + protected function getDefaultZone(IcingaConfig $config = null) + { + if ($this->get('host_id') === null) { + return parent::getDefaultZone(); + } else { + return $this->getRelatedObject('host', $this->get('host_id')) + ->getRenderingZone($config); + } + } + + /** + * @return string + */ + public function createWhere() + { + $where = parent::createWhere(); + if (! $this->hasBeenLoadedFromDb()) { + if (null === $this->get('service_set_id') + && null === $this->get('host_id') + && null === $this->get('id') + ) { + $where .= " AND object_type = 'template'"; + } + } + + return $where; + } + + + /** + * TODO: Duplicate code, clean this up, split it into multiple methods + * @param Db|null $connection + * @param string $prefix + * @param null $filter + * @return array + */ + public static function enumProperties( + Db $connection = null, + $prefix = '', + $filter = null + ) { + $serviceProperties = []; + if ($filter === null) { + $filter = new PropertiesFilter(); + } + $realProperties = static::create()->listProperties(); + sort($realProperties); + + if ($filter->match(PropertiesFilter::$SERVICE_PROPERTY, 'name')) { + $serviceProperties[$prefix . 'name'] = 'name'; + } + foreach ($realProperties as $prop) { + if (!$filter->match(PropertiesFilter::$SERVICE_PROPERTY, $prop)) { + continue; + } + + if (substr($prop, -3) === '_id') { + if ($prop === 'template_choice_id') { + continue; + } + $prop = substr($prop, 0, -3); + } + + $serviceProperties[$prefix . $prop] = $prop; + } + + $serviceVars = []; + + if ($connection !== null) { + foreach ($connection->fetchDistinctServiceVars() as $var) { + if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) { + if ($var->datatype) { + $serviceVars[$prefix . 'vars.' . $var->varname] = sprintf( + '%s (%s)', + $var->varname, + $var->caption + ); + } else { + $serviceVars[$prefix . 'vars.' . $var->varname] = $var->varname; + } + } + } + } + + //$properties['vars.*'] = 'Other custom variable'; + ksort($serviceVars); + + $props = mt('director', 'Service properties'); + $vars = mt('director', 'Custom variables'); + + $properties = []; + if (!empty($serviceProperties)) { + $properties[$props] = $serviceProperties; + $properties[$props][$prefix . 'groups'] = 'Groups'; + } + + if (!empty($serviceVars)) { + $properties[$vars] = $serviceVars; + } + + $hostProps = mt('director', 'Host properties'); + $hostVars = mt('director', 'Host Custom variables'); + + $hostProperties = IcingaHost::enumProperties($connection, 'host.'); + + if (array_key_exists($hostProps, $hostProperties)) { + $p = $hostProperties[$hostProps]; + if (!empty($p)) { + $properties[$hostProps] = $p; + } + } + + if (array_key_exists($vars, $hostProperties)) { + $p = $hostProperties[$vars]; + if (!empty($p)) { + $properties[$hostVars] = $p; + } + } + + return $properties; + } + + protected function beforeStore() + { + parent::beforeStore(); + if ($this->isObject() + && $this->get('service_set_id') === null + && $this->get('host_id') === null + ) { + throw new InvalidArgumentException( + 'Cannot store a Service object without a related host or set: ' . $this->getObjectName() + ); + } + } + + protected function notifyResolvers() + { + $resolver = $this->getServiceGroupMembershipResolver(); + $resolver->addObject($this); + $resolver->refreshDb(); + + return $this; + } + + protected function getServiceGroupMembershipResolver() + { + if ($this->servicegroupMembershipResolver === null) { + $this->servicegroupMembershipResolver = new ServiceGroupMembershipResolver( + $this->getConnection() + ); + } + + return $this->servicegroupMembershipResolver; + } + + public function setServiceGroupMembershipResolver(ServiceGroupMembershipResolver $resolver) + { + $this->servicegroupMembershipResolver = $resolver; + return $this; + } +} diff --git a/library/Director/Objects/IcingaServiceAssignment.php b/library/Director/Objects/IcingaServiceAssignment.php new file mode 100644 index 0000000..9910f7c --- /dev/null +++ b/library/Director/Objects/IcingaServiceAssignment.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaServiceAssignment extends IcingaObject +{ + protected $table = 'icinga_service_assignment'; + + protected $keyName = 'id'; + + protected $defaultProperties = array( + 'id' => null, + 'service_id' => null, + 'filter_string' => null, + ); + + protected $relations = array( + 'service' => 'IcingaService', + ); +} diff --git a/library/Director/Objects/IcingaServiceField.php b/library/Director/Objects/IcingaServiceField.php new file mode 100644 index 0000000..c43ec2d --- /dev/null +++ b/library/Director/Objects/IcingaServiceField.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaServiceField extends IcingaObjectField +{ + protected $keyName = array('service_id', 'datafield_id'); + + protected $table = 'icinga_service_field'; + + protected $defaultProperties = array( + 'service_id' => null, + 'datafield_id' => null, + 'is_required' => null, + 'var_filter' => null, + ); +} diff --git a/library/Director/Objects/IcingaServiceGroup.php b/library/Director/Objects/IcingaServiceGroup.php new file mode 100644 index 0000000..ae43ff3 --- /dev/null +++ b/library/Director/Objects/IcingaServiceGroup.php @@ -0,0 +1,42 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaServiceGroup extends IcingaObjectGroup +{ + protected $table = 'icinga_servicegroup'; + + /** @var ServiceGroupMembershipResolver */ + protected $servicegroupMembershipResolver; + + public function supportsAssignments() + { + return true; + } + + protected function getServiceGroupMembershipResolver() + { + if ($this->servicegroupMembershipResolver === null) { + $this->servicegroupMembershipResolver = new ServiceGroupMembershipResolver( + $this->getConnection() + ); + } + + return $this->servicegroupMembershipResolver; + } + + public function setServiceGroupMembershipResolver(ServiceGroupMembershipResolver $resolver) + { + $this->servicegroupMembershipResolver = $resolver; + return $this; + } + + protected function notifyResolvers() + { + $resolver = $this->getServiceGroupMembershipResolver(); + $resolver->addGroup($this); + $resolver->refreshDb(); + + return $this; + } +} diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php new file mode 100644 index 0000000..8217a59 --- /dev/null +++ b/library/Director/Objects/IcingaServiceSet.php @@ -0,0 +1,591 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Exception; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\Db\DbUtil; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Resolver\HostServiceBlacklist; +use InvalidArgumentException; +use Ramsey\Uuid\Uuid; +use RuntimeException; + +class IcingaServiceSet extends IcingaObject implements ExportInterface +{ + protected $table = 'icinga_service_set'; + + protected $defaultProperties = array( + 'id' => null, + 'uuid' => null, + 'host_id' => null, + 'object_name' => null, + 'object_type' => null, + 'description' => null, + 'assign_filter' => null, + ); + + protected $uuidColumn = 'uuid'; + + protected $keyName = array('host_id', 'object_name'); + + protected $supportsImports = true; + + protected $supportsCustomVars = true; + + protected $supportsApplyRules = true; + + protected $supportedInLegacy = true; + + protected $relations = array( + 'host' => 'IcingaHost', + ); + + public function isDisabled() + { + return false; + } + + public function supportsAssignments() + { + return true; + } + + protected function setKey($key) + { + if (is_int($key)) { + $this->set('id', $key); + } elseif (is_string($key)) { + $keyComponents = preg_split('~!~', $key); + if (count($keyComponents) === 1) { + $this->set('object_name', $keyComponents[0]); + $this->set('object_type', 'template'); + } else { + throw new InvalidArgumentException(sprintf( + 'Can not parse key: %s', + $key + )); + } + } else { + return parent::setKey($key); + } + + return $this; + } + + /** + * @return IcingaService[] + * @throws \Icinga\Exception\NotFoundError + */ + public function getServiceObjects() + { + // don't try to resolve services for unstored objects - as in getServiceObjectsForSet() + // also for diff in activity log + if ($this->get('id') === null) { + return []; + } + + if ($this->get('host_id')) { + $imports = $this->imports()->getObjects(); + if (empty($imports)) { + return array(); + } + $parent = array_shift($imports); + assert($parent instanceof IcingaServiceSet); + return $this->getServiceObjectsForSet($parent); + } else { + return $this->getServiceObjectsForSet($this); + } + } + + /** + * @param IcingaServiceSet $set + * @return IcingaService[] + * @throws \Icinga\Exception\NotFoundError + */ + protected function getServiceObjectsForSet(IcingaServiceSet $set) + { + $connection = $this->getConnection(); + if (self::$dbObjectStore !== null) { + $branchUuid = self::$dbObjectStore->getBranch()->getUuid(); + } else { + $branchUuid = null; + } + + $builder = new ServiceSetQueryBuilder($connection, $branchUuid); + return $builder->fetchServicesWithQuery($builder->selectServicesForSet($set)); + } + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @return object + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + if ($this->get('host_id')) { + $result = $this->exportSetOnHost(); + } else { + $result = $this->exportTemplate(); + } + + unset($result->uuid); + return $result; + } + + protected function exportSetOnHost() + { + // TODO. + throw new RuntimeException('Not yet'); + } + + /** + * @return object + * @deprecated + * @throws \Icinga\Exception\NotFoundError + */ + protected function exportTemplate() + { + $props = $this->getProperties(); + unset($props['id'], $props['host_id']); + $props['services'] = []; + foreach ($this->getServiceObjects() as $serviceObject) { + $props['services'][$serviceObject->getObjectName()] = $serviceObject->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'); + $serviceNames = []; + foreach ($services as $service) { + if (isset($service->fields)) { + unset($service->fields); + } + $name = $service->object_name; + $serviceNames[] = $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(); + } + } + + foreach ($existingServices as $existing) { + if (!in_array($existing->getObjectName(), $serviceNames)) { + $existing->delete(); + } + } + + return $object; + } + + public function beforeDelete() + { + // check if this is a template, or directly assigned to a host + if ($this->get('host_id') === null) { + // find all host sets and delete them + foreach ($this->fetchHostSets() as $set) { + $set->delete(); + } + } + + parent::beforeDelete(); + } + + /** + * @throws \Icinga\Exception\NotFoundError + */ + public function onDelete() + { + $hostId = $this->get('host_id'); + if ($hostId) { + $deleteIds = []; + foreach ($this->getServiceObjects() as $service) { + if ($idToDelete = $service->get('id')) { + $deleteIds[] = (int) $idToDelete; + } + } + + if (! empty($deleteIds)) { + $db = $this->getDb(); + $db->delete( + 'icinga_host_service_blacklist', + $db->quoteInto( + sprintf('host_id = %s AND service_id IN (?)', $hostId), + $deleteIds + ) + ); + } + } + + parent::onDelete(); + } + + /** + * @param IcingaConfig $config + * @throws \Icinga\Exception\NotFoundError + */ + public function renderToConfig(IcingaConfig $config) + { + // always print the header, so you have minimal info present + $file = $this->getConfigFileWithHeader($config); + + if ($this->get('assign_filter') === null && $this->isTemplate()) { + return; + } + + if ($config->isLegacy()) { + $this->renderToLegacyConfig($config); + return; + } + + $services = $this->getServiceObjects(); + if (empty($services)) { + return; + } + + // Loop over all services belonging to this set + // add our assign rules and then add the service to the config + // eventually clone them beforehand to not get into trouble with caches + // figure out whether we might need a zone property + foreach ($services as $service) { + if ($filter = $this->get('assign_filter')) { + $service->set('object_type', 'apply'); + $service->set('assign_filter', $filter); + } elseif ($hostId = $this->get('host_id')) { + $host = $this->getRelatedObject('host', $hostId)->getObjectName(); + if (in_array($host, $this->getBlacklistedHostnames($service))) { + continue; + } + $service->set('object_type', 'object'); + $service->set('use_var_overrides', 'y'); + $service->set('host_id', $hostId); + } else { + // Service set template without assign filter or host + continue; + } + + $this->copyVarsToService($service); + $file->addObject($service); + } + } + + /** + * @return array + */ + public function getBlacklistedHostnames($service) + { + // Hint: if ($this->isApplyRule()) would be nice, but apply rules are + // not enough, one might want to blacklist single services from Sets + // assigned to single Hosts. + if (PrefetchCache::shouldBeUsed()) { + $lookup = PrefetchCache::instance()->hostServiceBlacklist(); + } else { + $lookup = new HostServiceBlacklist($this->getConnection()); + } + + return $lookup->getBlacklistedHostnamesForService($service); + } + + protected function getConfigFileWithHeader(IcingaConfig $config) + { + $file = $config->configFile( + 'zones.d/' . $this->getRenderingZone($config) . '/servicesets' + ); + + $file->addContent($this->getConfigHeaderComment($config)); + return $file; + } + + protected function getConfigHeaderComment(IcingaConfig $config) + { + $name = $this->getObjectName(); + $assign = $this->get('assign_filter'); + + if ($config->isLegacy()) { + if ($assign !== null) { + return "## applied Service Set '${name}'\n\n"; + } else { + return "## Service Set '${name}' on this host\n\n"; + } + } else { + $comment = [ + "Service Set: ${name}", + ]; + + if (($host = $this->get('host')) !== null) { + $comment[] = 'on host ' . $host; + } + + if (($description = $this->get('description')) !== null) { + $comment[] = ''; + foreach (preg_split('~\\n~', $description) as $line) { + $comment[] = $line; + } + } + + if ($assign !== null) { + $comment[] = ''; + $comment[] = trim($this->renderAssign_Filter()); + } + + return "/**\n * " . join("\n * ", $comment) . "\n */\n\n"; + } + } + + public function copyVarsToService(IcingaService $service) + { + $serviceVars = $service->vars(); + + foreach ($this->vars() as $k => $var) { + $serviceVars->$k = $var; + } + + return $this; + } + + /** + * @param IcingaConfig $config + * @throws \Icinga\Exception\NotFoundError + */ + public function renderToLegacyConfig(IcingaConfig $config) + { + if ($this->get('assign_filter') === null && $this->isTemplate()) { + return; + } + + // evaluate my assign rules once, get related hosts + // Loop over all services belonging to this set + // generate every service with host_name host1,host2... -> not yet. And Zones? + + $conn = $this->getConnection(); + + // Delegating this to the service would look, but this way it's faster + if ($filter = $this->get('assign_filter')) { + $filter = Filter::fromQueryString($filter); + + $hostnames = HostApplyMatches::forFilter($filter, $conn); + } else { + $hostnames = array($this->getRelated('host')->getObjectName()); + } + + $blacklists = []; + + foreach ($this->mapHostsToZones($hostnames) as $zone => $names) { + $file = $config->configFile('director/' . $zone . '/servicesets', '.cfg'); + $file->addContent($this->getConfigHeaderComment($config)); + + foreach ($this->getServiceObjects() as $service) { + $object_name = $service->getObjectName(); + + if (! array_key_exists($object_name, $blacklists)) { + $blacklists[$object_name] = $service->getBlacklistedHostnames(); + } + + // check if all hosts in the zone ignore this service + $zoneNames = array_diff($names, $blacklists[$object_name]); + + $disabled = []; + foreach ($zoneNames as $name) { + if (IcingaHost::load($name, $this->getConnection())->isDisabled()) { + $disabled[] = $name; + } + } + $zoneNames = array_diff($zoneNames, $disabled); + + if (empty($zoneNames)) { + continue; + } + + $service->set('object_type', 'object'); + $service->set('host_id', $names); + + $this->copyVarsToService($service); + + $file->addLegacyObject($service); + } + } + } + + public function getRenderingZone(IcingaConfig $config = null) + { + if ($this->get('host_id') === null) { + if ($hostname = $this->get('host')) { + $host = IcingaHost::load($hostname, $this->getConnection()); + } else { + return $this->connection->getDefaultGlobalZoneName(); + } + } else { + $host = $this->getRelatedObject('host', $this->get('host_id')); + } + + return $host->getRenderingZone($config); + } + + public function createWhere() + { + $where = parent::createWhere(); + if (! $this->hasBeenLoadedFromDb()) { + if (null === $this->get('host_id') && null === $this->get('id')) { + $where .= " AND object_type = 'template'"; + } + } + + return $where; + } + + /** + * @return IcingaService[] + */ + public function fetchServices() + { + if ($store = self::$dbObjectStore) { + $uuid = $store->getBranch()->getUuid(); + } else { + $uuid = null; + } + $builder = new ServiceSetQueryBuilder($this->getConnection(), $uuid); + return $builder->fetchServicesWithQuery($builder->selectServicesForSet($this)); + } + + /** + * Fetch IcingaServiceSet that are based on this set and added to hosts directly + * + * @return IcingaServiceSet[] + */ + public function fetchHostSets() + { + $id = $this->get('id'); + if ($id === null) { + return []; + } + + $query = $this->db->select() + ->from( + ['o' => $this->table] + )->join( + ['ssi' => $this->table . '_inheritance'], + 'ssi.service_set_id = o.id', + [] + )->where( + 'ssi.parent_service_set_id = ?', + $id + ); + + return static::loadAll($this->connection, $query); + } + + /** + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + protected function beforeStore() + { + parent::beforeStore(); + + $name = $this->getObjectName(); + + if ($this->isObject() && $this->get('host_id') === null && $this->get('host') === null) { + throw new InvalidArgumentException( + 'A Service Set cannot be an object with no related host' + ); + } + // checking if template object_name is unique + // TODO: Move to IcingaObject + if (! $this->hasBeenLoadedFromDb() && $this->isTemplate() && static::exists($name, $this->connection)) { + throw new DuplicateKeyException( + '%s template "%s" already existing in database!', + $this->getType(), + $name + ); + } + } + + public function toSingleIcingaConfig() + { + $config = parent::toSingleIcingaConfig(); + + try { + foreach ($this->fetchHostSets() as $set) { + $set->renderToConfig($config); + } + } catch (Exception $e) { + $config->configFile( + 'failed-to-render' + )->prepend( + "/** Failed to render this Service Set **/\n" + . '/* ' . $e->getMessage() . ' */' + ); + } + + return $config; + } +} diff --git a/library/Director/Objects/IcingaServiceSetAssignment.php b/library/Director/Objects/IcingaServiceSetAssignment.php new file mode 100644 index 0000000..4a6ebbc --- /dev/null +++ b/library/Director/Objects/IcingaServiceSetAssignment.php @@ -0,0 +1,20 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaServiceSetAssignment extends IcingaObject +{ + protected $table = 'icinga_service_set_assignment'; + + protected $keyName = 'id'; + + protected $defaultProperties = array( + 'id' => null, + 'service_set_id' => null, + 'filter_string' => null, + ); + + protected $relations = array( + 'service_set' => 'IcingaServiceSet', + ); +} diff --git a/library/Director/Objects/IcingaServiceVar.php b/library/Director/Objects/IcingaServiceVar.php new file mode 100644 index 0000000..0b855b2 --- /dev/null +++ b/library/Director/Objects/IcingaServiceVar.php @@ -0,0 +1,29 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaServiceVar extends IcingaObject +{ + protected $keyName = array('service_id', 'varname'); + + protected $table = 'icinga_service_var'; + + protected $defaultProperties = array( + 'service_id' => null, + 'varname' => null, + 'varvalue' => null, + 'format' => null, + ); + + public function onInsert() + { + } + + public function onUpdate() + { + } + + public function onDelete() + { + } +} diff --git a/library/Director/Objects/IcingaTemplateChoice.php b/library/Director/Objects/IcingaTemplateChoice.php new file mode 100644 index 0000000..1a1be90 --- /dev/null +++ b/library/Director/Objects/IcingaTemplateChoice.php @@ -0,0 +1,321 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Web\Form\QuickForm; + +class IcingaTemplateChoice extends IcingaObject implements ExportInterface +{ + protected $objectTable; + + protected $defaultProperties = [ + 'id' => null, + 'object_name' => null, + 'description' => null, + 'min_required' => 0, + 'max_allowed' => 1, + 'required_template_id' => null, + 'allowed_roles' => null, + ]; + + private $choices; + + private $newChoices; + + public function getObjectShortTableName() + { + return substr(substr($this->table, 0, -16), 7); + } + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaTemplateChoice + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + if (isset($properties['originalId'])) { + unset($properties['originalId']); + } + $name = $properties['object_name']; + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Template Choice "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + $object->setProperties($properties); + + return $object; + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return array|object|\stdClass + */ + public function export() + { + $plain = (object) $this->getProperties(); + $plain->originalId = $plain->id; + unset($plain->id); + $requiredId = $plain->required_template_id; + unset($plain->required_template_id); + if ($requiredId) { + $db = $this->getDb(); + $query = $db->select() + ->from(['o' => $this->getObjectTableName()], 'o.object_name')->where("o.object_type = 'template'") + ->where('o.id = ?', $this->get('id')); + $plain->required_template = $db->fetchOne($query); + } + + $plain->members = array_values($this->getMembers()); + + return $plain; + } + + public function isMainChoice() + { + return $this->hasBeenLoadedFromDb() + && $this->connection->settings()->get('main_host_choice'); + } + + public function getObjectTableName() + { + return substr($this->table, 0, -16); + } + + /** + * @param QuickForm $form + * @param array $imports + * @param string $namePrefix + * @return \Zend_Form_Element + * @throws \Zend_Form_Exception + */ + public function createFormElement(QuickForm $form, $imports = [], $namePrefix = 'choice') + { + $required = $this->isRequired() && !$this->isTemplate(); + $type = $this->allowsMultipleChoices() ? 'multiselect' : 'select'; + $choices = $this->enumChoices(); + + $chosen = []; + foreach ($imports as $import) { + if (array_key_exists($import, $choices)) { + $chosen[] = $import; + } + } + + $attributes = [ + 'label' => $this->getObjectName(), + 'description' => $this->get('description'), + 'required' => $required, + 'ignore' => true, + 'value' => $chosen, + 'multiOptions' => $form->optionalEnum($choices), + 'class' => 'autosubmit' + ]; + + // unused + if ($type === 'extensibleSet') { + $attributes['sorted'] = true; + } + + $key = $namePrefix . $this->get('id'); + return $form->createElement($type, $key, $attributes); + } + + public function isRequired() + { + return (int) $this->get('min_required') > 0; + } + + public function allowsMultipleChoices() + { + return (int) $this->get('max_allowed') > 1; + } + + public function hasBeenModified() + { + if ($this->newChoices !== null && $this->choices !== $this->newChoices) { + return true; + } + + return parent::hasBeenModified(); + } + + public function getMembers() + { + return $this->enumChoices(); + } + + public function setMembers($members) + { + if (empty($members)) { + $this->newChoices = array(); + return $this; + } + $db = $this->getDb(); + $query = $db->select()->from( + ['o' => $this->getObjectTableName()], + ['o.id', 'o.object_name'] + )->where("o.object_type = 'template'") + ->where('o.object_name IN (?)', $members) + ->order('o.object_name'); + + $this->newChoices = $db->fetchPairs($query); + return $this; + } + + public function getChoices() + { + if ($this->newChoices !== null) { + return $this->newChoices; + } + + if ($this->choices === null) { + $this->choices = $this->fetchChoices(); + } + + return $this->choices; + } + + public function fetchChoices() + { + if ($this->hasBeenLoadedFromDb()) { + $db = $this->getDb(); + $query = $db->select()->from( + ['o' => $this->getObjectTableName()], + ['o.id', 'o.object_name'] + )->where("o.object_type = 'template'") + ->where('o.template_choice_id = ?', $this->get('id')) + ->order('o.object_name'); + + return $db->fetchPairs($query); + } else { + return []; + } + } + + public function enumChoices() + { + $choices = $this->getChoices(); + return array_combine($choices, $choices); + } + + /** + * @throws \Zend_Db_Adapter_Exception + */ + public function onStore() + { + parent::onStore(); + if ($this->newChoices !== $this->choices) { + $this->storeChoices(); + } + } + + /** + * @throws \Zend_Db_Adapter_Exception + */ + protected function storeChoices() + { + $id = $this->getProperty('id'); + $db = $this->getDb(); + $ids = array_keys($this->newChoices); + $table = $this->getObjectTableName(); + + if (empty($ids)) { + $db->update( + $table, + ['template_choice_id' => null], + $db->quoteInto( + sprintf('template_choice_id = %d', $id), + $ids + ) + ); + } else { + $db->update( + $table, + ['template_choice_id' => null], + $db->quoteInto( + sprintf('template_choice_id = %d AND id NOT IN (?)', $id), + $ids + ) + ); + $db->update( + $table, + ['template_choice_id' => $id], + $db->quoteInto('id IN (?)', $ids) + ); + } + } + + /** + * @param $roles + * @throws ProgrammingError + * @codingStandardsIgnoreStart + */ + public function setAllowed_roles($roles) + { + // @codingStandardsIgnoreEnd + $key = 'allowed_roles'; + if (is_array($roles)) { + $this->reallySet($key, json_encode($roles)); + } elseif (null === $roles) { + $this->reallySet($key, null); + } else { + throw new ProgrammingError( + 'Expected array or null for allowed_roles, got %s', + var_export($roles, 1) + ); + } + } + + /** + * @return array|null + * @codingStandardsIgnoreStart + */ + public function getAllowed_roles() + { + // @codingStandardsIgnoreEnd + + // Might be removed once all choice types have allowed_roles + if (! array_key_exists('allowed_roles', $this->properties)) { + return null; + } + + $roles = $this->getProperty('allowed_roles'); + if (is_string($roles)) { + return json_decode($roles); + } else { + return $roles; + } + } + + /** + * @param $type + * @codingStandardsIgnoreStart + */ + public function setObject_type($type) + { + // @codingStandardsIgnoreEnd + } +} diff --git a/library/Director/Objects/IcingaTemplateChoiceHost.php b/library/Director/Objects/IcingaTemplateChoiceHost.php new file mode 100644 index 0000000..10ddedd --- /dev/null +++ b/library/Director/Objects/IcingaTemplateChoiceHost.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaTemplateChoiceHost extends IcingaTemplateChoice +{ + protected $table = 'icinga_host_template_choice'; + + protected $objectTable = 'icinga_host'; + + protected $relations = array( + 'required_template' => 'IcingaHost', + ); +} diff --git a/library/Director/Objects/IcingaTemplateChoiceService.php b/library/Director/Objects/IcingaTemplateChoiceService.php new file mode 100644 index 0000000..5cdb43e --- /dev/null +++ b/library/Director/Objects/IcingaTemplateChoiceService.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaTemplateChoiceService extends IcingaTemplateChoice +{ + protected $table = 'icinga_service_template_choice'; + + protected $objectTable = 'icinga_service'; + + protected $relations = array( + 'required_template' => 'IcingaService', + ); +} diff --git a/library/Director/Objects/IcingaTemplateResolver.php b/library/Director/Objects/IcingaTemplateResolver.php new file mode 100644 index 0000000..61122a0 --- /dev/null +++ b/library/Director/Objects/IcingaTemplateResolver.php @@ -0,0 +1,479 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Application\Benchmark; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Exception\NestingError; + +// TODO: move the 'type' layer to another class +class IcingaTemplateResolver +{ + /** @var IcingaObject */ + protected $object; + + /** @var Db */ + protected $connection; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + protected $type; + + protected static $templates = array(); + + protected static $idIdx = array(); + + protected static $reverseIdIdx = array(); + + protected static $nameIdx = array(); + + protected static $idToName = array(); + + protected static $nameToId = array(); + + public function __construct(IcingaObject $object) + { + $this->setObject($object); + } + + /** + * Set a specific object for this resolver instance + */ + public function setObject(IcingaObject $object) + { + $this->object = $object; + $this->type = $object->getShortTableName(); + $this->table = $object->getTableName(); + $this->connection = $object->getConnection(); + $this->db = $this->connection->getDbAdapter(); + + return $this; + } + + /** + * Forget all template relation of the given object type + * + * @return self + */ + public function clearCache() + { + unset(self::$templates[$this->type]); + return $this; + } + + /** + * Fetch direct parents + * + * return IcingaObject[] + */ + public function fetchParents() + { + // TODO: involve lookup cache + $res = array(); + $class = $this->object; + foreach ($this->listParentIds() as $id) { + $object = $class::loadWithAutoIncId($id, $this->connection); + $res[$object->object_name] = $object; + } + + return $res; + } + + public function listParentIds($id = null) + { + $this->requireTemplates(); + + if ($id === null) { + $object = $this->object; + + if ($object->hasBeenLoadedFromDb()) { + if ($object->gotImports() && $object->imports()->hasBeenModified()) { + return $this->listUnstoredParentIds(); + } + + $id = $object->id; + } else { + return $this->listUnstoredParentIds(); + } + } + + $type = $this->type; + + if (array_key_exists($id, self::$idIdx[$type])) { + return array_keys(self::$idIdx[$type][$id]); + } + + return array(); + } + + protected function listUnstoredParentIds() + { + return $this->getIdsForNames($this->listUnstoredParentNames()); + } + + protected function listUnstoredParentNames() + { + return $this->object->imports()->listImportNames(); + } + + public function listParentNames($name = null) + { + $this->requireTemplates(); + + if ($name === null) { + $object = $this->object; + + if ($object->hasBeenLoadedFromDb()) { + if ($object->gotImports() && $object->imports()->hasBeenModified()) { + return $this->listUnstoredParentNames(); + } + + $name = $object->object_name; + } else { + return $this->listUnstoredParentNames(); + } + } + + $type = $this->type; + + if (array_key_exists($name, self::$nameIdx[$type])) { + return array_keys(self::$nameIdx[$type][$name]); + } + + return array(); + } + + public function fetchResolvedParents() + { + if ($this->object->hasBeenLoadedFromDb()) { + return $this->fetchObjectsById($this->listResolvedParentIds()); + } + + $objects = array(); + foreach ($this->object->imports()->getObjects() as $parent) { + $objects += $parent->templateResolver()->fetchResolvedParents(); + } + + return $objects; + } + + public function listResolvedParentIds() + { + $this->requireTemplates(); + return $this->resolveParentIds(); + } + + /** + * TODO: unfinished and not used currently + * + * @return array + */ + public function listResolvedParentNames() + { + $this->requireTemplates(); + if (array_key_exists($name, self::$nameIdx[$type])) { + return array_keys(self::$nameIdx[$type][$name]); + } + + return $this->resolveParentNames($this->object->object_name); + } + + public function listParentsById($id) + { + return $this->getNamesForIds($this->resolveParentIds($id)); + } + + public function listParentsByName($name) + { + return $this->resolveParentNames($name); + } + + /** + * Gives a list of all object ids met when walking through ancestry + * + * Tree is walked in import order, duplicates are preserved, the given + * objectId is added last + * + * @param int $objectId + * + * @return array + */ + public function listFullInheritancePathIds($objectId = null) + { + $parentIds = $this->listParentIds($objectId); + $ids = array(); + + foreach ($parentIds as $parentId) { + foreach ($this->listFullInheritancePathIds($parentId) as $id) { + $ids[] = $id; + } + + $ids[] = $parentId; + } + + $object = $this->object; + if ($objectId === null && $object->hasBeenLoadedFromDb()) { + $ids[] = $object->id; + } + + return $ids; + } + + public function listChildren($objectId = null) + { + if ($objectId === null) { + $objectId = $this->object->id; + } + + if (array_key_exists($objectId, self::$reverseIdIdx[$this->type])) { + return self::$reverseIdIdx[$this->type][$objectId]; + } else { + return array(); + } + } + + public function listChildIds($objectId = null) + { + return array_keys($this->listChildren($objectId)); + } + + public function listDescendantIds($objectId = null) + { + if ($objectId === null) { + $objectId = $this->object->id; + } + } + + public function listInheritancePathIds($objectId = null) + { + return $this->uniquePathIds($this->listFullInheritancePathIds($objectId)); + } + + public function uniquePathIds(array $ids) + { + $single = array(); + foreach (array_reverse($ids) as $id) { + if (array_key_exists($id, $single)) { + continue; + } + $single[$id] = $id; + } + + return array_reverse(array_keys($single)); + } + + protected function resolveParentNames($name, &$list = array(), $path = array()) + { + $this->assertNotInList($name, $path); + $path[$name] = true; + foreach ($this->listParentNames($name) as $parent) { + $list[$parent] = true; + $this->resolveParentNames($parent, $list, $path); + unset($list[$parent]); + $list[$parent] = true; + } + + return array_keys($list); + } + + protected function resolveParentIds($id = null, &$list = array(), $path = array()) + { + if ($id === null) { + if ($check = $this->object->id) { + $this->assertNotInList($check, $path); + $path[$check] = true; + } + } else { + $this->assertNotInList($id, $path); + $path[$id] = true; + } + + foreach ($this->listParentIds($id) as $parent) { + $list[$parent] = true; + $this->resolveParentIds($parent, $list, $path); + unset($list[$parent]); + $list[$parent] = true; + } + + return array_keys($list); + } + + protected function assertNotInList($id, &$list) + { + if (array_key_exists($id, $list)) { + $list = array_keys($list); + $list[] = $id; + if (is_numeric($id)) { + throw new NestingError( + 'Loop detected: %s', + implode(' -> ', $this->getNamesForIds($list)) + ); + } else { + throw new NestingError( + 'Loop detected: %s', + implode(' -> ', $list) + ); + } + } + } + + protected function getNamesForIds($ids) + { + $names = array(); + foreach ($ids as $id) { + $names[] = $this->getNameForId($id); + } + + return $names; + } + + protected function getNameForId($id) + { + return self::$idToName[$this->type][$id]; + } + + protected function getIdsForNames($names) + { + $this->requireTemplates(); + $ids = array(); + foreach ($names as $name) { + $ids[] = $this->getIdForName($name); + } + + return $ids; + } + + protected function getIdForName($name) + { + if (! array_key_exists($name, self::$nameToId[$this->type])) { + throw new NotFoundError('There is no such import: "%s"', $name); + } + + return self::$nameToId[$this->type][$name]; + } + + protected function fetchObjectsById($ids) + { + $class = $this->object; + $connection = $this->connection; + $res = array(); + + foreach ($ids as $id) { + $res[] = $class::loadWithAutoIncId($id, $connection); + } + + return $res; + } + + protected function requireTemplates() + { + if (! array_key_exists($this->type, self::$templates)) { + $this->prepareLookupTables(); + } + + return $this; + } + + protected function prepareLookupTables() + { + $type = $this->type; + + Benchmark::measure("Preparing '$type' TemplateResolver lookup tables"); + $templates = $this->fetchTemplates(); + + $ids = array(); + $reverseIds = array(); + $names = array(); + $idToName = array(); + $nameToId = array(); + + foreach ($templates as $row) { + $id = $row->id; + $idToName[$id] = $row->name; + $nameToId[$row->name] = $id; + + if ($row->parent_id === null) { + continue; + } + $parentId = $row->parent_id; + $parentName = $row->parent_name; + + if (array_key_exists($id, $ids)) { + $ids[$id][$parentId] = $parentName; + $names[$row->name][$parentName] = $row->parent_id; + } else { + $ids[$id] = array( + $parentId => $parentName + ); + + $names[$row->name] = array( + $parentName => $parentId + ); + } + + if (! array_key_exists($parentId, $reverseIds)) { + $reverseIds[$parentId] = array(); + } + $reverseIds[$parentId][$id] = $row->name; + } + + self::$idIdx[$type] = $ids; + self::$reverseIdIdx[$type] = $reverseIds; + self::$nameIdx[$type] = $names; + self::$templates[$type] = $templates; // TODO: this is unused, isn't it? + self::$idToName[$type] = $idToName; + self::$nameToId[$type] = $nameToId; + Benchmark::measure('Preparing TemplateResolver lookup tables'); + } + + protected function fetchTemplates() + { + $db = $this->db; + $type = $this->type; + $table = $this->object->getTableName(); + + $query = $db->select()->from( + array('o' => $table), + array( + 'id' => 'o.id', + 'name' => 'o.object_name', + 'parent_id' => 'p.id', + 'parent_name' => 'p.object_name', + ) + )->joinLeft( + array('i' => $table . '_inheritance'), + 'o.id = i.' . $type . '_id', + array() + )->joinLeft( + array('p' => $table), + 'p.id = i.parent_' . $type . '_id', + array() + )->order('o.id')->order('i.weight'); + + return $db->fetchAll($query); + } + + public function __destruct() + { + unset($this->connection); + unset($this->db); + unset($this->object); + } + + public function refreshObject(IcingaObject $object) + { + $type = $object->getShortTableName(); + $name = $object->getObjectName(); + $parentNames = $object->imports; + self::$nameIdx[$type][$name] = $parentNames; + if ($object->hasBeenLoadedFromDb()) { + $id = $object->getProperty('id'); + self::$idIdx[$type][$id] = $this->getIdsForNames($parentNames); + self::$idToName[$type][$id] = $name; + self::$nameToId[$type][$name] = $id; + } + return $this; + } +} diff --git a/library/Director/Objects/IcingaTimePeriod.php b/library/Director/Objects/IcingaTimePeriod.php new file mode 100644 index 0000000..1232366 --- /dev/null +++ b/library/Director/Objects/IcingaTimePeriod.php @@ -0,0 +1,190 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; + +class IcingaTimePeriod extends IcingaObject implements ExportInterface +{ + protected $table = 'icinga_timeperiod'; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'zone_id' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'prefer_includes' => null, + 'display_name' => null, + 'update_method' => null, + ]; + + protected $booleans = [ + 'prefer_includes' => 'prefer_includes', + ]; + + protected $supportsImports = true; + + protected $supportsRanges = true; + + protected $supportedInLegacy = true; + + protected $relations = array( + 'zone' => 'IcingaZone', + ); + + protected $multiRelations = [ + 'includes' => [ + 'relatedObjectClass' => 'IcingaTimeperiod', + 'relatedShortName' => 'include', + ], + 'excludes' => [ + 'relatedObjectClass' => 'IcingaTimeperiod', + 'relatedShortName' => 'exclude', + 'legacyPropertyName' => 'exclude' + ], + ]; + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return object + * @throws \Icinga\Exception\NotFoundError + */ + public function export() + { + $props = (array) $this->toPlainObject(); + ksort($props); + + return (object) $props; + } + + /** + * @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['object_name']; + $key = $name; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Time Period "%s" already exists', + $name + ); + } else { + $object = static::create([], $db); + } + $object->setProperties($properties); + + return $object; + } + + /** + * Render update property + * + * Avoid complaints for method names with underscore: + * @codingStandardsIgnoreStart + * + * @return string + */ + public function renderUpdate_method() + { + // @codingStandardsIgnoreEnd + return ''; + } + + protected function renderObjectHeader() + { + return parent::renderObjectHeader() + . ' import "legacy-timeperiod"' . "\n"; + } + + protected function checkPeriodInRange($now, $name = null) + { + if ($name !== null) { + $period = static::load($name, $this->connection); + } else { + $period = $this; + } + + foreach ($period->ranges()->getRanges() as $range) { + if ($range->isActive($now)) { + return true; + } + } + + return false; + } + + public function isActive($now = null) + { + if ($now === null) { + $now = time(); + } + + $preferIncludes = $this->get('prefer_includes') !== 'n'; + + $active = $this->checkPeriodInRange($now); + $included = false; + $excluded = false; + + $variants = [ + 'includes' => &$included, + 'excludes' => &$excluded + ]; + + foreach ($variants as $key => &$var) { + foreach ($this->get($key) as $name) { + if ($this->checkPeriodInRange($now, $name)) { + $var = true; + break; + } + } + } + + if ($preferIncludes) { + if ($included) { + return true; + } elseif ($excluded) { + return false; + } else { + return $active; + } + } else { + if ($excluded) { + return false; + } elseif ($included) { + return true; + } else { + return $active; + } + } + + // TODO: no range currently means (and renders) "never", Icinga behaves + // different. Figure out whether and how we should support this + return false; + } + + protected function prefersGlobalZone() + { + return true; + } +} diff --git a/library/Director/Objects/IcingaTimePeriodRange.php b/library/Director/Objects/IcingaTimePeriodRange.php new file mode 100644 index 0000000..55c1a3e --- /dev/null +++ b/library/Director/Objects/IcingaTimePeriodRange.php @@ -0,0 +1,88 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; + +class IcingaTimePeriodRange extends DbObject +{ + protected $keyName = array('timeperiod_id', 'range_key', 'range_type'); + + protected $table = 'icinga_timeperiod_range'; + + protected $defaultProperties = array( + 'timeperiod_id' => null, + 'range_key' => null, + 'range_value' => null, + 'range_type' => 'include', + 'merge_behaviour' => 'set', + ); + + public function isActive($now = null) + { + if ($now === null) { + $now = time(); + } + + if (false === ($weekDay = $this->getWeekDay($this->get('range_key')))) { + // TODO, dates are not yet supported + return false; + } + + if ((int) date('w', $now) !== $weekDay) { + return false; + } + + $timeRanges = preg_split('/\s*,\s*/', $this->get('range_value'), -1, PREG_SPLIT_NO_EMPTY); + foreach ($timeRanges as $timeRange) { + if ($this->timeRangeIsActive($timeRange, $now)) { + return true; + } + } + + return false; + } + + protected function timeRangeIsActive($rangeString, $now) + { + $hBegin = $mBegin = $hEnd = $mEnd = null; + if (sscanf($rangeString, '%2d:%2d-%2d:%2d', $hBegin, $mBegin, $hEnd, $mEnd) === 4) { + if ($this->timeFromHourMin($hBegin, $mBegin, $now) <= $now + && $this->timeFromHourMin($hEnd, $mEnd, $now) >= $now + ) { + return true; + } + } else { + // TODO: throw exception? + } + + return false; + } + + protected function timeFromHourMin($hour, $min, $now) + { + return strtotime(sprintf('%s %02d:%02d:00', date('Y-m-d', $now), $hour, $min)); + } + + protected function getWeekDay($day) + { + switch ($day) { + case 'sunday': + return 0; + case 'monday': + return 1; + case 'tuesday': + return 2; + case 'wednesday': + return 3; + case 'thursday': + return 4; + case 'friday': + return 5; + case 'saturday': + return 6; + } + + return false; + } +} diff --git a/library/Director/Objects/IcingaTimePeriodRanges.php b/library/Director/Objects/IcingaTimePeriodRanges.php new file mode 100644 index 0000000..b18437d --- /dev/null +++ b/library/Director/Objects/IcingaTimePeriodRanges.php @@ -0,0 +1,35 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Countable; +use Iterator; +use Icinga\Module\Director\IcingaConfig\IcingaConfigRenderer; +use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1; + +class IcingaTimePeriodRanges extends IcingaRanges implements Iterator, Countable, IcingaConfigRenderer +{ + protected $rangeClass = IcingaTimePeriodRange::class; + protected $objectIdColumn = 'timeperiod_id'; + + public function toLegacyConfigString() + { + if (empty($this->ranges) && $this->object->isTemplate()) { + return ''; + } + + $out = ''; + + foreach ($this->ranges as $range) { + $out .= c1::renderKeyValue( + $range->get('range_key'), + $range->get('range_value') + ); + } + if ($out !== '') { + $out = "\n".$out; + } + + return $out; + } +} diff --git a/library/Director/Objects/IcingaUser.php b/library/Director/Objects/IcingaUser.php new file mode 100644 index 0000000..394e849 --- /dev/null +++ b/library/Director/Objects/IcingaUser.php @@ -0,0 +1,92 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; + +class IcingaUser extends IcingaObject implements ExportInterface +{ + protected $table = 'icinga_user'; + + protected $defaultProperties = array( + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'display_name' => null, + 'email' => null, + 'pager' => null, + 'enable_notifications' => null, + 'period_id' => null, + 'zone_id' => null, + ); + + protected $uuidColumn = 'uuid'; + + protected $supportsGroups = true; + + protected $supportsCustomVars = true; + + protected $supportsFields = true; + + protected $supportsImports = true; + + protected $booleans = array( + 'enable_notifications' => 'enable_notifications' + ); + + protected $relatedSets = array( + 'states' => 'StateFilterSet', + 'types' => 'TypeFilterSet', + ); + + protected $relations = array( + 'period' => 'IcingaTimePeriod', + 'zone' => 'IcingaZone', + ); + + public function export() + { + return ImportExportHelper::simpleExport($this); + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return IcingaUser + * @throws DuplicateKeyException + * @throws \Icinga\Exception\NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + $key = $properties['object_name']; + + if ($replace && static::exists($key, $db)) { + $object = static::load($key, $db); + } elseif (static::exists($key, $db)) { + throw new DuplicateKeyException( + 'Cannot import, %s "%s" already exists', + static::create([])->getShortTableName(), + $key + ); + } else { + $object = static::create([], $db); + } + + // $object->newFields = $properties['fields']; + unset($properties['fields']); + $object->setProperties($properties); + + return $object; + } + + public function getUniqueIdentifier() + { + return $this->getObjectName(); + } +} diff --git a/library/Director/Objects/IcingaUserField.php b/library/Director/Objects/IcingaUserField.php new file mode 100644 index 0000000..4a6432c --- /dev/null +++ b/library/Director/Objects/IcingaUserField.php @@ -0,0 +1,17 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaUserField extends IcingaObjectField +{ + protected $keyName = array('user_id', 'datafield_id'); + + protected $table = 'icinga_user_field'; + + protected $defaultProperties = array( + 'user_id' => null, + 'datafield_id' => null, + 'is_required' => null, + 'var_filter' => null, + ); +} diff --git a/library/Director/Objects/IcingaUserGroup.php b/library/Director/Objects/IcingaUserGroup.php new file mode 100644 index 0000000..656235a --- /dev/null +++ b/library/Director/Objects/IcingaUserGroup.php @@ -0,0 +1,29 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class IcingaUserGroup extends IcingaObjectGroup +{ + protected $table = 'icinga_usergroup'; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'display_name' => null, + 'zone_id' => null, + ]; + + protected $relations = [ + 'zone' => 'IcingaZone', + ]; + + protected function prefersGlobalZone() + { + return false; + } +} diff --git a/library/Director/Objects/IcingaVar.php b/library/Director/Objects/IcingaVar.php new file mode 100644 index 0000000..10addf2 --- /dev/null +++ b/library/Director/Objects/IcingaVar.php @@ -0,0 +1,72 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\CustomVariable\CustomVariable; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; + +class IcingaVar extends DbObject +{ + protected $table = 'icinga_var'; + + protected $keyName = 'checksum'; + + /** @var CustomVariable */ + protected $var; + + protected $defaultProperties = [ + 'checksum' => null, + 'rendered_checksum' => null, + 'varname' => null, + 'varvalue' => null, + 'rendered' => null + ]; + + protected $binaryProperties = [ + 'checksum', + 'rendered_checksum', + ]; + + /** + * @param CustomVariable $customVar + * @param Db $db + * + * @return static + */ + public static function forCustomVar(CustomVariable $customVar, Db $db) + { + $rendered = $customVar->render(); + + $var = static::create(array( + 'checksum' => $customVar->checksum(), + 'rendered_checksum' => sha1($rendered, true), + 'varname' => $customVar->getKey(), + 'varvalue' => $customVar->toJson(), + 'rendered' => $rendered, + ), $db); + + $var->var = $customVar; + + return $var; + } + + /** + * @param CustomVariable $customVar + * @param Db $db + * + * @return static + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + public static function generateForCustomVar(CustomVariable $customVar, Db $db) + { + $var = static::forCustomVar($customVar, $db); + $var->store(); + return $var; + } + + protected function onInsert() + { + IcingaFlatVar::generateForCustomVar($this->var, $this->getConnection()); + } +} diff --git a/library/Director/Objects/IcingaZone.php b/library/Director/Objects/IcingaZone.php new file mode 100644 index 0000000..8d77e47 --- /dev/null +++ b/library/Director/Objects/IcingaZone.php @@ -0,0 +1,110 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c; + +class IcingaZone extends IcingaObject +{ + protected $table = 'icinga_zone'; + + protected $uuidColumn = 'uuid'; + + protected $defaultProperties = [ + 'id' => null, + 'uuid' => null, + 'object_name' => null, + 'object_type' => null, + 'disabled' => 'n', + 'parent_id' => null, + 'is_global' => 'n', + ]; + + protected $booleans = [ + // Global is a reserved word in SQL, column name was prefixed + 'is_global' => 'global' + ]; + + protected $relations = [ + 'parent' => 'IcingaZone', + ]; + + protected $supportsImports = true; + + protected static $globalZoneNames; + + private $endpointList; + + protected function renderCustomExtensions() + { + $endpoints = $this->listEndpoints(); + if (empty($endpoints)) { + return ''; + } + + return c::renderKeyValue('endpoints', c::renderArray($endpoints)); + } + + public function isGlobal() + { + return $this->get('is_global') === 'y'; + } + + public static function zoneNameIsGlobal($name, Db $connection) + { + if (self::$globalZoneNames === null) { + $db = $connection->getDbAdapter(); + self::setCachedGlobalZoneNames($db->fetchCol( + $db->select()->from('icinga_zone', 'object_name')->where('is_global = ?', 'y') + )); + } + + return \in_array($name, self::$globalZoneNames); + } + + public static function setCachedGlobalZoneNames($names) + { + self::$globalZoneNames = $names; + } + + public function getRenderingZone(IcingaConfig $config = null) + { + // If the zone has a parent zone... + if ($this->get('parent_id')) { + // ...we render the zone object to the parent zone + return $this->get('parent'); + } elseif ($this->get('is_global') === 'y') { + // ...additional global zones are rendered to our global zone... + return $this->connection->getDefaultGlobalZoneName(); + } else { + // ...and all the other zones are rendered to our master zone + return $this->connection->getMasterZoneName(); + } + } + + public function setEndpointList($list) + { + $this->endpointList = $list; + + return $this; + } + + // TODO: Move this away, should be prefetchable: + public function listEndpoints() + { + $id = $this->get('id'); + if ($id && $this->endpointList === null) { + $db = $this->getDb(); + $query = $db->select() + ->from('icinga_endpoint', 'object_name') + ->where('zone_id = ?', $id) + ->order('object_name'); + + $this->endpointList = $db->fetchCol($query); + } + + return $this->endpointList; + } +} diff --git a/library/Director/Objects/ImportExportHelper.php b/library/Director/Objects/ImportExportHelper.php new file mode 100644 index 0000000..98d34c6 --- /dev/null +++ b/library/Director/Objects/ImportExportHelper.php @@ -0,0 +1,68 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Db; + +/** + * Helper class, allows to reduce duplicate code. Might be moved elsewhere + * afterwards + */ +class ImportExportHelper +{ + /** + * Does not support every type out of the box + * + * @param IcingaObject $object + * @return object + * @throws \Icinga\Exception\NotFoundError + */ + public static function simpleExport(IcingaObject $object) + { + $props = (array) $object->toPlainObject(); + $props['fields'] = static::fetchFields($object); + ksort($props); // TODO: ksort in toPlainObject? + + return (object) $props; + } + + public static function fetchFields(IcingaObject $object) + { + return static::loadFieldReferences( + $object->getConnection(), + $object->getShortTableName(), + $object->get('id') + ); + } + + /** + * @param Db $connection + * @param string $type Warning: this will not be validated. + * @param int $id + * @return array + */ + public static function loadFieldReferences(Db $connection, $type, $id) + { + $db = $connection->getDbAdapter(); + $res = $db->fetchAll( + $db->select()->from([ + 'f' => "icinga_${type}_field" + ], [ + 'f.datafield_id', + 'f.is_required', + 'f.var_filter', + ])->join(['df' => 'director_datafield'], 'df.id = f.datafield_id', []) + ->where("${type}_id = ?", $id) + ->order('varname ASC') + ); + + if (empty($res)) { + return []; + } + + foreach ($res as $field) { + $field->datafield_id = (int) $field->datafield_id; + } + return $res; + } +} diff --git a/library/Director/Objects/ImportRowModifier.php b/library/Director/Objects/ImportRowModifier.php new file mode 100644 index 0000000..76982c2 --- /dev/null +++ b/library/Director/Objects/ImportRowModifier.php @@ -0,0 +1,91 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObjectWithSettings; +use Icinga\Module\Director\Hook\PropertyModifierHook; +use Icinga\Module\Director\Objects\Extension\PriorityColumn; +use RuntimeException; + +class ImportRowModifier extends DbObjectWithSettings implements InstantiatedViaHook +{ + use PriorityColumn; + + protected $table = 'import_row_modifier'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = [ + 'id' => null, + 'source_id' => null, + 'property_name' => null, + 'provider_class' => null, + 'target_property' => null, + 'priority' => null, + 'description' => null, + ]; + + protected $settingsTable = 'import_row_modifier_setting'; + + protected $settingsRemoteId = 'row_modifier_id'; + + private $hookInstance; + + public function getInstance() + { + if ($this->hookInstance === null) { + $class = $this->get('provider_class'); + /** @var PropertyModifierHook $obj */ + if (! class_exists($class)) { + throw new RuntimeException(sprintf( + 'Cannot instantiate Property modifier %s', + $class + )); + } + $obj = new $class; + $obj->setSettings($this->getSettings()); + $obj->setPropertyName($this->get('property_name')); + $obj->setTargetProperty($this->get('target_property')); + $obj->setDb($this->connection); + $this->hookInstance = $obj; + } + + return $this->hookInstance; + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return \stdClass + */ + public function export() + { + $properties = $this->getProperties(); + unset($properties['id']); + unset($properties['source_id']); + $properties['settings'] = $this->getInstance()->exportSettings(); + ksort($properties); + + return (object) $properties; + } + + public function setSettings($settings) + { + $settings = $this->getInstance()->setSettings((array) $settings)->getSettings(); + + return parent::setSettings($settings); // TODO: Change the autogenerated stub + } + + protected function beforeStore() + { + if (! $this->hasBeenLoadedFromDb() && $this->get('priority') === null) { + $this->setNextPriority('source_id'); + } + } + + protected function onInsert() + { + $this->refreshPriortyProperty(); + } +} diff --git a/library/Director/Objects/ImportRun.php b/library/Director/Objects/ImportRun.php new file mode 100644 index 0000000..d3bdb7c --- /dev/null +++ b/library/Director/Objects/ImportRun.php @@ -0,0 +1,159 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; + +class ImportRun extends DbObject +{ + protected $table = 'import_run'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + /** @var ImportSource */ + protected $importSource = null; + + protected $defaultProperties = [ + 'id' => null, + 'source_id' => null, + 'rowset_checksum' => null, + 'start_time' => null, + 'end_time' => null, + // TODO: Check whether succeeded could be dropped + 'succeeded' => null, + ]; + + protected $binaryProperties = [ + 'rowset_checksum', + ]; + + public function prepareImportedObjectQuery($columns = array('object_name')) + { + return $this->getDb()->select()->from( + array('r' => 'imported_row'), + $columns + )->joinLeft( + array('rsr' => 'imported_rowset_row'), + 'rsr.row_checksum = r.checksum', + array() + )->where( + 'rsr.rowset_checksum = ?', + $this->getConnection()->quoteBinary($this->rowset_checksum) + ); + } + + public function listColumnNames() + { + $db = $this->getDb(); + + $query = $db->select()->distinct()->from( + array('p' => 'imported_property'), + 'property_name' + )->join( + array('rp' => 'imported_row_property'), + 'rp.property_checksum = p.checksum', + array() + )->join( + array('rsr' => 'imported_rowset_row'), + 'rsr.row_checksum = rp.row_checksum', + array() + )->where('rsr.rowset_checksum = ?', $this->getConnection()->quoteBinary($this->rowset_checksum)); + + return $db->fetchCol($query); + } + + public function fetchRows($columns, $filter = null, $keys = null) + { + $db = $this->getDb(); + /** @var Db $connection */ + $connection = $this->getConnection(); + $binchecksum = $this->rowset_checksum; + + $query = $db->select()->from( + array('rsr' => 'imported_rowset_row'), + array( + 'object_name' => 'r.object_name', + 'property_name' => 'p.property_name', + 'property_value' => 'p.property_value', + 'format' => 'p.format' + ) + )->join( + array('r' => 'imported_row'), + 'rsr.row_checksum = r.checksum', + array() + )->join( + array('rp' => 'imported_row_property'), + 'r.checksum = rp.row_checksum', + array() + )->join( + array('p' => 'imported_property'), + 'p.checksum = rp.property_checksum', + array() + )->order('r.object_name'); + if ($connection->isMysql()) { + $query->where('rsr.rowset_checksum = :checksum')->bind([ + 'checksum' => $binchecksum + ]); + } else { + $query->where( + 'rsr.rowset_checksum = ?', + $connection->quoteBinary($binchecksum) + ); + } + + if ($columns === null) { + $columns = $this->listColumnNames(); + } else { + $query->where('p.property_name IN (?)', $columns); + } + + $result = array(); + $empty = (object) array(); + foreach ($columns as $k => $v) { + $empty->$k = null; + } + + if ($keys !== null) { + $query->where('r.object_name IN (?)', $keys); + } + + foreach ($db->fetchAll($query) as $row) { + if (! array_key_exists($row->object_name, $result)) { + $result[$row->object_name] = clone($empty); + } + + if ($row->format === 'json') { + $result[$row->object_name]->{$row->property_name} = json_decode($row->property_value); + } else { + $result[$row->object_name]->{$row->property_name} = $row->property_value; + } + } + + if ($filter) { + $filtered = array(); + foreach ($result as $key => $row) { + if ($filter->matches($row)) { + $filtered[$key] = $row; + } + } + + return $filtered; + } + + return $result; + } + + public function importSource() + { + if ($this->importSource === null) { + $this->importSource = ImportSource::loadWithAutoIncId( + (int) $this->get('source_id'), + $this->connection + ); + } + return $this->importSource; + } +} diff --git a/library/Director/Objects/ImportSource.php b/library/Director/Objects/ImportSource.php new file mode 100644 index 0000000..fd892ef --- /dev/null +++ b/library/Director/Objects/ImportSource.php @@ -0,0 +1,537 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Application\Benchmark; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Application\MemoryLimit; +use Icinga\Module\Director\Data\Db\DbObjectWithSettings; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Hook\PropertyModifierHook; +use Icinga\Module\Director\Import\Import; +use Icinga\Module\Director\Import\SyncUtils; +use InvalidArgumentException; +use Exception; + +class ImportSource extends DbObjectWithSettings implements ExportInterface +{ + protected $table = 'import_source'; + + protected $keyName = 'source_name'; + + protected $autoincKeyName = 'id'; + + protected $protectAutoinc = false; + + protected $defaultProperties = [ + 'id' => null, + 'source_name' => null, + 'provider_class' => null, + 'key_column' => null, + 'import_state' => 'unknown', + 'last_error_message' => null, + 'last_attempt' => null, + 'description' => null, + ]; + + protected $stateProperties = [ + 'import_state', + 'last_error_message', + 'last_attempt', + ]; + + protected $settingsTable = 'import_source_setting'; + + protected $settingsRemoteId = 'source_id'; + + private $rowModifiers; + + private $loadedRowModifiers; + + private $newRowModifiers; + + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return \stdClass + */ + public function export() + { + $plain = $this->getProperties(); + $plain['originalId'] = $plain['id']; + unset($plain['id']); + + foreach ($this->stateProperties as $key) { + unset($plain[$key]); + } + + $plain['settings'] = (object) $this->getSettings(); + $plain['modifiers'] = $this->exportRowModifiers(); + ksort($plain); + + return (object) $plain; + } + + /** + * @param $plain + * @param Db $db + * @param bool $replace + * @return ImportSource + * @throws DuplicateKeyException + * @throws NotFoundError + */ + public static function import($plain, Db $db, $replace = false) + { + $properties = (array) $plain; + if (isset($properties['originalId'])) { + $id = $properties['originalId']; + unset($properties['originalId']); + } else { + $id = null; + } + $name = $properties['source_name']; + + if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) { + $object = static::loadWithAutoIncId($id, $db); + } elseif ($replace && static::exists($name, $db)) { + $object = static::load($name, $db); + } elseif (static::existsWithName($name, $db)) { + throw new DuplicateKeyException( + 'Import Source %s already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + if (! isset($properties['modifiers'])) { + $properties['modifiers'] = []; + } + + $object->setProperties($properties); + + return $object; + } + + public function setModifiers(array $modifiers) + { + if ($this->loadedRowModifiers === null && $this->hasBeenLoadedFromDb()) { + $this->loadedRowModifiers = $this->fetchRowModifiers(); + } + $current = (array) $this->loadedRowModifiers; + if (\count($current) !== \count($modifiers)) { + $this->newRowModifiers = $modifiers; + } else { + $i = 0; + $modified = false; + foreach ($modifiers as $props) { + $this->loadedRowModifiers[$i]->setProperties((array) $props); + if ($this->loadedRowModifiers[$i]->hasBeenModified()) { + $modified = true; + } + $i++; + } + if ($modified) { + $this->newRowModifiers = $modifiers; + } + } + } + + public function hasBeenModified() + { + return $this->newRowModifiers !== null + || parent::hasBeenModified(); + } + + public function getUniqueIdentifier() + { + return $this->get('source_name'); + } + + /** + * @param $name + * @param Db $connection + * @return ImportSource + * @throws NotFoundError + */ + public static function loadByName($name, Db $connection) + { + $db = $connection->getDbAdapter(); + $properties = $db->fetchRow( + $db->select()->from('import_source')->where('source_name = ?', $name) + ); + if ($properties === false) { + throw new NotFoundError(sprintf( + 'There is no such Import Source: "%s"', + $name + )); + } + + return static::create([], $connection)->setDbProperties($properties); + } + + public static function existsWithName($name, Db $connection) + { + $db = $connection->getDbAdapter(); + + return (string) $name === (string) $db->fetchOne( + $db->select() + ->from('import_source', 'source_name') + ->where('source_name = ?', $name) + ); + } + + /** + * @param string $name + * @param int $id + * @param Db $connection + * @api internal + * @return bool + */ + protected static function existsWithNameAndId($name, $id, Db $connection) + { + $db = $connection->getDbAdapter(); + $dummy = new static; + $idCol = $dummy->autoincKeyName; + $keyCol = $dummy->keyName; + + return (string) $id === (string) $db->fetchOne( + $db->select() + ->from($dummy->table, $idCol) + ->where("$idCol = ?", $id) + ->where("$keyCol = ?", $name) + ); + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader + * @return array + */ + protected function exportRowModifiers() + { + $modifiers = []; + foreach ($this->fetchRowModifiers() as $modifier) { + $modifiers[] = $modifier->export(); + } + + return $modifiers; + } + + /** + * @param bool $required + * @return ImportRun|null + * @throws NotFoundError + */ + public function fetchLastRun($required = false) + { + return $this->fetchLastRunBefore(time() + 1, $required); + } + + /** + * @throws DuplicateKeyException + */ + protected function onStore() + { + parent::onStore(); + if ($this->newRowModifiers !== null) { + $connection = $this->getConnection(); + $db = $connection->getDbAdapter(); + $myId = $this->get('id'); + if ($this->hasBeenLoadedFromDb()) { + $db->delete( + 'import_row_modifier', + $db->quoteInto('source_id = ?', $myId) + ); + } + + foreach ($this->newRowModifiers as $modifier) { + $modifier = ImportRowModifier::create((array) $modifier, $connection); + $modifier->set('source_id', $myId); + $modifier->store(); + } + } + } + + /** + * @param $timestamp + * @param bool $required + * @return ImportRun|null + * @throws NotFoundError + */ + public function fetchLastRunBefore($timestamp, $required = false) + { + if (! $this->hasBeenLoadedFromDb()) { + return $this->nullUnlessRequired($required); + } + + if ($timestamp === null) { + $timestamp = time(); + } + + $db = $this->getDb(); + $query = $db->select()->from( + ['ir' => 'import_run'], + 'ir.id' + )->where('ir.source_id = ?', $this->get('id')) + ->where('ir.start_time < ?', date('Y-m-d H:i:s', $timestamp)) + ->order('ir.start_time DESC') + ->limit(1); + + $runId = $db->fetchOne($query); + + if ($runId) { + return ImportRun::load($runId, $this->getConnection()); + } else { + return $this->nullUnlessRequired($required); + } + } + + /** + * @param $required + * @return null + * @throws NotFoundError + */ + protected function nullUnlessRequired($required) + { + if ($required) { + throw new NotFoundError( + 'No data has been imported for "%s" yet', + $this->get('source_name') + ); + } + + return null; + } + + public function applyModifiers(&$data) + { + $modifiers = $this->fetchFlatRowModifiers(); + + if (empty($modifiers)) { + return $this; + } + + foreach ($modifiers as $modPair) { + /** @var PropertyModifierHook $modifier */ + list($property, $modifier) = $modPair; + $rejected = []; + $newRows = []; + foreach ($data as $key => $row) { + $this->applyPropertyModifierToRow($modifier, $property, $row); + if ($modifier->rejectsRow()) { + $rejected[] = $key; + $modifier->rejectRow(false); + } + if ($modifier->expandsRows()) { + $target = $modifier->getTargetProperty($property); + + $newValue = $row->$target; + if (\is_array($newValue)) { + foreach ($newValue as $val) { + $newRow = clone $row; + $newRow->$target = $val; + $newRows[] = $newRow; + } + $rejected[] = $key; + } + } + } + + foreach ($rejected as $key) { + unset($data[$key]); + } + foreach ($newRows as $row) { + $data[] = $row; + } + } + + return $this; + } + + public function getObjectName() + { + return $this->get('source_name'); + } + + public static function getKeyColumnName() + { + return 'source_name'; + } + + protected function applyPropertyModifierToRow(PropertyModifierHook $modifier, $key, $row) + { + if (! is_object($row)) { + throw new InvalidArgumentException('Every imported row MUST be an object'); + } + if ($modifier->requiresRow()) { + $modifier->setRow($row); + } + + if (property_exists($row, $key)) { + $value = $row->$key; + } elseif (strpos($key, '.') !== false) { + $value = SyncUtils::getSpecificValue($row, $key); + } else { + $value = null; + } + + $target = $modifier->getTargetProperty($key); + if (strpos($target, '.') !== false) { + throw new InvalidArgumentException(sprintf( + 'Cannot set value for nested key "%s"', + $target + )); + } + + if (is_array($value) && ! $modifier->hasArraySupport()) { + $new = []; + foreach ($value as $k => $v) { + $new[$k] = $modifier->transform($v); + } + $row->$target = $new; + } else { + $row->$target = $modifier->transform($value); + } + } + + public function getRowModifiers() + { + if ($this->rowModifiers === null) { + $this->prepareRowModifiers(); + } + + return $this->rowModifiers; + } + + public function hasRowModifiers() + { + return count($this->getRowModifiers()) > 0; + } + + /** + * @return ImportRowModifier[] + */ + public function fetchRowModifiers() + { + $db = $this->getDb(); + $modifiers = ImportRowModifier::loadAll( + $this->getConnection(), + $db->select() + ->from('import_row_modifier') + ->where('source_id = ?', $this->get('id')) + ->order('priority ASC') + ); + + if ($modifiers) { + return $modifiers; + } else { + return []; + } + } + + protected function fetchFlatRowModifiers() + { + $mods = []; + foreach ($this->fetchRowModifiers() as $mod) { + $mods[] = [$mod->get('property_name'), $mod->getInstance()]; + } + + return $mods; + } + + protected function prepareRowModifiers() + { + $modifiers = []; + + foreach ($this->fetchRowModifiers() as $mod) { + $name = $mod->get('property_name'); + if (! array_key_exists($name, $modifiers)) { + $modifiers[$name] = []; + } + + $modifiers[$name][] = $mod->getInstance(); + } + + $this->rowModifiers = $modifiers; + } + + public function listModifierTargetProperties() + { + $list = []; + foreach ($this->getRowModifiers() as $rowMods) { + /** @var PropertyModifierHook $mod */ + foreach ($rowMods as $mod) { + if ($mod->hasTargetProperty()) { + $list[$mod->getTargetProperty()] = true; + } + } + } + + return array_keys($list); + } + + /** + * @param bool $runImport + * @return bool + * @throws DuplicateKeyException + */ + public function checkForChanges($runImport = false) + { + $hadChanges = false; + + $name = $this->get('source_name'); + Benchmark::measure("Starting with import $name"); + $this->raiseLimits(); + try { + $import = new Import($this); + $this->set('last_attempt', date('Y-m-d H:i:s')); + if ($import->providesChanges()) { + Benchmark::measure("Found changes for $name"); + $hadChanges = true; + $this->set('import_state', 'pending-changes'); + + if ($runImport && $import->run()) { + Benchmark::measure("Import succeeded for $name"); + $this->set('import_state', 'in-sync'); + } + } else { + $this->set('import_state', 'in-sync'); + } + + $this->set('last_error_message', null); + } catch (Exception $e) { + $this->set('import_state', 'failing'); + Benchmark::measure("Import failed for $name"); + $this->set('last_error_message', $e->getMessage()); + } + + if ($this->hasBeenModified()) { + $this->store(); + } + + return $hadChanges; + } + + /** + * @return bool + * @throws DuplicateKeyException + */ + public function runImport() + { + return $this->checkForChanges(true); + } + + /** + * Raise PHP resource limits + * + * @return $this; + */ + protected function raiseLimits() + { + MemoryLimit::raiseTo('1024M'); + ini_set('max_execution_time', 0); + + return $this; + } +} diff --git a/library/Director/Objects/InstantiatedViaHook.php b/library/Director/Objects/InstantiatedViaHook.php new file mode 100644 index 0000000..79f3442 --- /dev/null +++ b/library/Director/Objects/InstantiatedViaHook.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Hook\JobHook; +use Icinga\Module\Director\Hook\PropertyModifierHook; + +interface InstantiatedViaHook +{ + /** + * @return mixed|PropertyModifierHook|JobHook + */ + public function getInstance(); +} diff --git a/library/Director/Objects/ObjectApplyMatches.php b/library/Director/Objects/ObjectApplyMatches.php new file mode 100644 index 0000000..018c880 --- /dev/null +++ b/library/Director/Objects/ObjectApplyMatches.php @@ -0,0 +1,239 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Filter\FilterExpression; +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Director\Application\MemoryLimit; +use Icinga\Module\Director\Data\AssignFilterHelper; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use stdClass; + +abstract class ObjectApplyMatches +{ + protected static $flatObjects; + + protected static $columnMap = array( + 'name' => 'object_name' + ); + + protected $object; + + protected $flatObject; + + protected static $type; + + protected static $preparedFilters = array(); + + public static function prepare(IcingaObject $object) + { + return new static($object); + } + + /** + * Prepare a Filter with fixed columns, and store the result + * + * @param Filter $filter + * + * @return Filter + */ + protected static function getPreparedFilter(Filter $filter) + { + $hash = spl_object_hash($filter); + if (! array_key_exists($hash, self::$preparedFilters)) { + $filter = clone($filter); + static::fixFilterColumns($filter); + self::$preparedFilters[$hash] = $filter; + } + return self::$preparedFilters[$hash]; + } + + public function matchesFilter(Filter $filter) + { + $filterObj = static::getPreparedFilter($filter); + if ($filterObj->isExpression() || ! $filterObj->isEmpty()) { + return AssignFilterHelper::matchesFilter($filterObj, $this->flatObject); + } else { + return false; + } + } + + /** + * @param Filter $filter + * @param Db $db + * + * @return array + */ + public static function forFilter(Filter $filter, Db $db) + { + $result = array(); + Benchmark::measure(sprintf('Starting Filter %s', $filter)); + $filter = clone($filter); + static::fixFilterColumns($filter); + $helper = new AssignFilterHelper($filter); + + foreach (static::flatObjects($db) as $object) { + if ($helper->matches($object)) { + $name = $object->object_name; + $result[] = $name; + } + } + Benchmark::measure(sprintf('Got %d results for %s', count($result), $filter)); + + return array_values($result); + } + + protected static function getType() + { + if (static::$type === null) { + throw new ProgrammingError( + 'Implementations of %s need ::$type to be defined, %s has not', + __CLASS__, + get_called_class() + ); + } + + return static::$type; + } + + protected static function flatObjects(Db $db) + { + if (self::$flatObjects === null) { + self::$flatObjects = static::fetchFlatObjects($db); + } + + return self::$flatObjects; + } + + protected static function raiseLimits() + { + // Note: IcingaConfig also raises the limit for generation, **but** we + // need the higher limit for preview. + MemoryLimit::raiseTo('1024M'); + } + + protected static function fetchFlatObjects(Db $db) + { + return static::fetchFlatObjectsByType($db, static::getType()); + } + + protected static function fetchFlatObjectsByType(Db $db, $type) + { + self::raiseLimits(); + + Benchmark::measure("ObjectApplyMatches: prefetching $type"); + PrefetchCache::initialize($db); + /** @var IcingaObject $class */ + $class = DbObjectTypeRegistry::classByType($type); + $all = $class::prefetchAll($db); + Benchmark::measure("ObjectApplyMatches: related objects for $type"); + $class::prefetchAllRelationsByType($type, $db); + Benchmark::measure("ObjectApplyMatches: preparing flat $type objects"); + + $objects = array(); + foreach ($all as $object) { + if ($object->isTemplate()) { + continue; + } + + $flat = $object->toPlainObject(true, false); + static::flattenVars($flat); + $objects[$object->getObjectName()] = $flat; + } + Benchmark::measure("ObjectApplyMatches: $type cache ready"); + + return $objects; + } + + public static function fixFilterColumns(Filter $filter) + { + if ($filter->isExpression()) { + /** @var FilterExpression $filter */ + static::fixFilterExpressionColumn($filter); + } else { + foreach ($filter->filters() as $sub) { + static::fixFilterColumns($sub); + } + } + } + + protected static function fixFilterExpressionColumn(FilterExpression $filter) + { + if (static::columnIsJson($filter)) { + $column = $filter->getExpression(); + $filter->setExpression($filter->getColumn()); + $filter->setColumn($column); + } + + $col = $filter->getColumn(); + $type = static::$type; + + if ($type && substr($col, 0, strlen($type) + 1) === "${type}.") { + $filter->setColumn($col = substr($col, strlen($type) + 1)); + } + + if (array_key_exists($col, self::$columnMap)) { + $filter->setColumn(self::$columnMap[$col]); + } + + $filter->setExpression(json_decode($filter->getExpression())); + } + + protected static function columnIsJson(FilterExpression $filter) + { + $col = $filter->getColumn(); + return strlen($col) && $col[0] === '"'; + } + + /** + * Helper, flattens all vars of a given object + * + * The object itself will be modified, and the 'vars' property will be + * replaced with corresponding 'vars.whatever' properties + * + * @param $object + * @param string $key + */ + protected static function flattenVars(stdClass $object, $key = 'vars') + { + if (property_exists($object, 'vars')) { + foreach ($object->vars as $k => $v) { + if (is_object($v)) { + static::flattenVars($v, $k); + } + $object->{$key . '.' . $k} = $v; + } + unset($object->vars); + } + } + + protected function __construct(IcingaObject $object) + { + $this->object = $object; + $flat = $object->toPlainObject(true, false); + // Sure, we are flat - but we might still want to match templates. + unset($flat->imports); + $flat->templates = $object->listFlatResolvedImportNames(); + $this->addAppliedGroupsToFlatObject($flat, $object); + static::flattenVars($flat); + $this->flatObject = $flat; + } + + protected function addAppliedGroupsToFlatObject($flat, IcingaObject $object) + { + if ($object instanceof IcingaHost) { + $appliedGroups = $object->getAppliedGroups(); + if (! empty($appliedGroups)) { + if (isset($flat->groups)) { + $flat->groups = array_merge($flat->groups, $appliedGroups); + } else { + $flat->groups = $appliedGroups; + } + } + } + } +} diff --git a/library/Director/Objects/ObjectWithArguments.php b/library/Director/Objects/ObjectWithArguments.php new file mode 100644 index 0000000..2f99460 --- /dev/null +++ b/library/Director/Objects/ObjectWithArguments.php @@ -0,0 +1,18 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +interface ObjectWithArguments +{ + /** + * @return boolean + */ + public function gotArguments(); + + /** + * @return IcingaArguments + */ + public function arguments(); + + public function unsetArguments(); +} diff --git a/library/Director/Objects/ServiceGroupMembershipResolver.php b/library/Director/Objects/ServiceGroupMembershipResolver.php new file mode 100644 index 0000000..4649212 --- /dev/null +++ b/library/Director/Objects/ServiceGroupMembershipResolver.php @@ -0,0 +1,8 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +class ServiceGroupMembershipResolver extends GroupMembershipResolver +{ + protected $type = 'service'; +} diff --git a/library/Director/Objects/SyncProperty.php b/library/Director/Objects/SyncProperty.php new file mode 100644 index 0000000..20c4700 --- /dev/null +++ b/library/Director/Objects/SyncProperty.php @@ -0,0 +1,48 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Objects\Extension\PriorityColumn; + +class SyncProperty extends DbObject +{ + use PriorityColumn; + + protected $table = 'sync_property'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = [ + 'id' => null, + 'rule_id' => null, + 'source_id' => null, + 'source_expression' => null, + 'destination_field' => null, + 'priority' => null, + 'filter_expression' => null, + 'merge_policy' => null + ]; + + protected function beforeStore() + { + if (! $this->hasBeenLoadedFromDb() && $this->get('priority') === null) { + $this->setNextPriority('rule_id'); + } + } + + public function setSource($name) + { + $source = ImportSource::loadByName($name, $this->getConnection()); + $this->set('source_id', $source->get('id')); + + return $this; + } + + protected function onInsert() + { + $this->refreshPriortyProperty(); + } +} diff --git a/library/Director/Objects/SyncRule.php b/library/Director/Objects/SyncRule.php new file mode 100644 index 0000000..89f7fd1 --- /dev/null +++ b/library/Director/Objects/SyncRule.php @@ -0,0 +1,553 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; +use Icinga\Module\Director\Exception\DuplicateKeyException; +use Icinga\Module\Director\Import\PurgeStrategy\PurgeStrategy; +use Icinga\Module\Director\Import\Sync; +use Exception; + +class SyncRule extends DbObject implements ExportInterface +{ + protected $table = 'sync_rule'; + + protected $keyName = 'rule_name'; + + protected $autoincKeyName = 'id'; + + protected $protectAutoinc = false; + + protected $defaultProperties = [ + 'id' => null, + 'rule_name' => null, + 'object_type' => null, + 'update_policy' => null, + 'purge_existing' => null, + 'purge_action' => null, + 'filter_expression' => null, + 'sync_state' => 'unknown', + 'last_error_message' => null, + 'last_attempt' => null, + 'description' => null, + ]; + + protected $stateProperties = [ + 'sync_state', + 'last_error_message', + 'last_attempt', + ]; + + private $sync; + + private $purgeStrategy; + + private $filter; + + private $hasCombinedKey; + + /** @var SyncProperty[] */ + private $syncProperties; + + private $sourceKeyPattern; + + private $destinationKeyPattern; + + private $newSyncProperties; + + private $originalId; + + public function listInvolvedSourceIds() + { + if (! $this->hasBeenLoadedFromDb()) { + return []; + } + + $db = $this->getDb(); + return array_map('intval', array_unique( + $db->fetchCol( + $db->select() + ->from(['p' => 'sync_property'], 'p.source_id') + ->join(['s' => 'import_source'], 's.id = p.source_id', array()) + ->where('rule_id = ?', $this->get('id')) + ->order('s.source_name') + ) + )); + } + + /** + * @return array + * @throws \Icinga\Exception\NotFoundError + */ + public function fetchInvolvedImportSources() + { + $sources = []; + + foreach ($this->listInvolvedSourceIds() as $sourceId) { + $sources[$sourceId] = ImportSource::loadWithAutoIncId($sourceId, $this->getConnection()); + } + + return $sources; + } + + public function getLastSyncTimestamp() + { + if (! $this->hasBeenLoadedFromDb()) { + return null; + } + + $db = $this->getDb(); + $query = $db->select()->from( + ['sr' => 'sync_run'], + 'sr.start_time' + )->where('sr.rule_id = ?', $this->get('id')) + ->order('sr.start_time DESC') + ->limit(1); + + return $db->fetchOne($query); + } + + public function getLastSyncRunId() + { + if (! $this->hasBeenLoadedFromDb()) { + return null; + } + + $db = $this->getDb(); + $query = $db->select()->from( + ['sr' => 'sync_run'], + 'sr.id' + )->where('sr.rule_id = ?', $this->get('id')) + ->order('sr.start_time DESC') + ->limit(1); + + return $db->fetchOne($query); + } + + public function matches($row) + { + if ($this->get('filter_expression') === null) { + return true; + } + + return $this->filter()->matches($row); + } + + /** + * @param bool $apply + * @return bool + * @throws DuplicateKeyException + */ + public function checkForChanges($apply = false) + { + $hadChanges = false; + + Benchmark::measure('Checking sync rule ' . $this->get('rule_name')); + try { + $this->set('last_attempt', date('Y-m-d H:i:s')); + $this->set('sync_state', 'unknown'); + $sync = $this->sync(); + if ($sync->hasModifications()) { + Benchmark::measure('Got modifications for sync rule ' . $this->get('rule_name')); + $this->set('sync_state', 'pending-changes'); + if ($apply && $runId = $sync->apply()) { + Benchmark::measure('Successfully synced rule ' . $this->get('rule_name')); + $this->set('sync_state', 'in-sync'); + } + + $hadChanges = true; + } else { + Benchmark::measure('No modifications for sync rule ' . $this->get('rule_name')); + $this->set('sync_state', 'in-sync'); + } + + $this->set('last_error_message', null); + } catch (Exception $e) { + $this->set('sync_state', 'failing'); + $this->set('last_error_message', $e->getMessage()); + // TODO: Store last error details / trace? + } + + if ($this->hasBeenModified()) { + $this->store(); + } + + return $hadChanges; + } + + /** + * @return IcingaObject[] + * @throws IcingaException + */ + public function getExpectedModifications() + { + return $this->sync()->getExpectedModifications(); + } + + /** + * @return bool + * @throws DuplicateKeyException + */ + public function applyChanges() + { + return $this->checkForChanges(true); + } + + public function getSourceKeyPattern() + { + if ($this->hasCombinedKey()) { + return $this->sourceKeyPattern; + } else { + return null; // ?? + } + } + + public function getDestinationKeyPattern() + { + if ($this->hasCombinedKey()) { + return $this->destinationKeyPattern; + } else { + return null; // ?? + } + } + + protected function sync() + { + if ($this->sync === null) { + $this->sync = new Sync($this); + } + + return $this->sync; + } + + /** + * @return Filter + */ + public function filter() + { + if ($this->filter === null) { + $this->filter = Filter::fromQueryString($this->get('filter_expression')); + } + + return $this->filter; + } + + public function purgeStrategy() + { + if ($this->purgeStrategy === null) { + $this->purgeStrategy = $this->loadConfiguredPurgeStrategy(); + } + + return $this->purgeStrategy; + } + + // TODO: Allow for more + protected function loadConfiguredPurgeStrategy() + { + if ($this->get('purge_existing') === 'y') { + return PurgeStrategy::load('ImportRunBased', $this); + } else { + return PurgeStrategy::load('PurgeNothing', $this); + } + } + + /** + * @deprecated please use \Icinga\Module\Director\Data\Exporter + * @return object + */ + public function export() + { + $plain = $this->getProperties(); + $plain['originalId'] = $plain['id']; + unset($plain['id']); + + foreach ($this->stateProperties as $key) { + unset($plain[$key]); + } + $plain['properties'] = $this->exportSyncProperties(); + ksort($plain); + + return (object) $plain; + } + + /** + * @param object $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; + if (isset($properties['originalId'])) { + $id = $properties['originalId']; + unset($properties['originalId']); + } else { + $id = null; + } + $name = $properties['rule_name']; + + if ($replace && $id && static::existsWithNameAndId($name, $id, $db)) { + $object = static::loadWithAutoIncId($id, $db); + } elseif ($replace && static::exists($name, $db)) { + $object = static::load($name, $db); + } elseif (static::existsWithName($name, $db)) { + throw new DuplicateKeyException( + 'Sync Rule %s already exists', + $name + ); + } else { + $object = static::create([], $db); + } + + $object->newSyncProperties = $properties['properties']; + unset($properties['properties']); + $object->setProperties($properties); + + return $object; + } + + public function getUniqueIdentifier() + { + return $this->get('rule_name'); + } + + /** + * @throws DuplicateKeyException + */ + protected function onStore() + { + parent::onStore(); + if ($this->newSyncProperties !== null) { + $connection = $this->getConnection(); + $db = $connection->getDbAdapter(); + $myId = $this->get('id'); + if ($this->originalId === null) { + $originalId = $myId; + } else { + $originalId = $this->originalId; + $this->originalId = null; + } + if ($this->hasBeenLoadedFromDb()) { + $db->delete( + 'sync_property', + $db->quoteInto('rule_id = ?', $myId) + ); + } + + foreach ($this->newSyncProperties as $property) { + unset($property->rule_name); + $property = SyncProperty::create((array) $property, $connection); + $property->set('rule_id', $myId); + $property->store(); + } + } + } + + /** + * @deprecated + * @return array + */ + protected function exportSyncProperties() + { + $all = []; + $db = $this->getDb(); + $sourceNames = $db->fetchPairs( + $db->select()->from('import_source', ['id', 'source_name']) + ); + + foreach ($this->getSyncProperties() as $property) { + $properties = $property->getProperties(); + $properties['source'] = $sourceNames[$properties['source_id']]; + unset($properties['id']); + unset($properties['rule_id']); + unset($properties['source_id']); + ksort($properties); + $all[] = (object) $properties; + } + + return $all; + } + + /** + * Whether we have a combined key (e.g. services on hosts) + * + * @return bool + */ + public function hasCombinedKey() + { + if ($this->hasCombinedKey === null) { + $this->hasCombinedKey = false; + + // TODO: Move to Objects + if ($this->get('object_type') === 'service') { + $hasHost = false; + $hasObjectName = false; + $hasServiceSet = false; + + foreach ($this->getSyncProperties() as $key => $property) { + if ($property->destination_field === 'host') { + $hasHost = $property->source_expression; + } + if ($property->destination_field === 'service_set') { + $hasServiceSet = $property->source_expression; + } + if ($property->destination_field === 'object_name') { + $hasObjectName = $property->source_expression; + } + } + + if ($hasHost !== false && $hasObjectName !== false) { + $this->hasCombinedKey = true; + $this->sourceKeyPattern = sprintf( + '%s!%s', + $hasHost, + $hasObjectName + ); + + $this->destinationKeyPattern = '${host}!${object_name}'; + } elseif ($hasServiceSet !== false && $hasObjectName !== false) { + $this->hasCombinedKey = true; + $this->sourceKeyPattern = sprintf( + '%s!%s', + $hasServiceSet, + $hasObjectName + ); + + $this->destinationKeyPattern = '${service_set}!${object_name}'; + } + } elseif ($this->get('object_type') === 'serviceSet') { + $hasHost = false; + $hasObjectName = false; + + foreach ($this->getSyncProperties() as $key => $property) { + if ($property->destination_field === 'host') { + $hasHost = $property->source_expression; + } + if ($property->destination_field === 'object_name') { + $hasObjectName = $property->source_expression; + } + } + + if ($hasHost !== false && $hasObjectName !== false) { + $this->hasCombinedKey = true; + $this->sourceKeyPattern = sprintf( + '%s!%s', + $hasHost, + $hasObjectName + ); + + $this->destinationKeyPattern = '${host}!${object_name}'; + } + } elseif ($this->get('object_type') === 'datalistEntry') { + $hasList = false; + $hasName = false; + + foreach ($this->getSyncProperties() as $key => $property) { + if ($property->destination_field === 'list_id') { + $hasList = $property->source_expression; + } + if ($property->destination_field === 'entry_name') { + $hasName = $property->source_expression; + } + } + + if ($hasList !== false && $hasName !== false) { + $this->hasCombinedKey = true; + $this->sourceKeyPattern = sprintf( + '%s!%s', + $hasList, + $hasName + ); + + $this->destinationKeyPattern = '${list_id}!${entry_name}'; + } + } + } + + return $this->hasCombinedKey; + } + + public function hasSyncProperties() + { + $properties = $this->getSyncProperties(); + return ! empty($properties); + } + + /** + * @return SyncProperty[] + */ + public function getSyncProperties() + { + if (! $this->hasBeenLoadedFromDb()) { + return []; + } + + if ($this->syncProperties === null) { + $this->syncProperties = $this->fetchSyncProperties(); + } + + return $this->syncProperties; + } + + public function fetchSyncProperties() + { + $db = $this->getDb(); + + return SyncProperty::loadAll( + $this->getConnection(), + $db->select() + ->from('sync_property') + ->where('rule_id = ?', $this->get('id')) + ->order('priority ASC') + ); + } + + /** + * TODO: implement in a generic way, this is duplicated code + * + * @param string $name + * @param Db $connection + * @api internal + * @return bool + */ + public static function existsWithName($name, Db $connection) + { + $db = $connection->getDbAdapter(); + + return (string) $name === (string) $db->fetchOne( + $db->select() + ->from('sync_rule', 'rule_name') + ->where('rule_name = ?', $name) + ); + } + + /** + * @param string $name + * @param int $id + * @param Db $connection + * @api internal + * @return bool + */ + protected static function existsWithNameAndId($name, $id, Db $connection) + { + $db = $connection->getDbAdapter(); + $dummy = new static; + $idCol = $dummy->autoincKeyName; + $keyCol = $dummy->keyName; + + return (string) $id === (string) $db->fetchOne( + $db->select() + ->from($dummy->table, $idCol) + ->where("$idCol = ?", $id) + ->where("$keyCol = ?", $name) + ); + } +} diff --git a/library/Director/Objects/SyncRun.php b/library/Director/Objects/SyncRun.php new file mode 100644 index 0000000..62f7378 --- /dev/null +++ b/library/Director/Objects/SyncRun.php @@ -0,0 +1,46 @@ +<?php + +namespace Icinga\Module\Director\Objects; + +use Icinga\Module\Director\Data\Db\DbObject; + +class SyncRun extends DbObject +{ + protected $table = 'sync_run'; + + protected $keyName = 'id'; + + protected $autoincKeyName = 'id'; + + protected $defaultProperties = array( + 'id' => null, + 'rule_id' => null, + 'rule_name' => null, + 'start_time' => null, + 'duration_ms' => null, + 'objects_created' => null, + 'objects_deleted' => null, + 'objects_modified' => null, + 'last_former_activity' => null, + 'last_related_activity' => null, + ); + + public static function start(SyncRule $rule) + { + return static::create( + array( + 'start_time' => date('Y-m-d H:i:s'), + 'rule_id' => $rule->id, + 'rule_name' => $rule->rule_name, + ), + $rule->getConnection() + ); + } + + public function countActivities() + { + return (int) $this->get('objects_deleted') + + (int) $this->get('objects_created') + + (int) $this->get('objects_modified'); + } +} |