summaryrefslogtreecommitdiffstats
path: root/library/Director/Deployment
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/Deployment
parentInitial commit. (diff)
downloadicingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz
icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Director/Deployment')
-rw-r--r--library/Director/Deployment/ConditionalConfigRenderer.php64
-rw-r--r--library/Director/Deployment/ConditionalDeployment.php190
-rw-r--r--library/Director/Deployment/DeploymentGracePeriod.php61
-rw-r--r--library/Director/Deployment/DeploymentInfo.php59
-rw-r--r--library/Director/Deployment/DeploymentStatus.php164
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);
+ }
+}