icingaweb2-module-director/library/Director/Core/LegacyDeploymentApi.php

467 lines
14 KiB
PHP

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