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()); } } } }