From b2afca24965bbbe758158abcc19effe52f3ae015 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Fri, 1 Jul 2022 08:36:01 +0200 Subject: [PATCH] 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());