467 lines
14 KiB
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());
|
|
}
|
|
}
|
|
}
|
|
}
|