From b2afca24965bbbe758158abcc19effe52f3ae015 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Fri, 1 Jul 2022 08:36:01 +0200 Subject: [PATCH 01/38] Sync: support branches --- application/controllers/BranchController.php | 15 +- .../controllers/SyncruleController.php | 133 ++++++++++++++++-- application/forms/SyncRunForm.php | 66 ++++++--- library/Director/Data/Db/DbObjectStore.php | 73 ++++++++++ library/Director/Db/Branch/Branch.php | 7 + library/Director/Db/Branch/BranchActivity.php | 12 +- library/Director/Db/Branch/BranchStore.php | 125 +++++++++++++++- library/Director/Import/Sync.php | 65 ++++++--- library/Director/Web/Form/ClickHereForm.php | 31 ++++ .../Web/Table/BranchActivityTable.php | 13 +- 10 files changed, 477 insertions(+), 63 deletions(-) create mode 100644 library/Director/Web/Form/ClickHereForm.php diff --git a/application/controllers/BranchController.php b/application/controllers/BranchController.php index 0afd5973..cdbab3a8 100644 --- a/application/controllers/BranchController.php +++ b/application/controllers/BranchController.php @@ -8,8 +8,10 @@ use gipfl\IcingaWeb2\Widget\NameValueTable; use Icinga\Module\Director\Data\Db\DbObjectStore; use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db\Branch\BranchActivity; +use Icinga\Module\Director\Db\Branch\BranchStore; use Icinga\Module\Director\IcingaConfig\IcingaConfig; use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\SyncRule; use Icinga\Module\Director\PlainObjectRenderer; use Icinga\Module\Director\Web\Controller\ActionController; use Icinga\Module\Director\Web\Controller\BranchHelper; @@ -24,6 +26,7 @@ class BranchController extends ActionController { parent::init(); IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); + SyncRule::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); } protected function checkDirectorPermissions() @@ -34,9 +37,17 @@ class BranchController extends ActionController { $this->assertPermission('director/showconfig'); $ts = $this->params->getRequired('ts'); - $this->addSingleTab($this->translate('Activity')); - $this->addTitle($this->translate('Branch Activity')); $activity = BranchActivity::load($ts, $this->db()); + $store = new BranchStore($this->db()); + $branch = $store->fetchBranchByUuid($activity->getBranchUuid()); + if ($branch->isSyncPreview()) { + $this->addSingleTab($this->translate('Sync Preview')); + $this->addTitle($this->translate('Expected Modification')); + } else { + $this->addSingleTab($this->translate('Activity')); + $this->addTitle($this->translate('Branch Activity')); + } + $this->content()->add($this->prepareActivityInfo($activity)); $this->showActivity($activity); } diff --git a/application/controllers/SyncruleController.php b/application/controllers/SyncruleController.php index 9780025e..c9a60530 100644 --- a/application/controllers/SyncruleController.php +++ b/application/controllers/SyncruleController.php @@ -4,6 +4,14 @@ namespace Icinga\Module\Director\Controllers; use gipfl\IcingaWeb2\Link; use gipfl\Web\Widget\Hint; +use Icinga\Date\DateFormatter; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; +use Icinga\Module\Director\Db\Branch\Branch; +use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\Web\Controller\BranchHelper; +use Icinga\Module\Director\Web\Form\ClickHereForm; +use Icinga\Module\Director\Web\Table\BranchActivityTable; use Icinga\Module\Director\Web\Widget\IcingaConfigDiff; use Icinga\Module\Director\Web\Widget\UnorderedList; use Icinga\Module\Director\Db\Cache\PrefetchCache; @@ -26,11 +34,14 @@ use Icinga\Module\Director\Web\Table\SyncpropertyTable; use Icinga\Module\Director\Web\Table\SyncRunTable; use Icinga\Module\Director\Web\Tabs\SyncRuleTabs; use Icinga\Module\Director\Web\Widget\SyncRunDetails; +use Icinga\Web\Notification; use ipl\Html\Form; use ipl\Html\Html; class SyncruleController extends ActionController { + use BranchHelper; + /** * @throws \Icinga\Exception\NotFoundError */ @@ -43,7 +54,18 @@ class SyncruleController extends ActionController $this->addTitle($this->translate('Sync rule: %s'), $ruleName); $checkForm = SyncCheckForm::load()->setSyncRule($rule)->handleRequest(); - $runForm = SyncRunForm::load()->setSyncRule($rule)->handleRequest(); + $store = new DbObjectStore($this->db(), $this->getBranch()); + $runForm = new SyncRunForm($rule, $store); + $runForm->on(SyncRunForm::ON_SUCCESS, function (SyncRunForm $form) { + $message = $form->getSuccessMessage(); + if ($message === null) { + Notification::error($this->translate('Synchronization failed')); + } else { + Notification::success($message); + } + $this->redirectNow($this->url()); + }); + $runForm->handleRequest($this->getServerRequest()); if ($lastRunId = $rule->getLastSyncRunId()) { $run = SyncRun::load($lastRunId, $this->db()); @@ -98,6 +120,15 @@ class SyncruleController extends ActionController } $c->add($checkForm); + if ($this->hasBranch()) { + $objectType = $rule->get('object_type'); + $table = DbObjectTypeRegistry::tableNameByType($objectType); + if (! $this->tableHasBranchSupport($table)) { + $this->showNotInBranch(sprintf($this->translate("Synchronizing '%s'"), $objectType)); + return; + } + } + $c->add($runForm); if ($run) { @@ -143,18 +174,61 @@ class SyncruleController extends ActionController { $rule = $this->requireSyncRule(); // $rule->set('update_policy', 'replace'); + $branchStore = new BranchStore($this->db()); + $tmpBranchName = Branch::PREFIX_SYNC_PREVIEW . '/' . $rule->get('id'); + $owner = $this->getAuth()->getUser()->getUsername(); + if ($this->getBranch()->isBranch()) { + // We could keep changes for preview on branch too + $branchStore->deleteByName($tmpBranchName); + $tmpBranch = $branchStore->cloneBranchForSync($this->getBranch(), $tmpBranchName, $owner); + $after = 1600000000; // a date in 2020, minus 10000000 + } else { + $tmpBranch = $branchStore->fetchOrCreateByName($tmpBranchName, $owner); + $after = null; + } + $store = new DbObjectStore($this->db(), $tmpBranch); + $this->tabs(new SyncRuleTabs($rule))->activate('preview'); - $this->addTitle('Sync Preview'); - $sync = new Sync($rule); + $this->addTitle($this->translate('Sync Preview')); + $sync = new Sync($rule, $store); + + $fetchExpected = true; + if ($tmpBranch) { + if ($lastTime = $branchStore->getLastActivityTime($tmpBranch, $after)) { + if ((time() - $lastTime) > 100) { + $branchStore->wipeBranch($tmpBranch, $after); + } else { + $here = (new ClickHereForm())->handleRequest($this->getServerRequest()); + if ($here->hasBeenClicked()) { + $branchStore->wipeBranch($tmpBranch, $after); + } else { + $fetchExpected = false; + } + $this->content()->add(Hint::info(Html::sprintf( + $this->translate('This preview has been generated %s, please click %s to regenerate it'), + DateFormatter::timeAgo($lastTime), + $here + ))); + } + } + } + try { - $modifications = $sync->getExpectedModifications(); + if ($fetchExpected) { + $modifications = $sync->getExpectedModifications(); + if ($tmpBranch) { + $sync->apply(); + } + } else { + return; + } } catch (\Exception $e) { $this->content()->add(Hint::error($e->getMessage())); return; } - if (empty($modifications)) { + if (empty($modifications) && $tmpBranch === null) { $this->content()->add(Hint::ok($this->translate( 'This Sync Rule is in sync and would currently not apply any changes' ))); @@ -162,11 +236,25 @@ class SyncruleController extends ActionController return; } + if ($tmpBranch) { + if (!$fetchExpected) { + $sync->apply(); + } + $changes = new BranchActivityTable($tmpBranch->getUuid(), $this->db()); + $changes->disableObjectLink(); + $changes->renderTo($this); + return; + } + + $this->showExpectedModificationSummary($modifications); + } + + protected function showExpectedModificationSummary($modifications) + { $create = []; $modify = []; $delete = []; $modifiedProperties = []; - /** @var IcingaObject $object */ foreach ($modifications as $object) { if ($object->hasBeenLoadedFromDb()) { @@ -416,9 +504,16 @@ class SyncruleController extends ActionController if (! $rule->hasSyncProperties()) { $this->addPropertyHint($rule); } + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } + } else { $this->addTitle($this->translate('Add sync rule')); $this->tabs(new SyncRuleTabs())->activate('add'); + if ($this->showNotInBranch($this->translate('Creating Sync Rules'))) { + return; + } } $form->handleRequest(); @@ -451,6 +546,9 @@ class SyncruleController extends ActionController ['class' => 'icon-paste'] ) ); + if ($this->showNotInBranch($this->translate('Cloning Sync Rules'))) { + return; + } $form = new CloneSyncRuleForm($rule); $this->content()->add($form); @@ -499,6 +597,14 @@ class SyncruleController extends ActionController $ruleId = (int) $rule->get('id'); $form = SyncPropertyForm::load()->setDb($db); + $this->tabs(new SyncRuleTabs($rule))->activate('property'); + $this->actions()->add(new Link( + $this->translate('back'), + 'director/syncrule/property', + ['rule_id' => $ruleId], + ['class' => 'icon-left-big'] + )); + if ($id = $this->params->get('id')) { $form->loadObject((int) $id); $this->addTitle( @@ -506,24 +612,21 @@ class SyncruleController extends ActionController $form->getObject()->get('destination_field'), $rule->get('rule_name') ); + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } } else { $this->addTitle( $this->translate('Add sync property: %s'), $rule->get('rule_name') ); + if ($this->showNotInBranch($this->translate('Modifying Sync Rules'))) { + return; + } } $form->setRule($rule); $form->setSuccessUrl('director/syncrule/property', ['rule_id' => $ruleId]); - - $this->actions()->add(new Link( - $this->translate('back'), - 'director/syncrule/property', - ['rule_id' => $ruleId], - ['class' => 'icon-left-big'] - )); - $this->content()->add($form->handleRequest()); - $this->tabs(new SyncRuleTabs($rule))->activate('property'); SyncpropertyTable::create($rule) ->handleSortPriorityActions($this->getRequest(), $this->getResponse()) ->renderTo($this); diff --git a/application/forms/SyncRunForm.php b/application/forms/SyncRunForm.php index 864e1f82..0bc5fda4 100644 --- a/application/forms/SyncRunForm.php +++ b/application/forms/SyncRunForm.php @@ -2,44 +2,66 @@ namespace Icinga\Module\Director\Forms; +use gipfl\Translation\TranslationHelper; +use gipfl\Web\Form; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Import\Sync; use Icinga\Module\Director\Objects\SyncRule; -use Icinga\Module\Director\Web\Form\DirectorForm; -class SyncRunForm extends DirectorForm +class SyncRunForm extends Form { + use TranslationHelper; + + protected $defaultDecoratorClass = null; + + /** @var ?string */ + protected $successMessage = null; + /** @var SyncRule */ protected $rule; - public function setSyncRule(SyncRule $rule) + /** @var DbObjectStore */ + protected $store; + + public function __construct(SyncRule $rule, DbObjectStore $store) { $this->rule = $rule; - return $this; + $this->store = $store; } - public function setup() + public function assemble() { - $this->submitLabel = false; - $this->addElement('submit', 'submit', array( - 'label' => $this->translate('Trigger this Sync'), - 'decorators' => array('ViewHelper') - )); + if ($this->store->getBranch()->isBranch()) { + $label = sprintf($this->translate('Sync to Branch: %s'), $this->store->getBranch()->getName()); + } else { + $label = $this->translate('Trigger this Sync'); + } + $this->addElement('submit', 'submit', [ + 'label' => $label, + ]); + } + + /** + * @return string|null + */ + public function getSuccessMessage() + { + return $this->successMessage; } public function onSuccess() { - $rule = $this->rule; - $changed = $rule->applyChanges(); - - if ($changed) { - $this->setSuccessMessage( - $this->translate(('Source has successfully been synchronized')) - ); - } elseif ($rule->get('sync_state') === 'in-sync') { - $this->notifySuccess( - $this->translate('Nothing changed, rule is in sync') - ); + $sync = new Sync($this->rule, $this->store); + if ($sync->hasModifications()) { + if ($sync->apply()) { + // and changed + $this->successMessage = $this->translate(('Source has successfully been synchronized')); + } else { + $this->successMessage = $this->translate('Nothing changed, rule is in sync'); + } } else { - $this->addError($this->translate('Synchronization failed')); + // Used to be $rule->get('sync_state') === 'in-sync', $changed = $rule->applyChanges(); + $this->successMessage = $this->translate('Nothing to do, rule is in sync'); } } } diff --git a/library/Director/Data/Db/DbObjectStore.php b/library/Director/Data/Db/DbObjectStore.php index 89721108..cc2a8836 100644 --- a/library/Director/Data/Db/DbObjectStore.php +++ b/library/Director/Data/Db/DbObjectStore.php @@ -6,6 +6,10 @@ use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Db\Branch\BranchActivity; use Icinga\Module\Director\Db\Branch\BranchedObject; +use Icinga\Module\Director\Db\Branch\MergeErrorDeleteMissingObject; +use Icinga\Module\Director\Db\Branch\MergeErrorModificationForMissingObject; +use Icinga\Module\Director\Db\Branch\MergeErrorRecreateOnMerge; +use Icinga\Module\Director\Objects\IcingaObject; use Ramsey\Uuid\UuidInterface; /** @@ -48,6 +52,75 @@ class DbObjectStore return $object; } + /** + * @param string $tableName + * @param string $arrayIdx + * @return DbObject[]|IcingaObject[] + * @throws MergeErrorRecreateOnMerge + * @throws MergeErrorDeleteMissingObject + * @throws MergeErrorModificationForMissingObject + */ + public function loadAll($tableName, $arrayIdx = 'uuid') + { + $db = $this->connection->getDbAdapter(); + $class = DbObjectTypeRegistry::classByType($tableName); + $query = $db->select()->from($tableName)->order('uuid'); + $result = []; + foreach ($db->fetchAll($query) as $row) { + $result[$row->uuid] = $class::create((array) $row, $this->connection); + $result[$row->uuid]->setBeingLoadedFromDb(); + } + if ($this->branch && $this->branch->isBranch()) { + $query = $db->select() + ->from(BranchActivity::DB_TABLE) + ->where('branch_uuid = ?', $this->connection->quoteBinary($this->branch->getUuid()->getBytes())) + ->order('timestamp_ns ASC'); + $rows = $db->fetchAll($query); + foreach ($rows as $row) { + $activity = BranchActivity::fromDbRow($row); + if ($activity->getObjectTable() !== $tableName) { + continue; + } + $uuid = $activity->getObjectUuid(); + $binaryUuid = $uuid->getBytes(); + + $exists = isset($result[$binaryUuid]); + if ($activity->isActionCreate()) { + if ($exists) { + throw new MergeErrorRecreateOnMerge($activity); + } else { + $new = $activity->createDbObject($this->connection); + $new->setBeingLoadedFromDb(); + $result[$binaryUuid] = $new; + } + } elseif ($activity->isActionDelete()) { + if ($exists) { + unset($result[$binaryUuid]); + } else { + throw new MergeErrorDeleteMissingObject($activity); + } + } else { + if ($exists) { + $activity->applyToDbObject($result[$binaryUuid])->setBeingLoadedFromDb(); + } else { + throw new MergeErrorModificationForMissingObject($activity); + } + } + } + } + + if ($arrayIdx === 'uuid') { + return $result; + } + + $indexedResult = []; + foreach ($result as $object) { + $indexedResult[$object->get($arrayIdx)] = $object; + } + + return $indexedResult; + } + public function exists($tableName, UuidInterface $uuid) { return BranchedObject::exists($this->connection, $tableName, $uuid, $this->branch->getUuid()); diff --git a/library/Director/Db/Branch/Branch.php b/library/Director/Db/Branch/Branch.php index b75a3e6a..83c26798 100644 --- a/library/Director/Db/Branch/Branch.php +++ b/library/Director/Db/Branch/Branch.php @@ -18,6 +18,8 @@ use stdClass; */ class Branch { + const PREFIX_SYNC_PREVIEW = '/syncpreview'; + /** @var UuidInterface|null */ protected $branchUuid; @@ -186,4 +188,9 @@ class Branch { return $this->owner; } + + public function isSyncPreview() + { + return (bool) preg_match('/^' . preg_quote(self::PREFIX_SYNC_PREVIEW, '/') . '\//', $this->getName()); + } } diff --git a/library/Director/Db/Branch/BranchActivity.php b/library/Director/Db/Branch/BranchActivity.php index 097af268..3812e753 100644 --- a/library/Director/Db/Branch/BranchActivity.php +++ b/library/Director/Db/Branch/BranchActivity.php @@ -121,6 +121,16 @@ class BranchActivity ); } + public static function fixFakeTimestamp($timestampNs) + { + if ($timestampNs < 1600000000 * 1000000) { + // fake TS for cloned branch in sync preview + return (int) $timestampNs * 1000000; + } + + return $timestampNs; + } + public function applyToDbObject(DbObject $object) { if (!$this->isActionModify()) { @@ -260,7 +270,7 @@ class BranchActivity */ public function getTimestamp() { - return (int) floor($this->timestampNs / 1000000); + return (int) floor(BranchActivity::fixFakeTimestamp($this->timestampNs) / 1000000); } /** diff --git a/library/Director/Db/Branch/BranchStore.php b/library/Director/Db/Branch/BranchStore.php index e8a96b32..65b8f73c 100644 --- a/library/Director/Db/Branch/BranchStore.php +++ b/library/Director/Db/Branch/BranchStore.php @@ -3,17 +3,36 @@ namespace Icinga\Module\Director\Db\Branch; use Icinga\Module\Director\Db; +use Icinga\Module\Director\Db\DbUtil; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; class BranchStore { + const TABLE = 'director_branch'; + const TABLE_ACTIVITY = 'director_branch_activity'; + const OBJECT_TABLES = [ + 'branched_icinga_apiuser', + 'branched_icinga_command', + 'branched_icinga_dependency', + 'branched_icinga_endpoint', + 'branched_icinga_host', + 'branched_icinga_hostgroup', + 'branched_icinga_notification', + 'branched_icinga_scheduled_downtime', + 'branched_icinga_service', + 'branched_icinga_service_set', + 'branched_icinga_servicegroup', + 'branched_icinga_timeperiod', + 'branched_icinga_user', + 'branched_icinga_usergroup', + 'branched_icinga_zone', + ]; + protected $connection; protected $db; - protected $table = 'director_branch'; - public function __construct(Db $connection) { $this->connection = $connection; @@ -40,6 +59,67 @@ class BranchStore return $this->newFromDbResult($this->select()->where('b.branch_name = ?', $name)); } + public function cloneBranchForSync(Branch $branch, $newName, $owner) + { + $this->runTransaction(function ($db) use ($branch, $newName, $owner) { + $tables = self::OBJECT_TABLES; + $tables[] = self::TABLE_ACTIVITY; + $newBranch = $this->createBranchByName($newName, $owner); + $oldQuotedUuid = DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db); + $quotedUuid = DbUtil::quoteBinaryCompat($newBranch->getUuid()->getBytes(), $db); + // $timestampNs = (int)floor(microtime(true) * 1000000); + // Hint: would love to do SELECT *, $quotedUuid AS branch_uuid FROM $table INTO $table + foreach ($tables as $table) { + $rows = $db->fetchAll($db->select()->from($table)->where('branch_uuid = ?', $oldQuotedUuid)); + foreach ($rows as $row) { + $modified = (array)$row; + $modified['branch_uuid'] = $quotedUuid; + if ($table === self::TABLE_ACTIVITY) { + $modified['timestamp_ns'] = round($modified['timestamp_ns'] / 1000000); + } + $db->insert($table, $modified); + } + } + }); + + return $this->fetchBranchByName($newName); + } + + protected function runTransaction($callback) + { + $db = $this->db; + $db->beginTransaction(); + try { + $callback($db); + $db->commit(); + } catch (\Exception $e) { + try { + $db->rollBack(); + } catch (\Exception $ignored) { + // + } + throw $e; + } + } + + public function wipeBranch(Branch $branch, $after = null) + { + $this->runTransaction(function ($db) use ($branch, $after) { + $tables = self::OBJECT_TABLES; + $tables[] = self::TABLE_ACTIVITY; + $quotedUuid = DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db); + $where = $db->quoteInto('branch_uuid = ?', $quotedUuid); + foreach ($tables as $table) { + if ($after && $table === self::TABLE_ACTIVITY) { + $db->delete($table, $where . ' AND timestamp_ns > ' . (int) $after); + } else { + $db->delete($table, $where); + } + } + }); + + } + protected function newFromDbResult($query) { if ($row = $this->db->fetchRow($query)) { @@ -78,7 +158,7 @@ class BranchStore 'ts_merge_request' => 'b.ts_merge_request', 'cnt_activities' => 'COUNT(ba.timestamp_ns)', ])->joinLeft( - ['ba' => 'director_branch_activity'], + ['ba' => self::TABLE_ACTIVITY], 'b.uuid = ba.branch_uuid', [] )->group('b.uuid'); @@ -114,7 +194,7 @@ class BranchStore 'description' => null, 'ts_merge_request' => null, ]; - $this->db->insert($this->table, $properties); + $this->db->insert(self::TABLE, $properties); if ($branch = static::fetchBranchByUuid($uuid)) { return $branch; @@ -128,14 +208,49 @@ class BranchStore public function deleteByUuid(UuidInterface $uuid) { - return $this->db->delete($this->table, $this->db->quoteInto( + return $this->db->delete(self::TABLE, $this->db->quoteInto( 'uuid = ?', $this->connection->quoteBinary($uuid->getBytes()) )); } + /** + * @param string $name + * @return int + */ + public function deleteByName($name) + { + return $this->db->delete(self::TABLE, $this->db->quoteInto( + 'branch_name = ?', + $name + )); + } + public function delete(Branch $branch) { return $this->deleteByUuid($branch->getUuid()); } + + /** + * @param Branch $branch + * @param ?int $after + * @return float|null + */ + public function getLastActivityTime(Branch $branch, $after = null) + { + $db = $this->db; + $query = $db->select() + ->from(self::TABLE_ACTIVITY, 'MAX(timestamp_ns)') + ->where('branch_uuid = ?', DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db)); + if ($after) { + $query->where('timestamp_ns > ?', (int) $after); + } + + $last = $db->fetchOne($query); + if ($last) { + return $last / 1000000; + } + + return null; + } } diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php index 5704d756..947d0714 100644 --- a/library/Director/Import/Sync.php +++ b/library/Director/Import/Sync.php @@ -7,6 +7,8 @@ use Icinga\Application\Benchmark; use Icinga\Data\Filter\Filter; use Icinga\Module\Director\Application\MemoryLimit; use Icinga\Module\Director\Data\Db\DbObject; +use Icinga\Module\Director\Data\Db\DbObjectStore; +use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db; use Icinga\Module\Director\Db\Cache\PrefetchCache; use Icinga\Module\Director\Objects\HostGroupMembershipResolver; @@ -76,13 +78,18 @@ class Sync /** @var HostGroupMembershipResolver|bool */ protected $hostGroupMembershipResolver; + /** @var ?DbObjectStore */ + protected $store; + /** * @param SyncRule $rule + * @param ?DbObjectStore $store */ - public function __construct(SyncRule $rule) + public function __construct(SyncRule $rule, DbObjectStore $store = null) { $this->rule = $rule; $this->db = $rule->getConnection(); + $this->store = $store; } /** @@ -388,11 +395,13 @@ class Sync if ($this->rule->hasCombinedKey()) { $this->objects = []; $destinationKeyPattern = $this->rule->getDestinationKeyPattern(); + if ($this->store) { + $objects = $this->store->loadAll(DbObjectTypeRegistry::tableNameByType($ruleObjectType)); + } else { + $objects = IcingaObject::loadAllByType($ruleObjectType, $this->db); + } - foreach (IcingaObject::loadAllByType( - $ruleObjectType, - $this->db - ) as $object) { + foreach ($objects as $object) { if ($object instanceof IcingaService) { if (strstr($destinationKeyPattern, '${host}') && $object->get('host_id') === null @@ -421,10 +430,11 @@ class Sync $this->objects[$key] = $object; } } else { - $this->objects = IcingaObject::loadAllByType( - $ruleObjectType, - $this->db - ); + if ($this->store) { + $this->objects = $this->store->loadAll(DbObjectTypeRegistry::tableNameByType($ruleObjectType), 'object_name'); + } else { + $this->objects = IcingaObject::loadAllByType($ruleObjectType, $this->db); + } } // TODO: should be obsoleted by a better "loadFiltered" method @@ -759,7 +769,9 @@ class Sync $objects = $this->prepare(); $db = $this->db; $dba = $db->getDbAdapter(); - $dba->beginTransaction(); + if (! $this->store) { // store has it's own transaction + $dba->beginTransaction(); + } $object = null; $updateOnly = $this->rule->get('update_policy') === 'update-only'; @@ -777,7 +789,11 @@ class Sync foreach ($objects as $object) { $this->setResolver($object); if (! $updateOnly && $object->shouldBeRemoved()) { - $object->delete(); + if ($this->store) { + $this->store->delete($object); + } else { + $object->delete(); + } $deleted++; continue; } @@ -785,10 +801,18 @@ class Sync if ($object->hasBeenModified()) { $existing = $object->hasBeenLoadedFromDb(); if ($existing) { - $object->store($db); + if ($this->store) { + $this->store->store($object); + } else { + $object->store($db); + } $modified++; } elseif ($allowCreate) { - $object->store($db); + if ($this->store) { + $this->store->store($object); + } else { + $object->store($db); + } $created++; } } @@ -810,7 +834,9 @@ class Sync $this->run->setProperties($runProperties)->store(); $this->notifyResolvers(); - $dba->commit(); + if (! $this->store) { + $dba->commit(); + } // Store duration after commit, as the commit might take some time $this->run->set('duration_ms', (int) round( @@ -819,13 +845,15 @@ class Sync Benchmark::measure('Done applying objects'); } catch (Exception $e) { - $dba->rollBack(); + if (! $this->store) { + $dba->rollBack(); + } - if ($object !== null && $object instanceof IcingaObject) { + if ($object instanceof IcingaObject) { throw new IcingaException( 'Exception while syncing %s %s: %s', get_class($object), - $object->get('object_name'), + $object->getObjectName(), $e->getMessage(), $e ); @@ -839,6 +867,9 @@ class Sync protected function prepareCache() { + if ($this->store) { + return $this; + } PrefetchCache::initialize($this->db); IcingaTemplateRepository::clear(); diff --git a/library/Director/Web/Form/ClickHereForm.php b/library/Director/Web/Form/ClickHereForm.php new file mode 100644 index 00000000..abba9d76 --- /dev/null +++ b/library/Director/Web/Form/ClickHereForm.php @@ -0,0 +1,31 @@ +addElement('submit', 'submit', [ + 'label' => $this->translate('here'), + 'class' => 'link-button' + ]); + } + + public function hasBeenClicked() + { + return $this->hasBeenClicked; + } + + public function onSuccess() + { + $this->hasBeenClicked = true; + } +} diff --git a/library/Director/Web/Table/BranchActivityTable.php b/library/Director/Web/Table/BranchActivityTable.php index d3a867a9..e7131efc 100644 --- a/library/Director/Web/Table/BranchActivityTable.php +++ b/library/Director/Web/Table/BranchActivityTable.php @@ -23,6 +23,8 @@ class BranchActivityTable extends ZfQueryBasedTable /** @var LocalTimeFormat */ protected $timeFormat; + protected $linkToObject = true; + public function __construct(UuidInterface $branchUuid, $db, UuidInterface $objectUuid = null) { $this->branchUuid = $branchUuid; @@ -38,7 +40,7 @@ class BranchActivityTable extends ZfQueryBasedTable public function renderRow($row) { - $ts = (int) floor($row->timestamp_ns / 1000000); + $ts = (int) floor(BranchActivity::fixFakeTimestamp($row->timestamp_ns) / 1000000); $this->splitByDay($ts); $activity = BranchActivity::fromDbRow($row); return $this::tr([ @@ -47,8 +49,17 @@ class BranchActivityTable extends ZfQueryBasedTable ])->addAttributes(['class' => ['action-' . $activity->getAction(), 'branched']]); } + public function disableObjectLink() + { + $this->linkToObject = false; + return $this; + } + protected function linkObject(BranchActivity $activity) { + if (! $this->linkToObject) { + return $activity->getObjectName(); + } // $type, UuidInterface $uuid // Later on replacing, service_set -> serviceset $type = preg_replace('/^icinga_/', '', $activity->getObjectTable()); From 956708475e737b8b936d76db4aecbd168d2bd2bf Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 25 Aug 2022 14:42:17 +0200 Subject: [PATCH 02/38] BranchSupport: new helper class --- library/Director/Db/Branch/BranchSupport.php | 38 +++++++++++++++++++ .../Director/Web/Controller/BranchHelper.php | 25 +----------- 2 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 library/Director/Db/Branch/BranchSupport.php diff --git a/library/Director/Db/Branch/BranchSupport.php b/library/Director/Db/Branch/BranchSupport.php new file mode 100644 index 00000000..dc8e81bb --- /dev/null +++ b/library/Director/Db/Branch/BranchSupport.php @@ -0,0 +1,38 @@ +get('object_type')) + ); + } +} diff --git a/library/Director/Web/Controller/BranchHelper.php b/library/Director/Web/Controller/BranchHelper.php index 1b75a256..ac2a4806 100644 --- a/library/Director/Web/Controller/BranchHelper.php +++ b/library/Director/Web/Controller/BranchHelper.php @@ -6,6 +6,7 @@ use Icinga\Module\Director\Data\Db\DbObjectStore; use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\Db\Branch\BranchSupport; use Icinga\Module\Director\Objects\IcingaObject; use Icinga\Module\Director\Web\Widget\NotInBranchedHint; @@ -17,23 +18,6 @@ trait BranchHelper /** @var BranchStore */ protected $branchStore; - protected static $banchedTables = [ - 'icinga_apiuser', - 'icinga_command', - 'icinga_dependency', - 'icinga_endpoint', - 'icinga_host', - 'icinga_hostgroup', - 'icinga_notification', - 'icinga_scheduled_downtime', - 'icinga_service', - 'icinga_servicegroup', - 'icinga_timeperiod', - 'icinga_user', - 'icinga_usergroup', - 'icinga_zone', - ]; - /** * @return false|\Ramsey\Uuid\UuidInterface */ @@ -69,14 +53,9 @@ trait BranchHelper return $this->getBranchUuid() !== null; } - protected function tableHasBranchSupport($table) - { - return in_array($table, self::$banchedTables, true); - } - protected function enableStaticObjectLoader($table) { - if ($this->tableHasBranchSupport($table)) { + if (BranchSupport::existsForTableName($table)) { IcingaObject::setDbObjectStore(new DbObjectStore($this->db(), $this->getBranch())); } } From ae45844bac9cd70ed34e7357867c256c26065685 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 29 Aug 2022 08:53:49 +0200 Subject: [PATCH 03/38] Sync: fix typo, remove useless cast --- library/Director/Import/Sync.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php index 947d0714..fc685181 100644 --- a/library/Director/Import/Sync.php +++ b/library/Director/Import/Sync.php @@ -367,13 +367,13 @@ class Sync if ($listId === null) { throw new InvalidArgumentException( - 'Cannot sync datalist entry without list_ist' + 'Cannot sync datalist entry without list_id' ); } $no = []; foreach ($this->objects as $k => $o) { - if ((int) $o->get('list_id') !== (int) $listId) { + if ((int) $o->get('list_id') !== $listId) { $no[] = $k; } } From 98cfcafdcdaab244b44881405dbbff7342347381 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 29 Aug 2022 08:55:46 +0200 Subject: [PATCH 04/38] BranchSupport: introduce new constants --- .../Branch/BranchModificationInspection.php | 24 +++--- library/Director/Db/Branch/BranchStore.php | 21 +---- library/Director/Db/Branch/BranchSupport.php | 77 +++++++++++++++---- 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/library/Director/Db/Branch/BranchModificationInspection.php b/library/Director/Db/Branch/BranchModificationInspection.php index 0faf2d28..0181b7db 100644 --- a/library/Director/Db/Branch/BranchModificationInspection.php +++ b/library/Director/Db/Branch/BranchModificationInspection.php @@ -31,16 +31,20 @@ class BranchModificationInspection public function describeBranch(UuidInterface $uuid) { $tables = [ - $this->translate('API Users') => 'branched_icinga_apiuser', - $this->translate('Endpoints') => 'branched_icinga_endpoint', - $this->translate('Zones') => 'branched_icinga_zone', - $this->translate('Commands') => 'branched_icinga_command', - $this->translate('Hosts') => 'branched_icinga_host', - $this->translate('Hostgroups') => 'branched_icinga_hostgroup', - $this->translate('Services') => 'branched_icinga_service', - $this->translate('Servicegroups') => 'branched_icinga_servicegroup', - $this->translate('Users') => 'branched_icinga_user', - $this->translate('Timeperiods') => 'branched_icinga_timeperiod', + $this->translate('API Users') => BranchSupport::BRANCHED_TABLE_ICINGA_APIUSER, + $this->translate('Endpoints') => BranchSupport::BRANCHED_TABLE_ICINGA_COMMAND, + $this->translate('Zones') => BranchSupport::BRANCHED_TABLE_ICINGA_DEPENDENCY, + $this->translate('Commands') => BranchSupport::BRANCHED_TABLE_ICINGA_ENDPOINT, + $this->translate('Hosts') => BranchSupport::BRANCHED_TABLE_ICINGA_HOST, + $this->translate('Hostgroups') => BranchSupport::BRANCHED_TABLE_ICINGA_HOSTGROUP, + $this->translate('Services') => BranchSupport::BRANCHED_TABLE_ICINGA_NOTIFICATION, + $this->translate('Servicegroups') => BranchSupport::BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME, + $this->translate('Users') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE, + $this->translate('Usergroups') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICEGROUP, + $this->translate('Timeperiods') => BranchSupport::BRANCHED_TABLE_ICINGA_TIMEPERIOD, + $this->translate('Notifications') => BranchSupport::BRANCHED_TABLE_ICINGA_USER, + $this->translate('Dependencies') => BranchSupport::BRANCHED_TABLE_ICINGA_USERGROUP, + $this->translate('Scheduled Downtimes') => BranchSupport::BRANCHED_TABLE_ICINGA_ZONE, ]; $parts = new HtmlDocument(); diff --git a/library/Director/Db/Branch/BranchStore.php b/library/Director/Db/Branch/BranchStore.php index 65b8f73c..78bc88ab 100644 --- a/library/Director/Db/Branch/BranchStore.php +++ b/library/Director/Db/Branch/BranchStore.php @@ -11,23 +11,6 @@ class BranchStore { const TABLE = 'director_branch'; const TABLE_ACTIVITY = 'director_branch_activity'; - const OBJECT_TABLES = [ - 'branched_icinga_apiuser', - 'branched_icinga_command', - 'branched_icinga_dependency', - 'branched_icinga_endpoint', - 'branched_icinga_host', - 'branched_icinga_hostgroup', - 'branched_icinga_notification', - 'branched_icinga_scheduled_downtime', - 'branched_icinga_service', - 'branched_icinga_service_set', - 'branched_icinga_servicegroup', - 'branched_icinga_timeperiod', - 'branched_icinga_user', - 'branched_icinga_usergroup', - 'branched_icinga_zone', - ]; protected $connection; @@ -62,7 +45,7 @@ class BranchStore public function cloneBranchForSync(Branch $branch, $newName, $owner) { $this->runTransaction(function ($db) use ($branch, $newName, $owner) { - $tables = self::OBJECT_TABLES; + $tables = BranchSupport::OBJECT_TABLES; $tables[] = self::TABLE_ACTIVITY; $newBranch = $this->createBranchByName($newName, $owner); $oldQuotedUuid = DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db); @@ -105,7 +88,7 @@ class BranchStore public function wipeBranch(Branch $branch, $after = null) { $this->runTransaction(function ($db) use ($branch, $after) { - $tables = self::OBJECT_TABLES; + $tables = BranchSupport::OBJECT_TABLES; $tables[] = self::TABLE_ACTIVITY; $quotedUuid = DbUtil::quoteBinaryCompat($branch->getUuid()->getBytes(), $db); $where = $db->quoteInto('branch_uuid = ?', $quotedUuid); diff --git a/library/Director/Db/Branch/BranchSupport.php b/library/Director/Db/Branch/BranchSupport.php index dc8e81bb..2d9cf57e 100644 --- a/library/Director/Db/Branch/BranchSupport.php +++ b/library/Director/Db/Branch/BranchSupport.php @@ -7,21 +7,70 @@ use Icinga\Module\Director\Objects\SyncRule; class BranchSupport { + const BRANCHED_TABLE_PREFIX = 'branched_'; + + const TABLE_ICINGA_APIUSER = 'icinga_apiuser'; + const TABLE_ICINGA_COMMAND = 'icinga_command'; + const TABLE_ICINGA_DEPENDENCY = 'icinga_dependency'; + const TABLE_ICINGA_ENDPOINT = 'icinga_endpoint'; + const TABLE_ICINGA_HOST = 'icinga_host'; + const TABLE_ICINGA_HOSTGROUP = 'icinga_hostgroup'; + const TABLE_ICINGA_NOTIFICATION = 'icinga_notification'; + const TABLE_ICINGA_SCHEDULED_DOWNTIME = 'icinga_scheduled_downtime'; + const TABLE_ICINGA_SERVICE = 'icinga_service'; + const TABLE_ICINGA_SERVICEGROUP = 'icinga_servicegroup'; + const TABLE_ICINGA_TIMEPERIOD = 'icinga_timeperiod'; + const TABLE_ICINGA_USER = 'icinga_user'; + const TABLE_ICINGA_USERGROUP = 'icinga_usergroup'; + const TABLE_ICINGA_ZONE = 'icinga_zone'; + + const BRANCHED_TABLE_ICINGA_APIUSER = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_APIUSER; + const BRANCHED_TABLE_ICINGA_COMMAND = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_COMMAND; + const BRANCHED_TABLE_ICINGA_DEPENDENCY = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_DEPENDENCY; + const BRANCHED_TABLE_ICINGA_ENDPOINT = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_ENDPOINT; + const BRANCHED_TABLE_ICINGA_HOST = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_HOST; + const BRANCHED_TABLE_ICINGA_HOSTGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_HOSTGROUP; + const BRANCHED_TABLE_ICINGA_NOTIFICATION = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_NOTIFICATION; + const BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SCHEDULED_DOWNTIME; + const BRANCHED_TABLE_ICINGA_SERVICE = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICE; + const BRANCHED_TABLE_ICINGA_SERVICEGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICEGROUP; + const BRANCHED_TABLE_ICINGA_TIMEPERIOD = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_TIMEPERIOD; + const BRANCHED_TABLE_ICINGA_USER = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_USER; + const BRANCHED_TABLE_ICINGA_USERGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_USERGROUP; + const BRANCHED_TABLE_ICINGA_ZONE = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_ZONE; + + const OBJECT_TABLES = [ + self::TABLE_ICINGA_APIUSER, + self::TABLE_ICINGA_COMMAND, + self::TABLE_ICINGA_DEPENDENCY, + self::TABLE_ICINGA_ENDPOINT, + self::TABLE_ICINGA_HOST, + self::TABLE_ICINGA_HOSTGROUP, + self::TABLE_ICINGA_NOTIFICATION, + self::TABLE_ICINGA_SCHEDULED_DOWNTIME, + self::TABLE_ICINGA_SERVICE, + self::TABLE_ICINGA_SERVICEGROUP, + self::TABLE_ICINGA_TIMEPERIOD, + self::TABLE_ICINGA_USER, + self::TABLE_ICINGA_USERGROUP, + self::TABLE_ICINGA_ZONE, + ]; + const BRANCHED_TABLES = [ - 'icinga_apiuser', - 'icinga_command', - 'icinga_dependency', - 'icinga_endpoint', - 'icinga_host', - 'icinga_hostgroup', - 'icinga_notification', - 'icinga_scheduled_downtime', - 'icinga_service', - 'icinga_servicegroup', - 'icinga_timeperiod', - 'icinga_user', - 'icinga_usergroup', - 'icinga_zone', + self::BRANCHED_TABLE_ICINGA_APIUSER, + self::BRANCHED_TABLE_ICINGA_COMMAND, + self::BRANCHED_TABLE_ICINGA_DEPENDENCY, + self::BRANCHED_TABLE_ICINGA_ENDPOINT, + self::BRANCHED_TABLE_ICINGA_HOST, + self::BRANCHED_TABLE_ICINGA_HOSTGROUP, + self::BRANCHED_TABLE_ICINGA_NOTIFICATION, + self::BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME, + self::BRANCHED_TABLE_ICINGA_SERVICE, + self::BRANCHED_TABLE_ICINGA_SERVICEGROUP, + self::BRANCHED_TABLE_ICINGA_TIMEPERIOD, + self::BRANCHED_TABLE_ICINGA_USER, + self::BRANCHED_TABLE_ICINGA_USERGROUP, + self::BRANCHED_TABLE_ICINGA_ZONE, ]; public static function existsForTableName($table) From 0f2045c8f6e55bfb123f5321d7ac889e6c33b4b8 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 29 Aug 2022 09:03:01 +0200 Subject: [PATCH 05/38] SyncruleController: fix preview logic --- .../controllers/SyncruleController.php | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/application/controllers/SyncruleController.php b/application/controllers/SyncruleController.php index c9a60530..928cf2c2 100644 --- a/application/controllers/SyncruleController.php +++ b/application/controllers/SyncruleController.php @@ -9,6 +9,7 @@ use Icinga\Module\Director\Data\Db\DbObjectStore; use Icinga\Module\Director\Data\Db\DbObjectTypeRegistry; use Icinga\Module\Director\Db\Branch\Branch; use Icinga\Module\Director\Db\Branch\BranchStore; +use Icinga\Module\Director\Db\Branch\BranchSupport; use Icinga\Module\Director\Web\Controller\BranchHelper; use Icinga\Module\Director\Web\Form\ClickHereForm; use Icinga\Module\Director\Web\Table\BranchActivityTable; @@ -123,7 +124,7 @@ class SyncruleController extends ActionController if ($this->hasBranch()) { $objectType = $rule->get('object_type'); $table = DbObjectTypeRegistry::tableNameByType($objectType); - if (! $this->tableHasBranchSupport($table)) { + if (! BranchSupport::existsForTableName($table)) { $this->showNotInBranch(sprintf($this->translate("Synchronizing '%s'"), $objectType)); return; } @@ -173,26 +174,35 @@ class SyncruleController extends ActionController public function previewAction() { $rule = $this->requireSyncRule(); - // $rule->set('update_policy', 'replace'); + $branchSupport = BranchSupport::existsForSyncRule($rule); $branchStore = new BranchStore($this->db()); - $tmpBranchName = Branch::PREFIX_SYNC_PREVIEW . '/' . $rule->get('id'); $owner = $this->getAuth()->getUser()->getUsername(); - if ($this->getBranch()->isBranch()) { - // We could keep changes for preview on branch too - $branchStore->deleteByName($tmpBranchName); - $tmpBranch = $branchStore->cloneBranchForSync($this->getBranch(), $tmpBranchName, $owner); - $after = 1600000000; // a date in 2020, minus 10000000 + if ($branchSupport) { + if ($this->getBranch()->isBranch()) { + $tmpBranchName = sprintf( + '%s/%s-%s', + Branch::PREFIX_SYNC_PREVIEW, + $this->getBranch()->getUuid()->toString(), + $rule->get('id') + ); + // We could keep changes for preview on branch too + $branchStore->deleteByName($tmpBranchName); + $tmpBranch = $branchStore->cloneBranchForSync($this->getBranch(), $tmpBranchName, $owner); + $after = 1600000000; // a date in 2020, minus 10000000 + } else { + $tmpBranchName = Branch::PREFIX_SYNC_PREVIEW . '/' . $rule->get('id'); + $tmpBranch = $branchStore->fetchOrCreateByName($tmpBranchName, $owner); + $after = null; + } + $store = new DbObjectStore($this->db(), $tmpBranch); } else { - $tmpBranch = $branchStore->fetchOrCreateByName($tmpBranchName, $owner); - $after = null; + $tmpBranch = $store = null; } - $store = new DbObjectStore($this->db(), $tmpBranch); $this->tabs(new SyncRuleTabs($rule))->activate('preview'); $this->addTitle($this->translate('Sync Preview')); $sync = new Sync($rule, $store); - - $fetchExpected = true; + $keepBranchPreview = false; if ($tmpBranch) { if ($lastTime = $branchStore->getLastActivityTime($tmpBranch, $after)) { if ((time() - $lastTime) > 100) { @@ -201,8 +211,9 @@ class SyncruleController extends ActionController $here = (new ClickHereForm())->handleRequest($this->getServerRequest()); if ($here->hasBeenClicked()) { $branchStore->wipeBranch($tmpBranch, $after); + $this->redirectNow($this->url()); } else { - $fetchExpected = false; + $keepBranchPreview = true; } $this->content()->add(Hint::info(Html::sprintf( $this->translate('This preview has been generated %s, please click %s to regenerate it'), @@ -212,41 +223,40 @@ class SyncruleController extends ActionController } } } - - try { - if ($fetchExpected) { - $modifications = $sync->getExpectedModifications(); - if ($tmpBranch) { - $sync->apply(); - } - } else { - return; - } - } catch (\Exception $e) { - $this->content()->add(Hint::error($e->getMessage())); - - return; - } - - if (empty($modifications) && $tmpBranch === null) { - $this->content()->add(Hint::ok($this->translate( - 'This Sync Rule is in sync and would currently not apply any changes' - ))); - - return; + if (!$keepBranchPreview) { + $modifications = $sync->getExpectedModifications(); } if ($tmpBranch) { - if (!$fetchExpected) { - $sync->apply(); + try { + if (!$keepBranchPreview) { + $sync->apply(); + } + } catch (\Exception $e) { + $this->content()->add(Hint::error($e->getMessage())); + return; } + $changes = new BranchActivityTable($tmpBranch->getUuid(), $this->db()); $changes->disableObjectLink(); + if (count($changes) === 0) { + $this->showInSync(); + } $changes->renderTo($this); - return; + } else { + if (empty($modifications)) { + $this->showInSync(); + return; + } + $this->showExpectedModificationSummary($modifications); } + } - $this->showExpectedModificationSummary($modifications); + protected function showInSync() + { + $this->content()->add(Hint::ok($this->translate( + 'This Sync Rule is in sync and would currently not apply any changes' + ))); } protected function showExpectedModificationSummary($modifications) From d4336311749be426d4a4e6790fbf08274bb749b2 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 29 Aug 2022 21:44:21 +0200 Subject: [PATCH 06/38] TableWithBranchSupport: new trait --- library/Director/Web/Table/ObjectsTable.php | 43 +--------------- .../Web/Table/TableWithBranchSupport.php | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 library/Director/Web/Table/TableWithBranchSupport.php diff --git a/library/Director/Web/Table/ObjectsTable.php b/library/Director/Web/Table/ObjectsTable.php index 5d683b52..511e727d 100644 --- a/library/Director/Web/Table/ObjectsTable.php +++ b/library/Director/Web/Table/ObjectsTable.php @@ -14,11 +14,12 @@ use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use gipfl\IcingaWeb2\Url; use Ramsey\Uuid\Uuid; -use Ramsey\Uuid\UuidInterface; use Zend_Db_Select as ZfSelect; class ObjectsTable extends ZfQueryBasedTable { + use TableWithBranchSupport; + /** @var ObjectRestriction[] */ protected $objectRestrictions; @@ -37,9 +38,6 @@ class ObjectsTable extends ZfQueryBasedTable protected $type; - /** @var UuidInterface|null */ - protected $branchUuid; - protected $baseObjectUrl; /** @var IcingaObject */ @@ -112,13 +110,6 @@ class ObjectsTable extends ZfQueryBasedTable return $this; } - public function setBranchUuid(UuidInterface $uuid = null) - { - $this->branchUuid = $uuid; - - return $this; - } - public function getColumns() { return $this->columns; @@ -256,36 +247,6 @@ class ObjectsTable extends ZfQueryBasedTable return $this->dummyObject; } - protected function branchifyColumns($columns) - { - $result = [ - 'uuid' => 'COALESCE(o.uuid, bo.uuid)' - ]; - $ignore = ['o.id']; - foreach ($columns as $alias => $column) { - if (substr($column, 0, 2) === 'o.' && ! in_array($column, $ignore)) { - // bo.column, o.column - $column = "COALESCE(b$column, $column)"; - } - - // Used in Service Tables: - if ($column === 'h.object_name' && $alias = 'host') { - $column = "COALESCE(bo.host, $column)"; - } - - $result[$alias] = $column; - } - - return $result; - } - - protected function stripSearchColumnAliases() - { - foreach ($this->searchColumns as &$column) { - $column = preg_replace('/^[a-z]+\./', '', $column); - } - } - protected function prepareQuery() { $table = $this->getDummyObject()->getTableName(); diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php new file mode 100644 index 00000000..bcf5a151 --- /dev/null +++ b/library/Director/Web/Table/TableWithBranchSupport.php @@ -0,0 +1,49 @@ +branchUuid = $uuid; + + return $this; + } + + protected function branchifyColumns($columns) + { + $result = [ + 'uuid' => 'COALESCE(o.uuid, bo.uuid)' + ]; + $ignore = ['o.id']; + foreach ($columns as $alias => $column) { + if (substr($column, 0, 2) === 'o.' && ! in_array($column, $ignore)) { + // bo.column, o.column + $column = "COALESCE(b$column, $column)"; + } + + // Used in Service Tables: + if ($column === 'h.object_name' && $alias = 'host') { + $column = "COALESCE(bo.host, $column)"; + } + + $result[$alias] = $column; + } + + return $result; + } + + protected function stripSearchColumnAliases() + { + foreach ($this->searchColumns as &$column) { + $column = preg_replace('/^[a-z]+\./', '', $column); + } + } +} From 28c149efed9041d7f8032fee6856281b8c301ebb Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 29 Aug 2022 23:17:07 +0200 Subject: [PATCH 07/38] IcingaServiceSetServiceTable: refactor link logic --- .../Table/IcingaServiceSetServiceTable.php | 113 +++++++++--------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php index 41763c02..ff03f988 100644 --- a/library/Director/Web/Table/IcingaServiceSetServiceTable.php +++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php @@ -154,73 +154,21 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable return $this->title ?: $this->translate('Servicename'); } - /** - * @param HtmlElement $parent - */ protected function renderTitleColumns() { if (! $this->host || ! $this->affectedHost) { return Html::tag('th', $this->getTitle()); } - if (! $this->host) { - $deleteLink = ''; - } elseif ($this->readonly) { - $deleteLink = Html::tag('span', [ - 'class' => 'icon-paste', - 'style' => 'float: right; font-weight: normal', - ], $this->host->getObjectName()); + if ($this->readonly) { + $link = $this->createFakeRemoveLinkForReadonlyView(); } elseif ($this->affectedHost->get('id') !== $this->host->get('id')) { - $host = $this->host; - $deleteLink = Link::create( - $host->getObjectName(), - 'director/host/services', - ['name' => $host->getObjectName()], - [ - 'class' => 'icon-paste', - 'style' => 'float: right; font-weight: normal', - 'data-base-target' => '_next', - 'title' => sprintf( - $this->translate('This set has been inherited from %s'), - $host->getObjectName() - ) - ] - ); + $link = $this->linkToHost($this->host); } else { - $deleteLink = new RemoveLinkForm( - $this->translate('Remove'), - sprintf( - $this->translate('Remove "%s" from this host'), - $this->getTitle() - ), - Url::fromPath('director/host/services', [ - 'name' => $this->host->getObjectName() - ]), - ['title' => $this->getTitle()] - ); - $deleteLink->runOnSuccess(function () { - $conn = $this->set->getConnection(); - $db = $conn->getDbAdapter(); - $query = $db->select()->from( - ['ss' => 'icinga_service_set'], - 'ss.id' - )->join( - ['ssih' => 'icinga_service_set_inheritance'], - 'ssih.service_set_id = ss.id', - [] - )->where( - 'ssih.parent_service_set_id = ?', - $this->set->get('id') - )->where('ss.host_id = ?', $this->host->get('id')); - IcingaServiceSet::loadWithAutoIncId( - $db->fetchOne($query), - $conn - )->delete(); - }); - $deleteLink->handleRequest(); + $link = $this->createRemoveLinkForm(); } - return $this::th([$this->getTitle(), $deleteLink]); + return $this::th([$this->getTitle(), $link]); } /** @@ -268,4 +216,55 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable return $query; } + + protected function createFakeRemoveLinkForReadonlyView() + { + return Html::tag('span', [ + 'class' => 'icon-paste', + 'style' => 'float: right; font-weight: normal', + ], $this->host->getObjectName()); + } + + protected function linkToHost(IcingaHost $host) + { + $hostname = $host->getObjectName(); + return Link::create($hostname, 'director/host/services', ['name' => $hostname], [ + 'class' => 'icon-paste', + 'style' => 'float: right; font-weight: normal', + 'data-base-target' => '_next', + 'title' => sprintf( + $this->translate('This set has been inherited from %s'), + $hostname + ) + ]); + } + + protected function createRemoveLinkForm() + { + $deleteLink = new RemoveLinkForm( + $this->translate('Remove'), + sprintf( + $this->translate('Remove "%s" from this host'), + $this->getTitle() + ), + Url::fromPath('director/host/services', [ + 'name' => $this->host->getObjectName() + ]), + ['title' => $this->getTitle()] + ); + $deleteLink->runOnSuccess(function () { + $conn = $this->set->getConnection(); + $db = $conn->getDbAdapter(); + $query = $db->select()->from(['ss' => 'icinga_service_set'], 'ss.id') + ->join(['ssih' => 'icinga_service_set_inheritance'], 'ssih.service_set_id = ss.id', []) + ->where('ssih.parent_service_set_id = ?', $this->set->get('id')) + ->where('ss.host_id = ?', $this->host->get('id')); + IcingaServiceSet::loadWithAutoIncId( + $db->fetchOne($query), + $conn + )->delete(); + }); + $deleteLink->handleRequest(); + return $deleteLink; + } } From dd85c2ee3547dae7e0dc5438af15c4ff8d6bdce8 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Wed, 31 Aug 2022 16:36:31 +0200 Subject: [PATCH 08/38] Sync: compare keys in a case-insensitive way fixes #2598 --- library/Director/Import/Sync.php | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/library/Director/Import/Sync.php b/library/Director/Import/Sync.php index fc685181..c55a6ad1 100644 --- a/library/Director/Import/Sync.php +++ b/library/Director/Import/Sync.php @@ -48,6 +48,9 @@ class Sync /** @var bool Whether we already prepared your sync */ protected $isPrepared = false; + /** @var bool Whether we applied strtolower() to existing object keys */ + protected $usedLowerCasedKeys = false; + protected $modify = []; protected $remove = []; @@ -313,6 +316,9 @@ class Sync foreach ($rows as $row) { if ($combinedKey) { $key = SyncUtils::fillVariables($sourceKeyPattern, $row); + if ($this->usedLowerCasedKeys) { + $key = strtolower($key); + } if (array_key_exists($key, $this->imported[$sourceId])) { throw new InvalidArgumentException(sprintf( @@ -341,7 +347,11 @@ class Sync if ($combinedKey) { $this->imported[$sourceId][$key] = $row; } else { - $this->imported[$sourceId][$row->$key] = $row; + if ($this->usedLowerCasedKeys) { + $this->imported[$sourceId][strtolower($row->$key)] = $row; + } else { + $this->imported[$sourceId][$row->$key] = $row; + } } } @@ -391,6 +401,7 @@ class Sync Benchmark::measure('Begin loading existing objects'); $ruleObjectType = $this->rule->get('object_type'); + $useLowerCaseKeys = $ruleObjectType !== 'datalistEntry'; // TODO: Make object_type (template, object...) and object_name mandatory? if ($this->rule->hasCombinedKey()) { $this->objects = []; @@ -418,6 +429,9 @@ class Sync $destinationKeyPattern, $object ); + if ($useLowerCaseKeys) { + $key = strtolower($key); + } if (array_key_exists($key, $this->objects)) { throw new InvalidArgumentException(sprintf( @@ -431,12 +445,22 @@ class Sync } } else { if ($this->store) { - $this->objects = $this->store->loadAll(DbObjectTypeRegistry::tableNameByType($ruleObjectType), 'object_name'); + $objects = $this->store->loadAll(DbObjectTypeRegistry::tableNameByType($ruleObjectType), 'object_name'); } else { - $this->objects = IcingaObject::loadAllByType($ruleObjectType, $this->db); + $objects = IcingaObject::loadAllByType($ruleObjectType, $this->db); + } + + if ($useLowerCaseKeys) { + $this->objects = []; + foreach ($objects as $key => $object) { + $this->objects[strtolower($key)] = $object; + } + } else { + $this->objects = $objects; } } + $this->usedLowerCasedKeys = $useLowerCaseKeys; // TODO: should be obsoleted by a better "loadFiltered" method if ($ruleObjectType === 'datalistEntry') { $this->removeForeignListEntries(); @@ -463,6 +487,9 @@ class Sync foreach ($this->imported[$sourceId] as $key => $row) { // Workaround: $a["10"] = "val"; -> array_keys($a) = [(int) 10] $key = (string) $key; + if ($this->usedLowerCasedKeys) { + $key = strtolower($key); + } if (! array_key_exists($key, $objects)) { // Safe default values for object_type and object_name if ($ruleObjectType === 'datalistEntry') { From 3c7c7bc61a10b0dd043a1d346eacfd448e880748 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 5 Sep 2022 12:23:13 +0200 Subject: [PATCH 09/38] ServicesetController: stringify uuid once --- application/controllers/ServicesetController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/controllers/ServicesetController.php b/application/controllers/ServicesetController.php index f4516053..c5507c0c 100644 --- a/application/controllers/ServicesetController.php +++ b/application/controllers/ServicesetController.php @@ -101,14 +101,15 @@ class ServicesetController extends ObjectController if ($this->branch->isBranch()) { return $this; } + $hexUuid = $this->object->getUniqueId()->toString(); $tabs = $this->tabs(); $tabs->add('services', [ 'url' => 'director/serviceset/services', - 'urlParams' => ['uuid' => $this->object->getUniqueId()], + 'urlParams' => ['uuid' => $hexUuid], 'label' => 'Services' ])->add('hosts', [ 'url' => 'director/serviceset/hosts', - 'urlParams' => ['uuid' => $this->object->getUniqueId()], + 'urlParams' => ['uuid' => $hexUuid], 'label' => 'Hosts' ]); From 1df495b41ed503bfe1f550749936ff067582ba3c Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Fri, 9 Sep 2022 14:30:51 +0200 Subject: [PATCH 10/38] UuidLookup: fix lookup for cloned branches --- library/Director/Db/Branch/UuidLookup.php | 1 + 1 file changed, 1 insertion(+) diff --git a/library/Director/Db/Branch/UuidLookup.php b/library/Director/Db/Branch/UuidLookup.php index c56996a7..31284a8b 100644 --- a/library/Director/Db/Branch/UuidLookup.php +++ b/library/Director/Db/Branch/UuidLookup.php @@ -94,6 +94,7 @@ class UuidLookup $uuid = self::fetchOptionalUuid($connection, $query); if ($uuid === null && $branch->isBranch()) { $query = self::addKeyToQuery($connection, $db->select()->from("branched_$table", 'uuid'), $key); + $query->where('branch_uuid = ?', $connection->quoteBinary($branch->getUuid()->getBytes())); $uuid = self::fetchOptionalUuid($connection, $query); } From 0cf8c76617ee1113a5152bef3406989dc8c00c7a Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 13 Sep 2022 10:20:24 +0200 Subject: [PATCH 11/38] IcingaObject: more details in the error message --- library/Director/Objects/IcingaObject.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php index a43e1451..f03e7932 100644 --- a/library/Director/Objects/IcingaObject.php +++ b/library/Director/Objects/IcingaObject.php @@ -565,7 +565,9 @@ abstract class IcingaObject extends DbObject implements IcingaConfigRenderer } catch (NotFoundError $e) { // Hint: eventually a NotFoundError would be better throw new RuntimeException(sprintf( - 'Unable to load object referenced from %s "%s", %s', + 'Unable to load object (%s: %s) referenced from %s "%s", %s', + $short, + $this->unresolvedRelatedProperties[$name], $this->getShortTableName(), $this->getObjectName(), lcfirst($e->getMessage()) From 9434cf50894c681632156467a2db0886e86cf2fc Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 15 Sep 2022 11:44:24 +0200 Subject: [PATCH 12/38] IcingaServiceSet: type hint for IDE --- library/Director/Objects/IcingaServiceSet.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index 6ebd570d..b7e6f356 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -94,7 +94,9 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface if (empty($imports)) { return array(); } - return $this->getServiceObjectsForSet(array_shift($imports)); + $parent = array_shift($imports); + assert($parent instanceof IcingaServiceSet); + return $this->getServiceObjectsForSet($parent); } else { return $this->getServiceObjectsForSet($this); } From 8bfbe2a80fb6bfd052c7b8cc91d936158f36c84f Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 15 Sep 2022 11:44:43 +0200 Subject: [PATCH 13/38] IcingaServiceSet: friendlier error message --- library/Director/Objects/IcingaServiceSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index b7e6f356..8e73ad93 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -587,7 +587,7 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface $config->configFile( 'failed-to-render' )->prepend( - "/** Failed to render this object **/\n" + "/** Failed to render this Service Set **/\n" . '/* ' . $e->getMessage() . ' */' ); } From 410913e51233194c0cd2b7fe7070cdd416b71273 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 15 Sep 2022 11:45:32 +0200 Subject: [PATCH 14/38] UuidLookup: fix host/set related fallback --- library/Director/Db/Branch/UuidLookup.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/Director/Db/Branch/UuidLookup.php b/library/Director/Db/Branch/UuidLookup.php index 31284a8b..f9f17478 100644 --- a/library/Director/Db/Branch/UuidLookup.php +++ b/library/Director/Db/Branch/UuidLookup.php @@ -47,15 +47,21 @@ class UuidLookup if ($uuid === null && $branch->isBranch()) { // TODO: use different tables? - $query = $db->select()->from('branched_icinga_service', 'uuid')->where('object_type = ?', $objectType); + $query = $db->select() + ->from('branched_icinga_service', 'uuid') + ->where('branch_uuid = ?', $connection->quoteBinary($branch->getUuid()->getBytes())); + if ($objectType) { + $query->where('object_type = ?', $objectType); + } $query = self::addKeyToQuery($connection, $query, $key); if ($host) { // TODO: uuid? - $query->add('host = ?', $host->getObjectName()); + $query->where('host = ?', $host->getObjectName()); } if ($set) { - $query->add('service_set = ?', $set->getObjectName()); + $query->where('service_set = ?', $set->getObjectName()); } + $uuid = self::fetchOptionalUuid($connection, $query); } From e3e92cdb3a2df33f7b08e1d9429724255e6887f0 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 19 Sep 2022 07:41:44 +0200 Subject: [PATCH 15/38] AppliedServiceSetLoader: change method visibility ...for fetchAppliedServiceSets() --- library/Director/Db/AppliedServiceSetLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Director/Db/AppliedServiceSetLoader.php b/library/Director/Db/AppliedServiceSetLoader.php index fb7eb5ea..b1e94087 100644 --- a/library/Director/Db/AppliedServiceSetLoader.php +++ b/library/Director/Db/AppliedServiceSetLoader.php @@ -28,7 +28,7 @@ class AppliedServiceSetLoader /** * @return IcingaServiceSet[] */ - public function fetchAppliedServiceSets() + protected function fetchAppliedServiceSets() { $sets = array(); $matcher = HostApplyMatches::prepare($this->host); From 166b8621143b60013c6ceb9f44446a75338d3c31 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 19 Sep 2022 10:09:52 +0200 Subject: [PATCH 16/38] PropertyModifierFromLatin1: use iconv --- .../Director/PropertyModifier/PropertyModifierFromLatin1.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/Director/PropertyModifier/PropertyModifierFromLatin1.php b/library/Director/PropertyModifier/PropertyModifierFromLatin1.php index ac12f82f..272956c7 100644 --- a/library/Director/PropertyModifier/PropertyModifierFromLatin1.php +++ b/library/Director/PropertyModifier/PropertyModifierFromLatin1.php @@ -3,7 +3,7 @@ namespace Icinga\Module\Director\PropertyModifier; use Icinga\Module\Director\Hook\PropertyModifierHook; -use Icinga\Module\Director\Web\Form\QuickForm; +use function iconv; class PropertyModifierFromLatin1 extends PropertyModifierHook { @@ -18,6 +18,6 @@ class PropertyModifierFromLatin1 extends PropertyModifierHook return null; } - return utf8_encode($value); + return iconv('ISO-8859-15', 'UTF-8', $value); } } From 8b1513830cb515f7b0bb132215e3b72cc4d919fd Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 07:20:29 +0200 Subject: [PATCH 17/38] IcingaServiceSet: do not delete Services w/o id --- library/Director/Objects/IcingaServiceSet.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index 8e73ad93..60648a55 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -278,7 +278,9 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface if ($hostId) { $deleteIds = []; foreach ($this->getServiceObjects() as $service) { - $deleteIds[] = (int) $service->get('id'); + if ($idToDelete = $service->get('id')) { + $deleteIds[] = (int) $idToDelete; + } } if (! empty($deleteIds)) { From ae0992f1965fbdda27263ae439203274330fdae1 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 11:54:10 +0200 Subject: [PATCH 18/38] UuidLookup: host_id VS host in branch --- library/Director/Db/Branch/UuidLookup.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/Director/Db/Branch/UuidLookup.php b/library/Director/Db/Branch/UuidLookup.php index f9f17478..b340e07e 100644 --- a/library/Director/Db/Branch/UuidLookup.php +++ b/library/Director/Db/Branch/UuidLookup.php @@ -99,6 +99,10 @@ class UuidLookup $query = self::addKeyToQuery($connection, $db->select()->from($table, 'uuid'), $key); $uuid = self::fetchOptionalUuid($connection, $query); if ($uuid === null && $branch->isBranch()) { + if (is_array($key) && isset($key['host_id'])) { + $key['host'] = IcingaHost::load($key['host_id'], $connection)->getObjectName(); + unset($key['host_id']); + } $query = self::addKeyToQuery($connection, $db->select()->from("branched_$table", 'uuid'), $key); $query->where('branch_uuid = ?', $connection->quoteBinary($branch->getUuid()->getBytes())); $uuid = self::fetchOptionalUuid($connection, $query); From b35b6b84cc36cb80ec0083d3e3ed5564fb2c3ff4 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:03:00 +0200 Subject: [PATCH 19/38] TableWithBranchSupport: do not branchify relations --- library/Director/Web/Table/TableWithBranchSupport.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php index bcf5a151..903bd85f 100644 --- a/library/Director/Web/Table/TableWithBranchSupport.php +++ b/library/Director/Web/Table/TableWithBranchSupport.php @@ -22,12 +22,16 @@ trait TableWithBranchSupport $result = [ 'uuid' => 'COALESCE(o.uuid, bo.uuid)' ]; - $ignore = ['o.id']; + $ignore = ['o.id', 'os.id', 'o.service_set_id', 'os.host_id']; foreach ($columns as $alias => $column) { if (substr($column, 0, 2) === 'o.' && ! in_array($column, $ignore)) { // bo.column, o.column $column = "COALESCE(b$column, $column)"; } + if (substr($column, 0, 3) === 'os.' && ! in_array($column, $ignore)) { + // bo.column, o.column + $column = "COALESCE(b$column, $column)"; + } // Used in Service Tables: if ($column === 'h.object_name' && $alias = 'host') { From 1fbb4d93b63c146d368c27db28772cb5c8231176 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:03:46 +0200 Subject: [PATCH 20/38] BranchedObjectHint: allow no object (create) --- library/Director/Web/Widget/BranchedObjectHint.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/Director/Web/Widget/BranchedObjectHint.php b/library/Director/Web/Widget/BranchedObjectHint.php index ccdbccf7..ec160940 100644 --- a/library/Director/Web/Widget/BranchedObjectHint.php +++ b/library/Director/Web/Widget/BranchedObjectHint.php @@ -15,7 +15,7 @@ class BranchedObjectHint extends HtmlDocument { use TranslationHelper; - public function __construct(Branch $branch, Auth $auth, BranchedObject $object) + public function __construct(Branch $branch, Auth $auth, BranchedObject $object = null) { if (! $branch->isBranch()) { return; @@ -29,6 +29,13 @@ class BranchedObjectHint extends HtmlDocument $label = $name; } $link = $hook->linkToBranch($branch, $auth, $label); + if ($object === null) { + $this->add(Hint::info(Html::sprintf($this->translate( + 'This object will be created in %s. It will not be part of any deployment' + . ' unless being merged' + ), $link))); + return; + } if (! $object->hasBeenTouchedByBranch()) { $this->add(Hint::info(Html::sprintf($this->translate( From 758b99126abf37d6849bc5dcd298b48ffce1bf67 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:04:46 +0200 Subject: [PATCH 21/38] IcingaServiceForm: no deactivate in branch for now --- application/forms/IcingaServiceForm.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/application/forms/IcingaServiceForm.php b/application/forms/IcingaServiceForm.php index c6b480ec..be0c5f0b 100644 --- a/application/forms/IcingaServiceForm.php +++ b/application/forms/IcingaServiceForm.php @@ -128,6 +128,8 @@ class IcingaServiceForm extends DirectorObjectForm if (! $this->providesOverrides()) { return; } + $hasDeleteButton = false; + $isBranch = $this->branch && $this->branch->isBranch(); if ($this->hasBeenBlacklisted()) { $this->addHtml( @@ -135,7 +137,10 @@ class IcingaServiceForm extends DirectorObjectForm ['name' => 'HINT_blacklisted'] ); $group = null; - $this->addDeleteButton($this->translate('Reactivate')); + if (! $isBranch) { + $this->addDeleteButton($this->translate('Reactivate')); + $hasDeleteButton = true; + } $this->setSubmitLabel(false); } else { $this->addOverrideHint(); @@ -164,10 +169,13 @@ class IcingaServiceForm extends DirectorObjectForm $this->setSubmitLabel(false); } - $this->addDeleteButton($this->translate('Deactivate')); + if (! $isBranch) { + $this->addDeleteButton($this->translate('Deactivate')); + $hasDeleteButton = true; + } } - if (! $this->hasSubmitButton()) { + if (! $this->hasSubmitButton() && $hasDeleteButton) { $this->addDisplayGroup([$this->deleteButtonName], 'buttons', [ 'decorators' => [ 'FormElements', From 2a5909917bcd129c74d02f06ae7724504de1ee37 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:05:43 +0200 Subject: [PATCH 22/38] BranchSupport, Inspection: allow Sets --- library/Director/Db/Branch/BranchModificationInspection.php | 1 + library/Director/Db/Branch/BranchSupport.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/library/Director/Db/Branch/BranchModificationInspection.php b/library/Director/Db/Branch/BranchModificationInspection.php index 0181b7db..978ca5da 100644 --- a/library/Director/Db/Branch/BranchModificationInspection.php +++ b/library/Director/Db/Branch/BranchModificationInspection.php @@ -39,6 +39,7 @@ class BranchModificationInspection $this->translate('Hostgroups') => BranchSupport::BRANCHED_TABLE_ICINGA_HOSTGROUP, $this->translate('Services') => BranchSupport::BRANCHED_TABLE_ICINGA_NOTIFICATION, $this->translate('Servicegroups') => BranchSupport::BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME, + $this->translate('Servicesets') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE_SET, $this->translate('Users') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICE, $this->translate('Usergroups') => BranchSupport::BRANCHED_TABLE_ICINGA_SERVICEGROUP, $this->translate('Timeperiods') => BranchSupport::BRANCHED_TABLE_ICINGA_TIMEPERIOD, diff --git a/library/Director/Db/Branch/BranchSupport.php b/library/Director/Db/Branch/BranchSupport.php index 2d9cf57e..74be0212 100644 --- a/library/Director/Db/Branch/BranchSupport.php +++ b/library/Director/Db/Branch/BranchSupport.php @@ -19,6 +19,7 @@ class BranchSupport const TABLE_ICINGA_SCHEDULED_DOWNTIME = 'icinga_scheduled_downtime'; const TABLE_ICINGA_SERVICE = 'icinga_service'; const TABLE_ICINGA_SERVICEGROUP = 'icinga_servicegroup'; + const TABLE_ICINGA_SERVICE_SET = 'icinga_service_set'; const TABLE_ICINGA_TIMEPERIOD = 'icinga_timeperiod'; const TABLE_ICINGA_USER = 'icinga_user'; const TABLE_ICINGA_USERGROUP = 'icinga_usergroup'; @@ -34,6 +35,7 @@ class BranchSupport const BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SCHEDULED_DOWNTIME; const BRANCHED_TABLE_ICINGA_SERVICE = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICE; const BRANCHED_TABLE_ICINGA_SERVICEGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICEGROUP; + const BRANCHED_TABLE_ICINGA_SERVICE_SET = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_SERVICE_SET; const BRANCHED_TABLE_ICINGA_TIMEPERIOD = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_TIMEPERIOD; const BRANCHED_TABLE_ICINGA_USER = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_USER; const BRANCHED_TABLE_ICINGA_USERGROUP = self::BRANCHED_TABLE_PREFIX. self::TABLE_ICINGA_USERGROUP; @@ -50,6 +52,7 @@ class BranchSupport self::TABLE_ICINGA_SCHEDULED_DOWNTIME, self::TABLE_ICINGA_SERVICE, self::TABLE_ICINGA_SERVICEGROUP, + self::TABLE_ICINGA_SERVICE_SET, self::TABLE_ICINGA_TIMEPERIOD, self::TABLE_ICINGA_USER, self::TABLE_ICINGA_USERGROUP, @@ -67,6 +70,7 @@ class BranchSupport self::BRANCHED_TABLE_ICINGA_SCHEDULED_DOWNTIME, self::BRANCHED_TABLE_ICINGA_SERVICE, self::BRANCHED_TABLE_ICINGA_SERVICEGROUP, + self::BRANCHED_TABLE_ICINGA_SERVICE_SET, self::BRANCHED_TABLE_ICINGA_TIMEPERIOD, self::BRANCHED_TABLE_ICINGA_USER, self::BRANCHED_TABLE_ICINGA_USERGROUP, @@ -75,7 +79,7 @@ class BranchSupport public static function existsForTableName($table) { - return in_array($table, self::BRANCHED_TABLES, true); + return in_array($table, self::OBJECT_TABLES, true); } public static function existsForSyncRule(SyncRule $rule) From 39f53b6cee9331a01d50a11b80ab47d253933218 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:06:52 +0200 Subject: [PATCH 23/38] ObjectController: branch hint on create --- library/Director/Web/Controller/ObjectController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php index a491024b..53fd8bb1 100644 --- a/library/Director/Web/Controller/ObjectController.php +++ b/library/Director/Web/Controller/ObjectController.php @@ -157,6 +157,10 @@ abstract class ObjectController extends ActionController } else { $this->addObject(); } + $branch = $this->getBranch(); + if ($branch->isBranch() && ! $this->getRequest()->isApiRequest()) { + $this->content()->add(new BranchedObjectHint($branch, $this->Auth())); + } $form->handleRequest(); $this->content()->add($form); From f301be425c098f51dcb1002332902710c0ead51d Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:07:24 +0200 Subject: [PATCH 24/38] HostController: pass branch to forms --- application/controllers/HostController.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php index 8c676986..e55310d1 100644 --- a/application/controllers/HostController.php +++ b/application/controllers/HostController.php @@ -125,6 +125,7 @@ class HostController extends ObjectController $this->content()->add( IcingaServiceSetForm::load() + ->setBranch($this->getBranch()) ->setHost($host) ->setDb($this->db()) ->handleRequest() @@ -209,11 +210,11 @@ class HostController extends ObjectController $branch = $this->getBranch(); $hostHasBeenCreatedInBranch = $branch->isBranch() && $host->get('id'); $content = $this->content(); - $table = (new ObjectsTableService($this->db()))->setAuth($this->Auth())->setHost($host) + $table = (new ObjectsTableService($this->db())) + ->setAuth($this->Auth()) + ->setHost($host) + ->setBranch($branch) ->setTitle($this->translate('Individual Service objects')); - if ($branch->isBranch()) { - $table->setBranchUuid($branch->getUuid()); - } if (count($table)) { $content->add($table); @@ -225,6 +226,7 @@ class HostController extends ObjectController foreach ($parents as $parent) { $table = (new ObjectsTableService($this->db())) ->setAuth($this->Auth()) + ->setBranch($branch) ->setHost($parent) ->setInheritedBy($host); if (count($table)) { @@ -251,6 +253,7 @@ class HostController extends ObjectController $content->add( IcingaServiceSetServiceTable::load($set) // ->setHost($host) + ->setBranch($branch) ->setAffectedHost($host) ->setTitle($title) ); @@ -279,6 +282,7 @@ class HostController extends ObjectController $host = $this->getHostObject(); $service = $this->params->getRequired('service'); $db = $this->db(); + $branch = $this->getBranch(); $this->controls()->setTabs(new Tabs()); $this->addSingleTab($this->translate('Configuration (read-only)')); $this->addTitle($this->translate('Services on %s'), $host->getObjectName()); @@ -287,6 +291,7 @@ class HostController extends ObjectController $table = (new ObjectsTableService($db)) ->setAuth($this->Auth()) ->setHost($host) + ->setBranch($branch) ->setReadonly() ->highlightService($service) ->setTitle($this->translate('Individual Service objects')); @@ -301,6 +306,7 @@ class HostController extends ObjectController foreach ($parents as $parent) { $table = (new ObjectsTableService($db)) ->setReadonly() + ->setBranch($branch) ->setHost($parent) ->highlightService($service) ->setInheritedBy($host); @@ -326,6 +332,7 @@ class HostController extends ObjectController $content->add( IcingaServiceSetServiceTable::load($set) // ->setHost($host) + ->setBranch($branch) ->setAffectedHost($host) ->setReadonly() ->highlightService($service) @@ -377,6 +384,7 @@ class HostController extends ObjectController $title = sprintf($this->translate('%s (Service set)'), $name); $table = IcingaServiceSetServiceTable::load($set) ->setHost($host) + ->setBranch($this->getBranch()) ->setAffectedHost($affectedHost) ->setTitle($title); if ($roService) { @@ -413,6 +421,7 @@ class HostController extends ObjectController $this->content()->add( IcingaServiceForm::load() ->setDb($db) + ->setBranch($this->getBranch()) ->setHost($host) ->setApplyGenerated($parent) ->setObject($service) @@ -453,6 +462,7 @@ class HostController extends ObjectController $form = IcingaServiceForm::load() ->setDb($db) + ->setBranch($this->getBranch()) ->setHost($host) ->setInheritedFrom($from->getObjectName()) ->setObject($service) @@ -530,6 +540,7 @@ class HostController extends ObjectController $form = IcingaServiceForm::load() ->setDb($db) + ->setBranch($this->getBranch()) ->setHost($host) ->setServiceSet($set) ->setObject($service) From 4e1cc13320323e9902475cd133def3dde057af10 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:16:20 +0200 Subject: [PATCH 25/38] ServiceController: enableStaticObjectLoader earlier --- application/controllers/ServiceController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php index 2438852e..3cd54d6a 100644 --- a/application/controllers/ServiceController.php +++ b/application/controllers/ServiceController.php @@ -41,6 +41,9 @@ class ServiceController extends ObjectController public function init() { + // This happens in parent::init() too, but is required to take place before the next two lines + $this->enableStaticObjectLoader($this->getTableName()); + // Hint: having Host and Set loaded first is important for UUID lookups with legacy URLs $this->host = $this->getOptionalRelatedObjectFromParams('host', 'host'); $this->set = $this->getOptionalRelatedObjectFromParams('service_set', 'set'); From 4d8e3f6db74223057abfc8193c82aa97d7fa598e Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:18:29 +0200 Subject: [PATCH 26/38] ServiceSetQueryBuilder: new query builder --- .../Data/Db/ServiceSetQueryBuilder.php | 158 ++++++++++++++++++ .../Table/IcingaServiceSetServiceTable.php | 46 +---- 2 files changed, 166 insertions(+), 38 deletions(-) create mode 100644 library/Director/Data/Db/ServiceSetQueryBuilder.php diff --git a/library/Director/Data/Db/ServiceSetQueryBuilder.php b/library/Director/Data/Db/ServiceSetQueryBuilder.php new file mode 100644 index 00000000..7841d1ee --- /dev/null +++ b/library/Director/Data/Db/ServiceSetQueryBuilder.php @@ -0,0 +1,158 @@ +connection = $connection; + $this->db = $connection->getDbAdapter(); + if ($uuid) { + $this->setBranchUuid($uuid); + } + } + + /** + * @return \Zend_Db_Select + * @throws \Zend_Db_Select_Exception + */ + public function selectServicesForSet(IcingaServiceSet $set) + { + $db = $this->connection->getDbAdapter(); + if ($this->branchUuid) { + $right = $this->selectRightBranchedServices($set)->columns($this->getRightBranchedColumns()); + $left = $this->selectLeftBranchedServices($set)->columns($this->getLeftBranchedColumns()); + $query = $this->db->select()->from(['u' => $db->select()->union([ + 'l' => new DbSelectParenthesis($left), + 'r' => new DbSelectParenthesis($right), + ])]); + $query->order('service_set'); + } else { + $query = $this->selectServices($set)->columns($this->getColumns()); + } + + return $query; + } + + protected function selectServices(IcingaServiceSet $set) + { + return $this->db + ->select() + ->from(['o' =>self::TABLE], []) + ->joinLeft(['os' => self::SET_TABLE], 'os.id = o.service_set_id', []) + ->where('os.uuid = ?', $this->connection->quoteBinary($set->getUniqueId()->getBytes())); + } + + protected function selectLeftBranchedServices(IcingaServiceSet $set) + { + return $this + ->selectServices($set) + ->joinLeft( + ['bo' => self::BRANCHED_TABLE], + $this->db->quoteInto('bo.uuid = o.uuid AND bo.branch_uuid = ?', $this->getQuotedBranchUuid()), + [] + ); + } + + protected function selectRightBranchedServices(IcingaServiceSet $set) + { + return $this->db + ->select() + ->from(['o' => self::TABLE], []) + ->joinRight(['bo' => self::BRANCHED_TABLE], 'bo.uuid = o.uuid', []) + ->where('bo.service_set = ?', $set->get('object_name')) + ->where('bo.branch_uuid = ?', $this->getQuotedBranchUuid()); + } + + protected static function resetQueryProperties(\Zend_Db_Select $query) + { + // TODO: Keep existing UUID, becomes important when using this for other tables too (w/o UNION) + // $columns = $query->getPart($query::COLUMNS); + $query->reset($query::COLUMNS); + $query->columns('uuid'); + return $query; + } + + public function fetchServicesWithQuery(\Zend_Db_Select $query) + { + static::resetQueryProperties($query); + $db = $this->connection->getDbAdapter(); + $uuids = $db->fetchCol($query); + + $services = []; + foreach ($uuids as $uuid) { + $service = IcingaService::loadWithUniqueId(Uuid::fromBytes(DbUtil::binaryResult($uuid)), $this->connection); + $service->set('service_set', null); // TODO: CHECK THIS!!!! + + $services[$service->getObjectName()] = $service; + } + + return $services; + } + + protected function getColumns() + { + return [ + 'uuid' => 'o.uuid', // MUST be first because of UNION column order, see branchifyColumns() + 'id' => 'o.id', + 'branch_uuid' => '(null)', + 'service_set' => 'os.object_name', + 'service' => 'o.object_name', + 'disabled' => 'o.disabled', + 'object_type' => 'o.object_type', + 'blacklisted' => "('n')", + ]; + } + + protected function getLeftBranchedColumns() + { + $columns = $this->getColumns(); + $columns['branch_uuid'] = 'bo.branch_uuid'; + $columns['service_set'] = 'COALESCE(os.object_name, bo.service_set)'; + + return $this->branchifyColumns($columns); + } + + protected function getRightBranchedColumns() + { + $columns = $this->getColumns(); + $columns = $this->branchifyColumns($columns); + $columns['branch_uuid'] = 'bo.branch_uuid'; + $columns['service_set'] = 'bo.service_set'; + $columns['id'] = '(NULL)'; + + return $columns; + } + + protected function getQuotedBranchUuid() + { + return $this->connection->quoteBinary($this->branchUuid->getBytes()); + } +} diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php index ff03f988..3c103749 100644 --- a/library/Director/Web/Table/IcingaServiceSetServiceTable.php +++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Web\Table; +use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder; +use Icinga\Module\Director\Db; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use Icinga\Module\Director\Forms\RemoveLinkForm; @@ -14,6 +16,8 @@ use gipfl\IcingaWeb2\Url; class IcingaServiceSetServiceTable extends ZfQueryBasedTable { + use TableWithBranchSupport; + /** @var IcingaServiceSet */ protected $set; @@ -177,44 +181,10 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable */ public function prepareQuery() { - $db = $this->db(); - $query = $db->select()->from( - ['s' => 'icinga_service'], - [ - 'id' => 's.id', - 'uuid' => 's.uuid', - 'service_set_id' => 's.service_set_id', - 'host_id' => 'ss.host_id', - 'service_set' => 'ss.object_name', - 'service' => 's.object_name', - 'disabled' => 's.disabled', - 'object_type' => 's.object_type', - ] - )->joinLeft( - ['ss' => 'icinga_service_set'], - 'ss.id = s.service_set_id', - [] - )->where( - 's.service_set_id = ?', - $this->set->get('id') - )->order('s.object_name'); - - if ($this->affectedHost) { - $query->joinLeft( - ['hsb' => 'icinga_host_service_blacklist'], - $db->quoteInto( - 's.id = hsb.service_id AND hsb.host_id = ?', - $this->affectedHost->get('id') - ), - [] - )->columns([ - 'blacklisted' => "CASE WHEN hsb.service_id IS NULL THEN 'n' ELSE 'y' END", - ]); - } else { - $query->columns(['blacklisted' => "('n')"]); - } - - return $query; + $connection = $this->connection(); + assert($connection instanceof Db); + $builder = new ServiceSetQueryBuilder($connection, $this->branchUuid); + return $builder->selectServicesForSet($this->set)->limit(100); } protected function createFakeRemoveLinkForReadonlyView() From 4d2f285c01aaed21624dfda7a070efd7984bedb8 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:19:31 +0200 Subject: [PATCH 27/38] IcingaServiceSetServiceTable: branch classes --- .../Web/Table/IcingaServiceSetServiceTable.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/library/Director/Web/Table/IcingaServiceSetServiceTable.php b/library/Director/Web/Table/IcingaServiceSetServiceTable.php index 3c103749..47207743 100644 --- a/library/Director/Web/Table/IcingaServiceSetServiceTable.php +++ b/library/Director/Web/Table/IcingaServiceSetServiceTable.php @@ -9,7 +9,6 @@ use ipl\Html\Html; use Icinga\Module\Director\Forms\RemoveLinkForm; use Icinga\Module\Director\Objects\IcingaHost; use Icinga\Module\Director\Objects\IcingaServiceSet; -use ipl\Html\HtmlElement; use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use gipfl\IcingaWeb2\Url; @@ -142,17 +141,28 @@ class IcingaServiceSetServiceTable extends ZfQueryBasedTable $tr = $this::row([ $this->getServiceLink($row) ]); - + $classes = $this->getRowClasses($row); if ($row->disabled === 'y') { - $tr->getAttributes()->add('class', 'disabled'); + $classes[] = 'disabled'; } if ($row->blacklisted === 'y') { - $tr->getAttributes()->add('class', 'strike-links'); + $classes[] = 'strike-links'; + } + if (! empty($classes)) { + $tr->getAttributes()->add('class', $classes); } return $tr; } + protected function getRowClasses($row) + { + if ($row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; + } + protected function getTitle() { return $this->title ?: $this->translate('Servicename'); From 3c8bb6bd16d3de66c578fb9321b04827184ebfc8 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:19:53 +0200 Subject: [PATCH 28/38] IcingaServiceForm: protect against branched objects ...w/o id. Can be improved --- application/forms/IcingaServiceForm.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/application/forms/IcingaServiceForm.php b/application/forms/IcingaServiceForm.php index be0c5f0b..d3b25541 100644 --- a/application/forms/IcingaServiceForm.php +++ b/application/forms/IcingaServiceForm.php @@ -224,13 +224,19 @@ class IcingaServiceForm extends DirectorObjectForm if ($this->blacklisted === null) { $host = $this->host; + // Safety check, branches + $hostId = $host->get('id'); $service = $this->getServiceToBeBlacklisted(); + $serviceId = $service->get('id'); + if (! $hostId || ! $serviceId) { + return false; + } $db = $this->db->getDbAdapter(); if ($this->providesOverrides()) { $this->blacklisted = 1 === (int)$db->fetchOne( $db->select()->from('icinga_host_service_blacklist', 'COUNT(*)') - ->where('host_id = ?', $host->get('id')) - ->where('service_id = ?', $service->get('id')) + ->where('host_id = ?', $hostId) + ->where('service_id = ?', $serviceId) ); } else { $this->blacklisted = false; From 131d4e27ba94ad1cbd9593dbf93070ade0001f1c Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:20:44 +0200 Subject: [PATCH 29/38] TableWithBranchSupport: provide setter --- .../Web/Table/TableWithBranchSupport.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/Director/Web/Table/TableWithBranchSupport.php b/library/Director/Web/Table/TableWithBranchSupport.php index 903bd85f..7c5b15cc 100644 --- a/library/Director/Web/Table/TableWithBranchSupport.php +++ b/library/Director/Web/Table/TableWithBranchSupport.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Web\Table; +use Icinga\Module\Director\Db\Branch\Branch; use Ramsey\Uuid\UuidInterface; trait TableWithBranchSupport @@ -10,6 +11,21 @@ trait TableWithBranchSupport /** @var UuidInterface|null */ protected $branchUuid; + /** + * Convenience method, only UUID is required + * + * @param Branch|null $branch + * @return $this + */ + public function setBranch(Branch $branch = null) + { + if ($branch && $branch->isBranch()) { + $this->setBranchUuid($branch->getUuid()); + } + + return $this; + } + public function setBranchUuid(UuidInterface $uuid = null) { $this->branchUuid = $uuid; From 6f173b83926358ee0aa63a9421e7f53d34538a11 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:22:05 +0200 Subject: [PATCH 30/38] ObjectsController: pass branch to table --- library/Director/Web/Controller/ObjectsController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/Director/Web/Controller/ObjectsController.php b/library/Director/Web/Controller/ObjectsController.php index 67927ebd..8c10b442 100644 --- a/library/Director/Web/Controller/ObjectsController.php +++ b/library/Director/Web/Controller/ObjectsController.php @@ -367,7 +367,9 @@ abstract class ObjectsController extends ActionController ) ); - ObjectSetTable::create($type, $this->db(), $this->getAuth())->renderTo($this); + ObjectSetTable::create($type, $this->db(), $this->getAuth()) + ->setBranch($this->getBranch()) + ->renderTo($this); } /** From de4b890e8be56a3e32b23838950af41503057d07 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:30:06 +0200 Subject: [PATCH 31/38] IcingaServiceSetForm: use host, not host_id --- application/forms/IcingaServiceSetForm.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/forms/IcingaServiceSetForm.php b/application/forms/IcingaServiceSetForm.php index d4841a1b..21508d51 100644 --- a/application/forms/IcingaServiceSetForm.php +++ b/application/forms/IcingaServiceSetForm.php @@ -69,7 +69,8 @@ class IcingaServiceSetForm extends DirectorObjectForm } $this->addHidden('object_type', 'object'); - $this->addHidden('host_id', $this->host->id); + $this->addHidden('host', $this->host->getObjectName()); + $this->groupMainProperties(); } public function setHost(IcingaHost $host) @@ -77,6 +78,7 @@ class IcingaServiceSetForm extends DirectorObjectForm $this->host = $host; return $this; } + protected function addSingleImportsElement() { $enum = $this->enumAllowedTemplates(); From a684929cf508cb93d4659a15f4283f8775c0a3ae Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:48:26 +0200 Subject: [PATCH 32/38] ObjectController: allow Service Sets in Branches --- .../Director/Web/Controller/ObjectController.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/library/Director/Web/Controller/ObjectController.php b/library/Director/Web/Controller/ObjectController.php index 53fd8bb1..0c06937d 100644 --- a/library/Director/Web/Controller/ObjectController.php +++ b/library/Director/Web/Controller/ObjectController.php @@ -140,13 +140,6 @@ abstract class ObjectController extends ActionController if ($oType = $this->params->get('type', 'object')) { $form->setPreferredObjectType($oType); } - if ($this->getTableName() === 'icinga_service_set' - && $this->showNotInBranch($this->translate('Creating Service Sets')) - ) { - $this->addTitle($this->translate('Create a new Service Set')); - return; - } - if ($oType === 'template') { if ($this->showNotInBranch($this->translate('Creating Templates'))) { $this->addTitle($this->translate('Create a new Template')); @@ -174,7 +167,11 @@ abstract class ObjectController extends ActionController $object = $this->requireObject(); $this->tabs()->activate('modify'); $this->addObjectTitle(); - if ($object->isTemplate() && $this->showNotInBranch($this->translate('Modifying Templates'))) { + // Hint: Service Sets are 'templates' (as long as not being assigned to a host + if ($this->getTableName() !== 'icinga_service_set' + && $object->isTemplate() + && $this->showNotInBranch($this->translate('Modifying Templates')) + ) { return; } if ($object->isApplyRule() && $this->showNotInBranch($this->translate('Modifying Apply Rules'))) { From 25f961a4c8c7c1abd80776dc094fb06aeb228c99 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:50:37 +0200 Subject: [PATCH 33/38] HostController: allow to add Sets --- application/controllers/HostController.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php index e55310d1..534978f8 100644 --- a/application/controllers/HostController.php +++ b/application/controllers/HostController.php @@ -119,9 +119,6 @@ class HostController extends ObjectController $host = $this->getHostObject(); $this->addServicesHeader(); $this->addTitle($this->translate('Add Service Set to %s'), $host->getObjectName()); - if ($this->showNotInBranch($this->translate('Creating Service Sets'))) { - return; - } $this->content()->add( IcingaServiceSetForm::load() From 59d62d0ff9d2537852e6342b5b336522041a5ac1 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:53:00 +0200 Subject: [PATCH 34/38] ObjectSetTable: branch support --- library/Director/Web/Table/ObjectSetTable.php | 99 +++++++++++++++---- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/library/Director/Web/Table/ObjectSetTable.php b/library/Director/Web/Table/ObjectSetTable.php index a8f69ddf..c7dd05cb 100644 --- a/library/Director/Web/Table/ObjectSetTable.php +++ b/library/Director/Web/Table/ObjectSetTable.php @@ -7,11 +7,14 @@ use Icinga\Module\Director\Db; use gipfl\IcingaWeb2\Link; use gipfl\IcingaWeb2\Table\ZfQueryBasedTable; use gipfl\IcingaWeb2\Url; +use Icinga\Module\Director\Db\DbSelectParenthesis; use Icinga\Module\Director\Restriction\FilterByNameRestriction; use Ramsey\Uuid\Uuid; class ObjectSetTable extends ZfQueryBasedTable { + use TableWithBranchSupport; + protected $searchColumns = [ 'os.object_name', 'os.description', @@ -51,7 +54,8 @@ class ObjectSetTable extends ZfQueryBasedTable $url = Url::fromPath("director/${type}set", $params); - return static::tr([ + $classes = $this->getRowClasses($row); + $tr = static::tr([ static::td([ Link::create(sprintf( $this->translate('%s (%d members)'), @@ -61,24 +65,47 @@ class ObjectSetTable extends ZfQueryBasedTable $row->description ? ': ' . $row->description : null ]) ]); + if (! empty($classes)) { + $tr->getAttributes()->add('class', $classes); + } + + return $tr; + } + + protected function getRowClasses($row) + { + if ($row->branch_uuid !== null) { + return ['branch_modified']; + } + return []; } protected function prepareQuery() { $type = $this->getType(); + $table = "icinga_${type}_set"; $columns = [ 'id' => 'os.id', 'uuid' => 'os.uuid', + 'branch_uuid' => '(NULL)', 'object_name' => 'os.object_name', 'object_type' => 'os.object_type', 'assign_filter' => 'os.assign_filter', 'description' => 'os.description', 'count_services' => 'COUNT(DISTINCT o.id)', ]; + if ($this->branchUuid) { + $columns['count_services'] = 'COUNT(DISTINCT COALESCE(o.uuid))'; + $columns['branch_uuid'] = 'bos.branch_uuid'; + } + if ($this->branchUuid) { + $columns = $this->branchifyColumns($columns); + $this->stripSearchColumnAliases(); + } $query = $this->db()->select()->from( - ['os' => "icinga_${type}_set"], + ['os' => $table], $columns )->joinLeft( ['o' => "icinga_${type}"], @@ -92,22 +119,60 @@ class ObjectSetTable extends ZfQueryBasedTable "${type}_set" ); $nameFilter->applyToQuery($query, 'os'); - // Disabled for now, check for correctness: - // $query->joinLeft( - // ['osi' => "icinga_${type}_set_inheritance"], - // "osi.parent_${type}_set_id = os.id", - // [] - // )->joinLeft( - // ['oso' => "icinga_${type}_set"], - // "oso.id = oso.${type}_set_id", - // [] - // ); - // 'count_hosts' => 'COUNT(DISTINCT oso.id)', + if ($this->branchUuid) { + $right = clone($query); - $query - ->group('os.id') - ->where('os.object_type = ?', 'template') - ->order('os.object_name'); + /** @var Db $conn */ + $conn = $this->connection(); + $query->joinLeft( + ['bos' => "branched_$table"], + // TODO: PgHexFunc + $this->db()->quoteInto( + 'bos.uuid = os.uuid AND bos.branch_uuid = ?', + $conn->quoteBinary($this->branchUuid->getBytes()) + ), + [] + )->where("(bos.branch_deleted IS NULL OR bos.branch_deleted = 'n')"); + $right->joinRight( + ['bos' => "branched_$table"], + 'bos.uuid = os.uuid', + [] + ) + ->where('os.uuid IS NULL') + ->where('bos.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes())); + $query->group('COALESCE(os.uuid, bos.uuid)'); + $right->group('COALESCE(os.uuid, bos.uuid)'); + + $query = $this->db()->select()->union([ + 'l' => new DbSelectParenthesis($query), + 'r' => new DbSelectParenthesis($right), + ]); + $query = $this->db()->select()->from(['u' => $query]); + $query->order('object_name')->limit(100); + + $query + ->group('uuid') + ->where('object_type = ?', 'template') + ->order('object_name'); + + } else { + // Disabled for now, check for correctness: + // $query->joinLeft( + // ['osi' => "icinga_${type}_set_inheritance"], + // "osi.parent_${type}_set_id = os.id", + // [] + // )->joinLeft( + // ['oso' => "icinga_${type}_set"], + // "oso.id = oso.${type}_set_id", + // [] + // ); + // 'count_hosts' => 'COUNT(DISTINCT oso.id)', + + $query + ->group('os.uuid') + ->where('os.object_type = ?', 'template') + ->order('os.object_name'); + } return $query; } From c56b190469bcc1d3c511498d0569388ed24cf2e3 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:54:28 +0200 Subject: [PATCH 35/38] ServicesetController: allow branch access --- application/controllers/ServicesetController.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/application/controllers/ServicesetController.php b/application/controllers/ServicesetController.php index c5507c0c..684d2fc8 100644 --- a/application/controllers/ServicesetController.php +++ b/application/controllers/ServicesetController.php @@ -71,7 +71,9 @@ class ServicesetController extends ObjectController ['class' => 'icon-plus'] )); - IcingaServiceSetServiceTable::load($set)->renderTo($this); + IcingaServiceSetServiceTable::load($set) + ->setBranch($this->getBranch()) + ->renderTo($this); } public function hostsAction() @@ -98,16 +100,17 @@ class ServicesetController extends ObjectController protected function addServiceSetTabs() { - if ($this->branch->isBranch()) { - return $this; - } $hexUuid = $this->object->getUniqueId()->toString(); $tabs = $this->tabs(); $tabs->add('services', [ 'url' => 'director/serviceset/services', 'urlParams' => ['uuid' => $hexUuid], 'label' => 'Services' - ])->add('hosts', [ + ]); + if ($this->branch->isBranch()) { + return $this; + } + $tabs->add('hosts', [ 'url' => 'director/serviceset/hosts', 'urlParams' => ['uuid' => $hexUuid], 'label' => 'Hosts' From b19dd5f62da0d6c58857ff0c303f58920c68afb6 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 12:55:55 +0200 Subject: [PATCH 36/38] IcingaServiceSet: use query builder to retrieve... ...services, this is required for branches --- library/Director/Objects/IcingaServiceSet.php | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php index 60648a55..8217a594 100644 --- a/library/Director/Objects/IcingaServiceSet.php +++ b/library/Director/Objects/IcingaServiceSet.php @@ -4,6 +4,7 @@ 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; @@ -104,30 +105,20 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface /** * @param IcingaServiceSet $set - * @return array + * @return IcingaService[] * @throws \Icinga\Exception\NotFoundError */ protected function getServiceObjectsForSet(IcingaServiceSet $set) { - if ($set->get('id') === null) { - return array(); - } $connection = $this->getConnection(); - $db = $this->getDb(); - $uuids = $db->fetchCol( - $db->select()->from('icinga_service', 'uuid') - ->where('service_set_id = ?', $set->get('id')) - ); - - $services = array(); - foreach ($uuids as $uuid) { - $service = IcingaService::loadWithUniqueId(Uuid::fromBytes(DbUtil::binaryResult($uuid)), $connection); - $service->set('service_set', null); - - $services[$service->getObjectName()] = $service; + if (self::$dbObjectStore !== null) { + $branchUuid = self::$dbObjectStore->getBranch()->getUuid(); + } else { + $branchUuid = null; } - return $services; + $builder = new ServiceSetQueryBuilder($connection, $branchUuid); + return $builder->fetchServicesWithQuery($builder->selectServicesForSet($set)); } public function getUniqueIdentifier() @@ -487,11 +478,16 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface public function getRenderingZone(IcingaConfig $config = null) { if ($this->get('host_id') === null) { - return $this->connection->getDefaultGlobalZoneName(); + 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); } + + return $host->getRenderingZone($config); } public function createWhere() @@ -511,17 +507,13 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface */ public function fetchServices() { - $connection = $this->getConnection(); - $db = $connection->getDbAdapter(); - - /** @var IcingaService[] $services */ - $services = IcingaService::loadAll( - $connection, - $db->select()->from('icinga_service') - ->where('service_set_id = ?', $this->get('id')) - ); - - return $services; + if ($store = self::$dbObjectStore) { + $uuid = $store->getBranch()->getUuid(); + } else { + $uuid = null; + } + $builder = new ServiceSetQueryBuilder($this->getConnection(), $uuid); + return $builder->fetchServicesWithQuery($builder->selectServicesForSet($this)); } /** @@ -561,7 +553,7 @@ class IcingaServiceSet extends IcingaObject implements ExportInterface $name = $this->getObjectName(); - if ($this->isObject() && $this->get('host_id') === null) { + 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' ); From 42b06a0b37f5ae30b7f999e4df3ee455c80633df Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 14:45:04 +0200 Subject: [PATCH 37/38] schema: branched service sets --- schema/mysql-migrations/upgrade_180.sql | 26 ++++++++++++++++++++ schema/mysql.sql | 27 ++++++++++++++++++++- schema/pgsql-migrations/upgrade_180.sql | 32 +++++++++++++++++++++++++ schema/pgsql.sql | 31 +++++++++++++++++++++++- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 schema/mysql-migrations/upgrade_180.sql create mode 100644 schema/pgsql-migrations/upgrade_180.sql diff --git a/schema/mysql-migrations/upgrade_180.sql b/schema/mysql-migrations/upgrade_180.sql new file mode 100644 index 00000000..fb44365e --- /dev/null +++ b/schema/mysql-migrations/upgrade_180.sql @@ -0,0 +1,26 @@ +CREATE TABLE branched_icinga_service_set ( + uuid VARBINARY(16) NOT NULL, + branch_uuid VARBINARY(16) NOT NULL, + branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n', + branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n', + + object_name VARCHAR(128) DEFAULT NULL, + object_type ENUM('object', 'template', 'external_object') DEFAULT NULL, + host VARCHAR(255) DEFAULT NULL, + description TEXT DEFAULT NULL, + assign_filter TEXT DEFAULT NULL, + + imports TEXT DEFAULT NULL, + set_null TEXT DEFAULT NULL, + PRIMARY KEY (branch_uuid, uuid), + INDEX search_object_name (object_name), + CONSTRAINT icinga_service_set_branch + FOREIGN KEY branch (branch_uuid) + REFERENCES director_branch (uuid) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO director_schema_migration + (schema_version, migration_time) + VALUES (180, NOW()); diff --git a/schema/mysql.sql b/schema/mysql.sql index b8f65e8d..1b3dfb62 100644 --- a/schema/mysql.sql +++ b/schema/mysql.sql @@ -2309,6 +2309,31 @@ CREATE TABLE branched_icinga_service ( ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE branched_icinga_service_set ( + uuid VARBINARY(16) NOT NULL, + branch_uuid VARBINARY(16) NOT NULL, + branch_created ENUM('y', 'n') NOT NULL DEFAULT 'n', + branch_deleted ENUM('y', 'n') NOT NULL DEFAULT 'n', + + object_name VARCHAR(128) DEFAULT NULL, + object_type ENUM('object', 'template', 'external_object') DEFAULT NULL, + host VARCHAR(255) DEFAULT NULL, + description TEXT DEFAULT NULL, + assign_filter TEXT DEFAULT NULL, + + + imports TEXT DEFAULT NULL, + imports TEXT DEFAULT NULL, + set_null TEXT DEFAULT NULL, + PRIMARY KEY (branch_uuid, uuid), + INDEX search_object_name (object_name), + CONSTRAINT icinga_service_set_branch + FOREIGN KEY branch (branch_uuid) + REFERENCES director_branch (uuid) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CREATE TABLE branched_icinga_notification ( uuid VARBINARY(16) NOT NULL, branch_uuid VARBINARY(16) NOT NULL, @@ -2415,4 +2440,4 @@ CREATE TABLE branched_icinga_dependency ( INSERT INTO director_schema_migration (schema_version, migration_time) - VALUES (179, NOW()); + VALUES (180, NOW()); diff --git a/schema/pgsql-migrations/upgrade_180.sql b/schema/pgsql-migrations/upgrade_180.sql new file mode 100644 index 00000000..b6ae70d4 --- /dev/null +++ b/schema/pgsql-migrations/upgrade_180.sql @@ -0,0 +1,32 @@ +CREATE TABLE branched_icinga_service_set ( + uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16), + branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16), + branch_created enum_boolean NOT NULL DEFAULT 'n', + branch_deleted enum_boolean NOT NULL DEFAULT 'n', + + object_name character varying(255) DEFAULT NULL, + object_type enum_object_type_all DEFAULT NULL, + disabled enum_boolean DEFAULT NULL, + host character varying(255) DEFAULT NULL, + description TEXT DEFAULT NULL, + assign_filter text DEFAULT NULL, + + + imports TEXT DEFAULT NULL, + vars TEXT DEFAULT NULL, + set_null TEXT DEFAULT NULL, + PRIMARY KEY (branch_uuid, uuid), + CONSTRAINT icinga_service_branch + FOREIGN KEY (branch_uuid) + REFERENCES director_branch (uuid) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX service_set_branch_object_name ON branched_icinga_service_set (branch_uuid, object_name); +CREATE INDEX branched_service_set_search_object_name ON branched_icinga_service_set (object_name); + + +INSERT INTO director_schema_migration + (schema_version, migration_time) + VALUES (180, NOW()); diff --git a/schema/pgsql.sql b/schema/pgsql.sql index 025ec5b4..bae64257 100644 --- a/schema/pgsql.sql +++ b/schema/pgsql.sql @@ -2637,6 +2637,35 @@ CREATE INDEX branched_service_search_object_name ON branched_icinga_service (obj CREATE INDEX branched_service_search_display_name ON branched_icinga_service (display_name); +CREATE TABLE branched_icinga_service_set ( + uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16), + branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16), + branch_created enum_boolean NOT NULL DEFAULT 'n', + branch_deleted enum_boolean NOT NULL DEFAULT 'n', + + object_name character varying(255) DEFAULT NULL, + object_type enum_object_type_all DEFAULT NULL, + disabled enum_boolean DEFAULT NULL, + host character varying(255) DEFAULT NULL, + description TEXT DEFAULT NULL, + assign_filter text DEFAULT NULL, + + + imports TEXT DEFAULT NULL, + vars TEXT DEFAULT NULL, + set_null TEXT DEFAULT NULL, + PRIMARY KEY (branch_uuid, uuid), + CONSTRAINT icinga_service_branch + FOREIGN KEY (branch_uuid) + REFERENCES director_branch (uuid) + ON DELETE CASCADE + ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX service_set_branch_object_name ON branched_icinga_service_set (branch_uuid, object_name); +CREATE INDEX branched_service_set_search_object_name ON branched_icinga_service_set (object_name); + + CREATE TABLE branched_icinga_notification ( uuid bytea NOT NULL UNIQUE CHECK(LENGTH(uuid) = 16), branch_uuid bytea NOT NULL CHECK(LENGTH(branch_uuid) = 16), @@ -2749,4 +2778,4 @@ CREATE INDEX branched_dependency_search_object_name ON branched_icinga_dependenc INSERT INTO director_schema_migration (schema_version, migration_time) - VALUES (179, NOW()); + VALUES (180, NOW()); From 47488d138e9cf4f779fad4b46b19815caeca763b Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Tue, 20 Sep 2022 15:27:16 +0200 Subject: [PATCH 38/38] ObjectSetTable: fix a PostgreSQL issue --- library/Director/Web/Table/ObjectSetTable.php | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/library/Director/Web/Table/ObjectSetTable.php b/library/Director/Web/Table/ObjectSetTable.php index c7dd05cb..70ffab7e 100644 --- a/library/Director/Web/Table/ObjectSetTable.php +++ b/library/Director/Web/Table/ObjectSetTable.php @@ -93,13 +93,10 @@ class ObjectSetTable extends ZfQueryBasedTable 'object_type' => 'os.object_type', 'assign_filter' => 'os.assign_filter', 'description' => 'os.description', - 'count_services' => 'COUNT(DISTINCT o.id)', + 'count_services' => 'COUNT(DISTINCT o.uuid)', ]; if ($this->branchUuid) { - $columns['count_services'] = 'COUNT(DISTINCT COALESCE(o.uuid))'; $columns['branch_uuid'] = 'bos.branch_uuid'; - } - if ($this->branchUuid) { $columns = $this->branchifyColumns($columns); $this->stripSearchColumnAliases(); } @@ -142,6 +139,11 @@ class ObjectSetTable extends ZfQueryBasedTable ->where('bos.branch_uuid = ?', $conn->quoteBinary($this->branchUuid->getBytes())); $query->group('COALESCE(os.uuid, bos.uuid)'); $right->group('COALESCE(os.uuid, bos.uuid)'); + if ($conn->isPgsql()) { + // This is ugly, might want to modify the query - even a subselect looks better + $query->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid'); + $right->group('bos.uuid')->group('os.uuid')->group('os.id')->group('bos.branch_uuid'); + } $query = $this->db()->select()->union([ 'l' => new DbSelectParenthesis($query), @@ -154,7 +156,18 @@ class ObjectSetTable extends ZfQueryBasedTable ->group('uuid') ->where('object_type = ?', 'template') ->order('object_name'); - + if ($conn->isPgsql()) { + // BS. Drop count? Sub-select? Better query? + $query + ->group('uuid') + ->group('id') + ->group('branch_uuid') + ->group('object_name') + ->group('object_type') + ->group('assign_filter') + ->group('description') + ->group('count_services'); + }; } else { // Disabled for now, check for correctness: // $query->joinLeft(