diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:17:31 +0000 |
commit | f66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch) | |
tree | fbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Deployment | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip |
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | library/Director/Deployment/ConditionalConfigRenderer.php | 64 | ||||
-rw-r--r-- | library/Director/Deployment/ConditionalDeployment.php | 190 | ||||
-rw-r--r-- | library/Director/Deployment/DeploymentGracePeriod.php | 61 | ||||
-rw-r--r-- | library/Director/Deployment/DeploymentInfo.php | 59 | ||||
-rw-r--r-- | library/Director/Deployment/DeploymentStatus.php | 164 |
5 files changed, 538 insertions, 0 deletions
diff --git a/library/Director/Deployment/ConditionalConfigRenderer.php b/library/Director/Deployment/ConditionalConfigRenderer.php new file mode 100644 index 0000000..0b24418 --- /dev/null +++ b/library/Director/Deployment/ConditionalConfigRenderer.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Director\Deployment; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorActivityLog; + +class ConditionalConfigRenderer +{ + /** @var Db */ + protected $db; + + protected $forceRendering = false; + + public function __construct(Db $connection) + { + $this->db = $connection; + } + + public function forceRendering($force = true) + { + $this->forceRendering = $force; + + return $this; + } + + public function getConfig() + { + if ($this->shouldGenerate()) { + return IcingaConfig::generate($this->db); + } + + return $this->loadLatestActivityConfig(); + } + + protected function loadLatestActivityConfig() + { + $db = $this->db; + + return IcingaConfig::loadByActivityChecksum($db->getLastActivityChecksum(), $db); + } + + protected function shouldGenerate() + { + return $this->forceRendering || !$this->configForLatestActivityExists(); + } + + protected function configForLatestActivityExists() + { + $db = $this->db; + try { + $latestActivity = DirectorActivityLog::loadLatest($db); + } catch (NotFoundError $e) { + return false; + } + + return IcingaConfig::existsForActivityChecksum( + bin2hex($latestActivity->get('checksum')), + $db + ); + } +} diff --git a/library/Director/Deployment/ConditionalDeployment.php b/library/Director/Deployment/ConditionalDeployment.php new file mode 100644 index 0000000..0f64028 --- /dev/null +++ b/library/Director/Deployment/ConditionalDeployment.php @@ -0,0 +1,190 @@ +<?php + +namespace Icinga\Module\Director\Deployment; + +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\Core\CoreApi; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\NullLogger; + +class ConditionalDeployment implements LoggerAwareInterface +{ + use LoggerAwareTrait; + + /** @var Db */ + protected $db; + + /** @var CoreApi */ + protected $api; + + /** @var ?DeploymentGracePeriod */ + protected $gracePeriod = null; + + protected $force = false; + + protected $hasBeenForced = false; + + /** @var ?string */ + protected $noDeploymentReason = null; + + public function __construct(Db $connection, CoreApi $api = null) + { + $this->setLogger(new NullLogger()); + $this->db = $connection; + if ($api === null) { + $this->api = $connection->getDeploymentEndpoint()->api(); + } else { + $this->api = $api; + } + $this->refresh(); + } + + /** + * @param IcingaConfig $config + * @return ?DirectorDeploymentLog + */ + public function deploy(IcingaConfig $config) + { + $this->hasBeenForced = false; + if ($this->shouldDeploy($config)) { + return $this->reallyDeploy($config); + } elseif ($this->force) { + $deployment = $this->reallyDeploy($config); + $this->hasBeenForced = true; + + return $deployment; + } + + return null; + } + + /** + * @param bool $force + * @return $this + */ + public function force($force = true) + { + $this->force = $force; + return $this; + } + + public function setGracePeriod(DeploymentGracePeriod $gracePeriod) + { + $this->gracePeriod = $gracePeriod; + return $this; + } + + public function refresh() + { + $this->api->collectLogFiles($this->db); + $this->api->wipeInactiveStages($this->db); + } + + public function waitForStartupAfterDeploy(DirectorDeploymentLog $deploymentLog, $timeout) + { + $startTime = time(); + while ((time() - $startTime) <= $timeout) { + $deploymentFromDB = DirectorDeploymentLog::load($deploymentLog->getId(), $this->db); + $stageCollected = $deploymentFromDB->get('stage_collected'); + if ($stageCollected === null) { + usleep(500000); + continue; + } + if ($stageCollected === 'n') { + return 'stage has not been collected (Icinga "lost" the deployment)'; + } + if ($deploymentFromDB->get('startup_succeeded') === 'y') { + return true; + } + return 'deployment failed during startup (usually a Configuration Error)'; + } + return 'deployment timed out (while waiting for an Icinga restart)'; + } + + /** + * @return string|null + */ + public function getNoDeploymentReason() + { + return $this->noDeploymentReason; + } + + public function hasBeenForced() + { + return $this->hasBeenForced; + } + + protected function shouldDeploy(IcingaConfig $config) + { + $this->noDeploymentReason = null; + if ($this->hasNeverDeployed()) { + return true; + } + + if ($this->isWithinGracePeriod()) { + $this->noDeploymentReason = 'Grace period is active'; + return false; + } + + if ($this->deployedConfigMatches($config)) { + $this->noDeploymentReason = 'Config matches last deployed one'; + return false; + } + + if ($this->getActiveChecksum() === $config->getHexChecksum()) { + $this->noDeploymentReason = 'Config matches active stage'; + return false; + } + + return true; + } + + protected function hasNeverDeployed() + { + return !DirectorDeploymentLog::hasDeployments($this->db); + } + + protected function isWithinGracePeriod() + { + return $this->gracePeriod && $this->gracePeriod->isActive(); + } + + protected function deployedConfigMatches(IcingaConfig $config) + { + if ($deployment = DirectorDeploymentLog::optionalLatest($this->db)) { + return $deployment->getConfigHexChecksum() === $config->getHexChecksum(); + } + + return false; + } + + protected function getActiveChecksum() + { + return DirectorDeploymentLog::getConfigChecksumForStageName( + $this->db, + $this->api->getActiveStageName() + ); + } + + /** + * @param IcingaConfig $config + * @return bool|DirectorDeploymentLog + * @throws IcingaException + * @throws \Icinga\Module\Director\Exception\DuplicateKeyException + */ + protected function reallyDeploy(IcingaConfig $config) + { + $checksum = $config->getHexChecksum(); + $this->logger->info(sprintf('Director ConfigJob ready to deploy "%s"', $checksum)); + if ($deployment = $this->api->dumpConfig($config, $this->db)) { + $this->logger->notice(sprintf('Director ConfigJob deployed config "%s"', $checksum)); + return $deployment; + } else { + throw new IcingaException('Failed to deploy config "%s"', $checksum); + } + } +} diff --git a/library/Director/Deployment/DeploymentGracePeriod.php b/library/Director/Deployment/DeploymentGracePeriod.php new file mode 100644 index 0000000..6cde25a --- /dev/null +++ b/library/Director/Deployment/DeploymentGracePeriod.php @@ -0,0 +1,61 @@ +<?php + +namespace Icinga\Module\Director\Deployment; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; + +class DeploymentGracePeriod +{ + /** @var int */ + protected $graceTimeSeconds; + + /** @var Db */ + protected $db; + + /** + * @param int $graceTimeSeconds + * @param Db $db + */ + public function __construct($graceTimeSeconds, Db $db) + { + $this->graceTimeSeconds = $graceTimeSeconds; + $this->db = $db; + } + + /** + * Whether we're still within a grace period + * @return bool + */ + public function isActive() + { + if ($deployment = $this->lastDeployment()) { + return $deployment->getDeploymentTimestamp() > $this->getGracePeriodStart(); + } + + return false; + } + + protected function getGracePeriodStart() + { + return time() - $this->graceTimeSeconds; + } + + public function getRemainingGraceTime() + { + if ($this->isActive()) { + if ($deployment = $this->lastDeployment()) { + return $deployment->getDeploymentTimestamp() - $this->getGracePeriodStart(); + } else { + return null; + } + } + + return 0; + } + + protected function lastDeployment() + { + return DirectorDeploymentLog::optionalLatest($this->db); + } +} diff --git a/library/Director/Deployment/DeploymentInfo.php b/library/Director/Deployment/DeploymentInfo.php new file mode 100644 index 0000000..77d52de --- /dev/null +++ b/library/Director/Deployment/DeploymentInfo.php @@ -0,0 +1,59 @@ +<?php + +namespace Icinga\Module\Director\Deployment; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaObject; + +class DeploymentInfo +{ + /** @var IcingaObject */ + protected $object; + + protected $db; + + /** @var int */ + protected $totalChanges; + + /** @var int */ + protected $objectChanges; + + public function __construct(Db $db) + { + $this->db = $db; + } + + public function setObject(IcingaObject $object) + { + $this->object = $object; + return $this; + } + + public function getTotalChanges() + { + if ($this->totalChanges === null) { + $this->totalChanges = $this->db->countActivitiesSinceLastDeployedConfig(); + } + + return $this->totalChanges; + } + + public function getSingleObjectChanges() + { + if ($this->objectChanges === null) { + if ($this->object === null) { + $this->objectChanges = 0; + } else { + $this->objectChanges = $this->db + ->countActivitiesSinceLastDeployedConfig($this->object); + } + } + + return $this->objectChanges; + } + + public function hasUndeployedChanges() + { + return $this->getSingleObjectChanges() > 0 && $this->getTotalChanges() > 0; + } +} diff --git a/library/Director/Deployment/DeploymentStatus.php b/library/Director/Deployment/DeploymentStatus.php new file mode 100644 index 0000000..ae850c6 --- /dev/null +++ b/library/Director/Deployment/DeploymentStatus.php @@ -0,0 +1,164 @@ +<?php + +namespace Icinga\Module\Director\Deployment; + +use Exception; +use Icinga\Module\Director\Core\CoreApi; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; + +class DeploymentStatus +{ + protected $db; + + protected $api; + + public function __construct(Db $db, CoreApi $api) + { + $this->db = $db; + $this->api = $api; + } + + public function getDeploymentStatus($configs = null, $activities = null) + { + try { + if (DirectorDeploymentLog::hasUncollected($this->db)) { + $this->api->collectLogFiles($this->db); + } + } catch (Exception $e) { + // Ignore eventual issues while talking to Icinga + } + + $activeConfiguration = null; + $lastActivityLogChecksum = null; + $configChecksum = null; + if ($stageName = $this->api->getActiveStageName()) { + $activityLogChecksum = DirectorDeploymentLog::getRelatedToActiveStage($this->api, $this->db); + if ($activityLogChecksum === null) { + $activeConfiguration = [ + 'stage_name' => $stageName, + 'config' => null, + 'activity' => null + ]; + } else { + $lastActivityLogChecksum = bin2hex($activityLogChecksum->get('last_activity_checksum')); + $configChecksum = $this->getConfigChecksumForStageName($stageName); + $activeConfiguration = [ + 'stage_name' => $stageName, + 'config' => ($configChecksum) ? : null, + 'activity' => $lastActivityLogChecksum + ]; + } + } + $result = [ + 'active_configuration' => (object) $activeConfiguration, + ]; + + if ($configs) { + $result['configs'] = (object) $this->getDeploymentStatusForConfigChecksums( + explode(',', $configs), + $configChecksum + ); + } + + if ($activities) { + $result['activities'] = (object) $this->getDeploymentStatusForActivityLogChecksums( + explode(',', $activities), + $lastActivityLogChecksum + ); + } + return (object) $result; + } + + public function getConfigChecksumForStageName($stageName) + { + $db = $this->db->getDbAdapter(); + $query = $db->select()->from( + ['l' => 'director_deployment_log'], + ['checksum' => $this->db->dbHexFunc('l.config_checksum')] + )->where('l.stage_name = ?', $stageName); + + return $db->fetchOne($query); + } + + public function getDeploymentStatusForConfigChecksums($configChecksums, $activeConfigChecksum) + { + $db = $this->db->getDbAdapter(); + $results = array_combine($configChecksums, array_map(function () { + return 'unknown'; + }, $configChecksums)); + $binaryConfigChecksums = []; + foreach ($configChecksums as $singleConfigChecksum) { + $binaryConfigChecksums[$singleConfigChecksum] = $this->db->quoteBinary(hex2bin($singleConfigChecksum)); + } + $deployedConfigs = $this->getDeployedConfigs(array_values($binaryConfigChecksums)); + + foreach ($results as $singleChecksum => &$status) { + // active if it's equal to the provided active + if ($singleChecksum === $activeConfigChecksum) { + $status = 'active'; + } else { + if (isset($deployedConfigs[$singleChecksum])) { + $status = ($deployedConfigs[$singleChecksum] === 'y') ? 'deployed' : 'failed'; + } else { + // check if it's in generated_config table it is undeployed + $generatedConfigQuery = $db->select()->from( + ['g' => 'director_generated_config'], + ['checksum' => 'g.checksum'] + )->where('g.checksum = ?', $binaryConfigChecksums[$singleChecksum]); + if ($db->fetchOne($generatedConfigQuery)) { + $status = 'undeployed'; + } + } + // otherwise leave unknown + } + } + + return $results; + } + + public function getDeploymentStatusForActivityLogChecksums($activityLogChecksums, $activeActivityLogChecksum) + { + $db = $this->db->getDbAdapter(); + $results = array_combine($activityLogChecksums, array_map(function () { + return 'unknown'; + }, $activityLogChecksums)); + + foreach ($results as $singleActivityLogChecksum => &$status) { + // active if it's equal to the provided active + if ($singleActivityLogChecksum === $activeActivityLogChecksum) { + $status = 'active'; + } else { + // get last deployed activity id and check if it's less than the passed one + $generatedConfigQuery = $db->select()->from( + ['a' => 'director_activity_log'], + ['id' => 'a.id'] + )->where('a.checksum = ?', $this->db->quoteBinary(hex2bin($singleActivityLogChecksum))); + if ($singleActivityLogData = $db->fetchOne($generatedConfigQuery)) { + if ($lastDeploymentActivityLogId = $this->db->getLastDeploymentActivityLogId()) { + if ((int) $singleActivityLogData > $lastDeploymentActivityLogId) { + $status = 'undeployed'; + } else { + $status = 'deployed'; + } + } + } + } + } + return $results; + } + + /** + * @param array $binaryConfigChecksums + * @return array + */ + public function getDeployedConfigs(array $binaryConfigChecksums) + { + $db = $this->db->getDbAdapter(); + $deploymentLogQuery = $db->select()->from(['l' => 'director_deployment_log'], [ + 'checksum' => $this->db->dbHexFunc('l.config_checksum'), + 'deployed' => 'l.startup_succeeded' + ])->where('l.config_checksum IN (?)', $binaryConfigChecksums); + return $db->fetchPairs($deploymentLogQuery); + } +} |