diff options
Diffstat (limited to '')
-rw-r--r-- | library/Director/Core/LegacyDeploymentApi.php | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/library/Director/Core/LegacyDeploymentApi.php b/library/Director/Core/LegacyDeploymentApi.php new file mode 100644 index 0000000..7287c4a --- /dev/null +++ b/library/Director/Core/LegacyDeploymentApi.php @@ -0,0 +1,466 @@ +<?php + +namespace Icinga\Module\Director\Core; + +use Exception; +use Icinga\Exception\IcingaException; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\IcingaConfig\IcingaConfig; +use Icinga\Module\Director\Objects\DirectorDeploymentLog; + +/** + * Legacy DeploymentApi for Icinga 1.x configuration deployment + * + * @package Icinga\Module\Director\Core + */ +class LegacyDeploymentApi implements DeploymentApiInterface +{ + protected $db; + protected $deploymentPath; + protected $activationScript; + + protected $dir_mode; + protected $file_mode; + + public function __construct(Db $db) + { + $this->db = $db; + $settings = $this->db->settings(); + $this->deploymentPath = $settings->deployment_path_v1; + $this->activationScript = $settings->activation_script_v1; + + $this->dir_mode = base_convert($settings->get('deployment_file_mode_v1', '2775'), 8, 10); + $this->file_mode = base_convert($settings->get('deployment_dir_mode_v1', '0664'), 8, 10); + } + + /** + * TODO: merge in common class + * @inheritdoc + */ + public function collectLogFiles(Db $db) + { + $packageName = $db->settings()->get('icinga_package_name'); + $existing = $this->listPackageStages($packageName); + + foreach (DirectorDeploymentLog::getUncollected($db) as $deployment) { + $stage = $deployment->get('stage_name'); + if (! in_array($stage, $existing)) { + continue; + } + + try { + $availableFiles = $this->listStageFiles($stage); + } catch (Exception $e) { + // Could not collect stage files. Doesn't matter, let's try next time + continue; + } + + if (in_array('startup.log', $availableFiles) + && in_array('status', $availableFiles) + ) { + $status = $this->getStagedFile($stage, 'status'); + $status = trim($status); + if ($status === '0') { + $deployment->set('startup_succeeded', 'y'); + } else { + $deployment->set('startup_succeeded', 'n'); + } + $deployment->set('startup_log', $this->shortenStartupLog( + $this->getStagedFile($stage, 'startup.log') + )); + } else { + // Stage seems to be incomplete, let's try again next time + continue; + } + $deployment->set('stage_collected', 'y'); + + $deployment->store(); + } + } + + /** + * TODO: merge in common class + * @inheritdoc + */ + public function wipeInactiveStages(Db $db) + { + $uncollected = DirectorDeploymentLog::getUncollected($db); + $packageName = $db->settings()->get('icinga_package_name'); + $currentStage = $this->getActiveStageName(); + + // try to expire old deployments + foreach ($uncollected as $name => $deployment) { + /** @var DirectorDeploymentLog $deployment */ + if ($deployment->get('dump_succeeded') === 'n' + || $deployment->get('startup_succeeded') === null + ) { + $start_time = strtotime($deployment->start_time); + + // older than an hour and no startup + if ($start_time + 3600 < time()) { + $deployment->set('startup_succeeded', 'n'); + $deployment->set('startup_log', 'Activation timed out...'); + $deployment->store(); + } + } + } + + foreach ($this->listPackageStages($packageName) as $stage) { + if (array_key_exists($stage, $uncollected) + && $uncollected[$stage]->get('startup_succeeded') === null + ) { + continue; + } elseif ($stage === $currentStage) { + continue; + } else { + $this->deleteStage($packageName, $stage); + } + } + } + + /** @inheritdoc */ + public function getActiveStageName() + { + $this->assertDeploymentPath(); + + $path = $this->deploymentPath . DIRECTORY_SEPARATOR . 'active'; + + if (file_exists($path)) { + if (is_link($path)) { + $linkTarget = readlink($path); + $linkTargetDir = dirname($linkTarget); + $linkTargetName = basename($linkTarget); + + if ($linkTargetDir === $this->deploymentPath || $linkTargetDir === '.') { + return $linkTargetName; + } else { + throw new IcingaException( + 'Active stage link pointing to a invalid target: %s -> %s', + $path, + $linkTarget + ); + } + } else { + throw new IcingaException('Active stage is not a symlink: %s', $path); + } + } else { + return false; + } + } + + /** @inheritdoc */ + public function listStageFiles($stage) + { + $path = $this->getStagePath($stage); + if (! is_dir($path)) { + throw new IcingaException('Deployment stage "%s" does not exist at: %s', $stage, $path); + } + return $this->listDirectoryContents($path); + } + + /** @inheritdoc */ + public function listPackageStages($packageName) + { + $this->assertPackageName($packageName); + $this->assertDeploymentPath(); + + $dh = @opendir($this->deploymentPath); + if ($dh === null) { + throw new IcingaException('Can not list contents of %s', $this->deploymentPath); + } + + $stages = array(); + while ($file = readdir($dh)) { + if ($file === '.' || $file === '..') { + continue; + } elseif (is_dir($this->deploymentPath . DIRECTORY_SEPARATOR . $file) + && substr($file, 0, 9) === 'director-' + ) { + $stages[] = $file; + } + } + + return $stages; + } + + /** @inheritdoc */ + public function getStagedFile($stage, $file) + { + $path = $this->getStagePath($stage); + + $filePath = $path . DIRECTORY_SEPARATOR . $file; + + if (! file_exists($filePath)) { + throw new IcingaException('Could not find file %s', $filePath); + } else { + return file_get_contents($filePath); + } + } + + /** @inheritdoc */ + public function deleteStage($packageName, $stageName) + { + $this->assertPackageName($packageName); + $this->assertDeploymentPath(); + + $path = $this->getStagePath($stageName); + + static::rrmdir($path); + } + + /** @inheritdoc */ + public function dumpConfig(IcingaConfig $config, Db $db, $packageName = null) + { + if ($packageName === null) { + $packageName = $db->settings()->get('icinga_package_name'); + } + $this->assertPackageName($packageName); + $this->assertDeploymentPath(); + + $start = microtime(true); + $deployment = DirectorDeploymentLog::create(array( + // 'config_id' => $config->id, + // 'peer_identity' => $endpoint->object_name, + 'peer_identity' => $this->deploymentPath, + 'start_time' => date('Y-m-d H:i:s'), + 'config_checksum' => $config->getChecksum(), + 'last_activity_checksum' => $config->getLastActivityChecksum() + // 'triggered_by' => Util::getUsername(), + // 'username' => Util::getUsername(), + // 'module_name' => $moduleName, + )); + + $stage_name = 'director-' .date('Ymd-His'); + $deployment->set('stage_name', $stage_name); + + try { + $succeeded = $this->deployStage($stage_name, $config->getFileContents()); + if ($succeeded === true) { + $succeeded = $this->activateStage($stage_name); + } + } catch (Exception $e) { + $deployment->set('dump_succeeded', 'n'); + $deployment->set('startup_log', $e->getMessage()); + $deployment->set('startup_succeeded', 'n'); + $deployment->store($db); + throw $e; + } + + $duration = (int) ((microtime(true) - $start) * 1000); + $deployment->set('duration_dump', $duration); + + $deployment->set('dump_succeeded', $succeeded === true ? 'y' : 'n'); + + $deployment->store($db); + return $succeeded; + } + + /** + * Deploy a new stage, and write all files to it + * + * @param string $stage Name of the stage + * @param array $files Array of files, $fileName => $content + * + * @return bool Success status + * + * @throws IcingaException When something could not be accessed + */ + protected function deployStage($stage, $files) + { + $path = $this->deploymentPath . DIRECTORY_SEPARATOR . $stage; + + if (file_exists($path)) { + throw new IcingaException('Stage "%s" does already exist at: ', $stage, $path); + } else { + $this->mkdir($path); + + foreach ($files as $file => $content) { + $fullPath = $path . DIRECTORY_SEPARATOR . $file; + $this->mkdir(dirname($fullPath), true); + + $fh = @fopen($fullPath, 'w'); + if ($fh === null) { + throw new IcingaException('Could not open file "%s" for writing.', $fullPath); + } + chmod($fullPath, $this->file_mode); + + fwrite($fh, $content); + fclose($fh); + } + + return true; + } + } + + /** + * Starts activation of + * + * Note: script should probably fork to background? + * + * @param string $stage Stage to activate + * + * @return bool + * + * @throws IcingaException For an execution error + */ + protected function activateStage($stage) + { + if ($this->activationScript === null || trim($this->activationScript) === '') { + // skip activation, could be done by external cron worker + return true; + } else { + $command = sprintf('%s %s 2>&1', escapeshellcmd($this->activationScript), escapeshellarg($stage)); + $output = null; + $rc = null; + exec($command, $output, $rc); + $output = join("\n", $output); + if ($rc !== 0) { + throw new IcingaException("Activation script did exit with return code %d:\n\n%s", $rc, $output); + } + return true; + } + } + + /** + * Recursively dump directory contents, with relative path + * + * @param string $path Absolute path to read from + * @param int $depth Internal counter + * + * @return string[] + * + * @throws IcingaException When directory could not be opened + */ + protected function listDirectoryContents($path, $depth = 0) + { + $dh = @opendir($path); + if ($dh === null) { + throw new IcingaException('Can not list contents of %s', $path); + } + + $files = array(); + while ($file = readdir($dh)) { + $fullPath = $path . DIRECTORY_SEPARATOR . $file; + if ($file === '.' || $file === '..') { + continue; + } elseif (is_dir($fullPath)) { + $subdirFiles = $this->listDirectoryContents($fullPath, $depth + 1); + foreach ($subdirFiles as $subFile) { + $files[] = $file . DIRECTORY_SEPARATOR . $subFile; + } + } else { + $files[] = $file; + } + } + + if ($depth === 0) { + sort($files); + } + + return $files; + } + + /** + * Assert that only the director module is interacted with + * + * @param string $packageName + * @throws IcingaException When another module is requested + */ + protected function assertPackageName($packageName) + { + if ($packageName !== 'director') { + throw new IcingaException('Does not supported different modules!'); + } + } + + /** + * Assert the deployment path to be configured, existing, and writeable + * + * @throws IcingaException + */ + protected function assertDeploymentPath() + { + if ($this->deploymentPath === null) { + throw new IcingaException('Deployment path is not configured for legacy config!'); + } elseif (! is_dir($this->deploymentPath)) { + throw new IcingaException('Deployment path is not a directory: %s', $this->deploymentPath); + } elseif (! is_writeable($this->deploymentPath)) { + throw new IcingaException('Deployment path is not a writeable: %s', $this->deploymentPath); + } + } + + /** + * TODO: avoid code duplication: copied from CoreApi + * + * @param string $log The log contents to shorten + * @return string + */ + protected function shortenStartupLog($log) + { + $logLen = strlen($log); + if ($logLen < 1024 * 60) { + return $log; + } + + $part = substr($log, 0, 1024 * 20); + $parts = explode("\n", $part); + array_pop($parts); + $begin = implode("\n", $parts) . "\n\n"; + + $part = substr($log, -1024 * 20); + $parts = explode("\n", $part); + array_shift($parts); + $end = "\n\n" . implode("\n", $parts); + + return $begin . sprintf( + '[..] %d bytes removed by Director [..]', + $logLen - (strlen($begin) + strlen($end)) + ) . $end; + } + + /** + * Return the full path of a stage + * + * @param string $stage Name of the stage + * + * @return string + */ + public function getStagePath($stage) + { + $this->assertDeploymentPath(); + return $this->deploymentPath . DIRECTORY_SEPARATOR . $stage; + } + + /** + * @from https://php.net/manual/de/function.rmdir.php#108113 + * @param $dir + */ + protected static function rrmdir($dir) + { + foreach (glob($dir . '/*') as $file) { + if (is_dir($file)) { + static::rrmdir($file); + } else { + unlink($file); + } + } + + rmdir($dir); + } + + protected function mkdir($path, $recursive = false) + { + if (! file_exists($path)) { + if ($recursive) { + $this->mkdir(dirname($path)); + } + + try { + mkdir($path); + chmod($path, $this->dir_mode); + } catch (Exception $e) { + throw new IcingaException('Could not create path "%s": %s', $path, $e->getMessage()); + } + } + } +} |