summaryrefslogtreecommitdiffstats
path: root/library/Director/Objects
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Objects
parentInitial commit. (diff)
downloadicingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz
icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--library/Director/Objects/DirectorActivityLog.php232
-rw-r--r--library/Director/Objects/DirectorDatafield.php344
-rw-r--r--library/Director/Objects/DirectorDatafieldCategory.php64
-rw-r--r--library/Director/Objects/DirectorDatalist.php225
-rw-r--r--library/Director/Objects/DirectorDatalistEntry.php112
-rw-r--r--library/Director/Objects/DirectorDeploymentLog.php199
-rw-r--r--library/Director/Objects/DirectorJob.php314
-rw-r--r--library/Director/Objects/DynamicApplyMatches.php14
-rw-r--r--library/Director/Objects/Extension/Arguments.php61
-rw-r--r--library/Director/Objects/Extension/FlappingSupport.php54
-rw-r--r--library/Director/Objects/Extension/PriorityColumn.php40
-rw-r--r--library/Director/Objects/GroupMembershipResolver.php689
-rw-r--r--library/Director/Objects/HostApplyMatches.php8
-rw-r--r--library/Director/Objects/HostGroupMembershipResolver.php8
-rw-r--r--library/Director/Objects/IcingaApiUser.php31
-rw-r--r--library/Director/Objects/IcingaArguments.php442
-rw-r--r--library/Director/Objects/IcingaCommand.php365
-rw-r--r--library/Director/Objects/IcingaCommandArgument.php263
-rw-r--r--library/Director/Objects/IcingaCommandField.php17
-rw-r--r--library/Director/Objects/IcingaDependency.php631
-rw-r--r--library/Director/Objects/IcingaEndpoint.php157
-rw-r--r--library/Director/Objects/IcingaFlatVar.php61
-rw-r--r--library/Director/Objects/IcingaHost.php668
-rw-r--r--library/Director/Objects/IcingaHostField.php17
-rw-r--r--library/Director/Objects/IcingaHostGroup.php42
-rw-r--r--library/Director/Objects/IcingaHostGroupAssignment.php20
-rw-r--r--library/Director/Objects/IcingaHostVar.php29
-rw-r--r--library/Director/Objects/IcingaNotification.php254
-rw-r--r--library/Director/Objects/IcingaNotificationField.php17
-rw-r--r--library/Director/Objects/IcingaObject.php3258
-rw-r--r--library/Director/Objects/IcingaObjectField.php26
-rw-r--r--library/Director/Objects/IcingaObjectGroup.php76
-rw-r--r--library/Director/Objects/IcingaObjectGroups.php408
-rw-r--r--library/Director/Objects/IcingaObjectImports.php439
-rw-r--r--library/Director/Objects/IcingaObjectLegacyAssignments.php79
-rw-r--r--library/Director/Objects/IcingaObjectMultiRelations.php454
-rw-r--r--library/Director/Objects/IcingaRanges.php321
-rw-r--r--library/Director/Objects/IcingaRelatedObject.php211
-rw-r--r--library/Director/Objects/IcingaScheduledDowntime.php135
-rw-r--r--library/Director/Objects/IcingaScheduledDowntimeRange.php88
-rw-r--r--library/Director/Objects/IcingaScheduledDowntimeRanges.php18
-rw-r--r--library/Director/Objects/IcingaService.php828
-rw-r--r--library/Director/Objects/IcingaServiceAssignment.php20
-rw-r--r--library/Director/Objects/IcingaServiceField.php17
-rw-r--r--library/Director/Objects/IcingaServiceGroup.php42
-rw-r--r--library/Director/Objects/IcingaServiceSet.php591
-rw-r--r--library/Director/Objects/IcingaServiceSetAssignment.php20
-rw-r--r--library/Director/Objects/IcingaServiceVar.php29
-rw-r--r--library/Director/Objects/IcingaTemplateChoice.php321
-rw-r--r--library/Director/Objects/IcingaTemplateChoiceHost.php14
-rw-r--r--library/Director/Objects/IcingaTemplateChoiceService.php14
-rw-r--r--library/Director/Objects/IcingaTemplateResolver.php479
-rw-r--r--library/Director/Objects/IcingaTimePeriod.php190
-rw-r--r--library/Director/Objects/IcingaTimePeriodRange.php88
-rw-r--r--library/Director/Objects/IcingaTimePeriodRanges.php35
-rw-r--r--library/Director/Objects/IcingaUser.php92
-rw-r--r--library/Director/Objects/IcingaUserField.php17
-rw-r--r--library/Director/Objects/IcingaUserGroup.php29
-rw-r--r--library/Director/Objects/IcingaVar.php72
-rw-r--r--library/Director/Objects/IcingaZone.php110
-rw-r--r--library/Director/Objects/ImportExportHelper.php68
-rw-r--r--library/Director/Objects/ImportRowModifier.php91
-rw-r--r--library/Director/Objects/ImportRun.php159
-rw-r--r--library/Director/Objects/ImportSource.php537
-rw-r--r--library/Director/Objects/InstantiatedViaHook.php14
-rw-r--r--library/Director/Objects/ObjectApplyMatches.php239
-rw-r--r--library/Director/Objects/ObjectWithArguments.php18
-rw-r--r--library/Director/Objects/ServiceGroupMembershipResolver.php8
-rw-r--r--library/Director/Objects/SyncProperty.php48
-rw-r--r--library/Director/Objects/SyncRule.php553
-rw-r--r--library/Director/Objects/SyncRun.php46
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');
+ }
+}