diff options
Diffstat (limited to '')
-rw-r--r-- | library/Businessprocess/Storage/ConfigDiff.php | 91 | ||||
-rw-r--r-- | library/Businessprocess/Storage/LegacyConfigParser.php | 409 | ||||
-rw-r--r-- | library/Businessprocess/Storage/LegacyConfigRenderer.php | 255 | ||||
-rw-r--r-- | library/Businessprocess/Storage/LegacyStorage.php | 207 | ||||
-rw-r--r-- | library/Businessprocess/Storage/Storage.php | 107 |
5 files changed, 1069 insertions, 0 deletions
diff --git a/library/Businessprocess/Storage/ConfigDiff.php b/library/Businessprocess/Storage/ConfigDiff.php new file mode 100644 index 0000000..495151e --- /dev/null +++ b/library/Businessprocess/Storage/ConfigDiff.php @@ -0,0 +1,91 @@ +<?php + +namespace Icinga\Module\Businessprocess\Storage; + +use Diff; +use Diff_Renderer_Html_Inline; +use Diff_Renderer_Html_SideBySide; +use Diff_Renderer_Text_Context; +use Diff_Renderer_Text_Unified; +use ipl\Html\ValidHtml; + +class ConfigDiff implements ValidHtml +{ + protected $a; + + protected $b; + + protected $diff; + protected $opcodes; + + protected function __construct($a, $b) + { + $this->requireVendorLib('Diff.php'); + + if (empty($a)) { + $this->a = array(); + } else { + $this->a = explode("\n", (string) $a); + } + + if (empty($b)) { + $this->b = array(); + } else { + $this->b = explode("\n", (string) $b); + } + + $options = array( + 'context' => 5, + // 'ignoreWhitespace' => true, + // 'ignoreCase' => true, + ); + $this->diff = new Diff($this->a, $this->b, $options); + } + + /** + * @return string + */ + public function render() + { + return $this->renderHtmlSideBySide(); + } + + public function renderHtmlSideBySide() + { + $this->requireVendorLib('Diff/Renderer/Html/SideBySide.php'); + $renderer = new Diff_Renderer_Html_SideBySide; + return $this->diff->render($renderer); + } + + public function renderHtmlInline() + { + $this->requireVendorLib('Diff/Renderer/Html/Inline.php'); + $renderer = new Diff_Renderer_Html_Inline; + return $this->diff->render($renderer); + } + + public function renderTextContext() + { + $this->requireVendorLib('Diff/Renderer/Text/Context.php'); + $renderer = new Diff_Renderer_Text_Context; + return $this->diff->render($renderer); + } + + public function renderTextUnified() + { + $this->requireVendorLib('Diff/Renderer/Text/Unified.php'); + $renderer = new Diff_Renderer_Text_Unified; + return $this->diff->render($renderer); + } + + protected function requireVendorLib($file) + { + require_once dirname(dirname(__DIR__)) . '/vendor/php-diff/lib/' . $file; + } + + public static function create($a, $b) + { + $diff = new static($a, $b); + return $diff; + } +} diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php new file mode 100644 index 0000000..17fc8a5 --- /dev/null +++ b/library/Businessprocess/Storage/LegacyConfigParser.php @@ -0,0 +1,409 @@ +<?php + +namespace Icinga\Module\Businessprocess\Storage; + +use Icinga\Application\Benchmark; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\SystemPermissionException; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Metadata; + +class LegacyConfigParser +{ + /** @var int */ + protected $currentLineNumber; + + /** @var string */ + protected $currentFilename; + + protected $name; + + /** @var BpConfig */ + protected $config; + + /** @var array */ + protected $missingNodes = []; + + /** + * LegacyConfigParser constructor + * + * @param $name + */ + private function __construct($name) + { + $this->name = $name; + $this->config = new BpConfig(); + $this->config->setName($name); + } + + /** + * @return BpConfig + */ + public function getParsedConfig() + { + return $this->config; + } + + /** + * @param $name + * @param $filename + * + * @return BpConfig + */ + public static function parseFile($name, $filename) + { + Benchmark::measure('Loading business process ' . $name); + $parser = new static($name); + $parser->reallyParseFile($filename); + Benchmark::measure('Business process ' . $name . ' loaded'); + return $parser->getParsedConfig(); + } + + /** + * @param $name + * @param $string + * + * @return BpConfig + */ + public static function parseString($name, $string) + { + Benchmark::measure('Loading BP config from file: ' . $name); + $parser = new static($name); + + $config = $parser->getParsedConfig(); + $config->setMetadata( + static::readMetadataFromString($name, $string) + ); + + foreach (preg_split('/\r?\n/', $string) as $line) { + $parser->parseLine($line); + } + + $parser->resolveMissingNodes(); + + Benchmark::measure('Business process ' . $name . ' loaded'); + return $config; + } + + protected function reallyParseFile($filename) + { + $file = $this->currentFilename = $filename; + $fh = @fopen($file, 'r'); + if (! $fh) { + throw new SystemPermissionException('Could not open "%s"', $filename); + } + + $config = $this->config; + $config->setMetadata( + $this::readMetadataFromFileHeader($config->getName(), $filename) + ); + + $this->currentLineNumber = 0; + while ($line = fgets($fh)) { + $this->parseLine($line); + } + + $this->resolveMissingNodes(); + + fclose($fh); + unset($this->currentLineNumber); + unset($this->currentFilename); + } + + /** + * Resolve previously missed business process nodes + * + * @throws ConfigurationError In case a referenced process does not exist + */ + protected function resolveMissingNodes() + { + foreach ($this->missingNodes as $name => $parents) { + foreach ($parents as $parent) { + /** @var BpNode $parent */ + $parent->addChild($this->config->getNode($name)); + } + } + } + + public static function readMetadataFromFileHeader($name, $filename) + { + $metadata = new Metadata($name); + $fh = fopen($filename, 'r'); + $cnt = 0; + while ($cnt < 15 && false !== ($line = fgets($fh))) { + $cnt++; + static::parseHeaderLine($line, $metadata); + } + + fclose($fh); + return $metadata; + } + + public static function readMetadataFromString($name, &$string) + { + $metadata = new Metadata($name); + + $lines = preg_split('/\r?\n/', substr($string, 0, 8092)); + foreach ($lines as $line) { + static::parseHeaderLine($line, $metadata); + } + + return $metadata; + } + + protected function splitCommaSeparated($string) + { + return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); + } + + protected function readHeaderString($string, Metadata $metadata) + { + foreach (preg_split('/\r?\n/', $string) as $line) { + $this->parseHeaderLine($line, $metadata); + } + + return $metadata; + } + + /** + * @return array + */ + protected function emptyHeader() + { + return array( + 'Title' => null, + 'Description' => null, + 'Owner' => null, + 'AllowedUsers' => null, + 'AllowedGroups' => null, + 'AllowedRoles' => null, + 'Backend' => null, + 'Statetype' => 'soft', + 'SLAHosts' => null + ); + } + + /** + * @param $line + * @param Metadata $metadata + */ + protected static function parseHeaderLine($line, Metadata $metadata) + { + if (preg_match('/^\s*#\s+(.+?)\s*:\s*(.+)$/', trim($line), $m)) { + if ($metadata->hasKey($m[1])) { + $metadata->set($m[1], $m[2]); + } + } + } + + /** + * @param $line + * @param BpConfig $bp + */ + protected function parseDisplay(&$line, BpConfig $bp) + { + list($display, $name, $desc) = preg_split('~\s*;\s*~', substr($line, 8), 3); + $bp->getBpNode($name)->setAlias($desc)->setDisplay($display); + if ($display > 0) { + $bp->addRootNode($name); + } + } + + /** + * @param $line + * @param BpConfig $bp + */ + protected function parseExternalInfo(&$line, BpConfig $bp) + { + list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); + $bp->getBpNode($name)->setInfoCommand($script); + } + + protected function parseExtraInfo(&$line, BpConfig $bp) + { + // TODO: Not yet + // list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); + // $this->getNode($name)->setExtraInfo($script); + } + + protected function parseInfoUrl(&$line, BpConfig $bp) + { + list($name, $url) = preg_split('~\s*;\s*~', substr($line, 9), 2); + $bp->getBpNode($name)->setInfoUrl($url); + } + + protected function parseStateOverrides(&$line, BpConfig $bp) + { + // state_overrides <bp-node>!<child>|n-n[,n-n]!<child>|n-n[,n-n] + $segments = preg_split('~\s*!\s*~', substr($line, 16)); + $node = $bp->getNode(array_shift($segments)); + foreach ($segments as $overrideDef) { + list($childName, $overrides) = preg_split('~\s*\|\s*~', $overrideDef, 2); + + $stateOverrides = []; + foreach (preg_split('~\s*,\s*~', $overrides) as $override) { + list($from, $to) = preg_split('~\s*-\s*~', $override, 2); + $stateOverrides[(int) $from] = (int) $to; + } + + $node->setStateOverrides($stateOverrides, $childName); + } + } + + protected function parseExtraLine(&$line, $typeLength, BpConfig $bp) + { + $type = substr($line, 0, $typeLength); + if (substr($type, 0, 7) === 'display') { + $this->parseDisplay($line, $bp); + return true; + } + + switch ($type) { + case 'external_info': + $this->parseExternalInfo($line, $bp); + break; + case 'extra_info': + $this->parseExtraInfo($line, $bp); + break; + case 'info_url': + $this->parseInfoUrl($line, $bp); + break; + case 'state_overrides': + $this->parseStateOverrides($line, $bp); + break; + case 'template': + // compat, ignoring for now + break; + default: + return false; + } + + return true; + } + + /** + * Parses a single line + * + * Adds eventual new knowledge to the given Business Process config + * + * @param $line + * + * @throws ConfigurationError + */ + protected function parseLine(&$line) + { + $bp = $this->config; + $line = trim($line); + + $this->currentLineNumber++; + + // Skip empty or comment-only lines + if (empty($line) || $line[0] === '#') { + return; + } + + // Space found in the first 16 cols? Might be a line with extra information + $pos = strpos($line, ' '); + if ($pos !== false && $pos < 16) { + if ($this->parseExtraLine($line, $pos, $bp)) { + return; + } + } + + if (strpos($line, '=') === false) { + $this->parseError('Got invalid line'); + } + + list($name, $value) = preg_split('~\s*=\s*~', $line, 2); + + if (strpos($name, ';') !== false) { + $this->parseError('No semicolon allowed in varname'); + } + + $op = '&'; + if (preg_match_all('~(?<!\\\\)([\|\+&\!\%])~', $value, $m)) { + $op = implode('', $m[1]); + for ($i = 1; $i < strlen($op); $i++) { + if ($op[$i] !== $op[$i - 1]) { + $this->parseError('Mixing operators is not allowed'); + } + } + } + $op = $op[0]; + $op_name = $op; + + if ($op === '+') { + if (! preg_match('~^(\d+)(?::(\d+))?\s*of:\s*(.+?)$~', $value, $m)) { + $this->parseError('syntax: <var> = <num> of: <var1> + <var2> [+ <varn>]*'); + } + $op_name = $m[1]; + // New feature: $minWarn = $m[2]; + $value = $m[3]; + } + + $node = new BpNode((object) array( + 'name' => $name, + 'operator' => $op_name, + 'child_names' => [] + )); + $node->setBpConfig($bp); + + $cmps = preg_split('~\s*(?<!\\\\)\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY); + foreach ($cmps as $val) { + $val = preg_replace('~(\\\\([\|\+&\!\%]))~', '$2', $val); + if (strpos($val, ';') !== false) { + if ($bp->hasNode($val)) { + $node->addChild($bp->getNode($val)); + } else { + list($host, $service) = preg_split('~;~', $val, 2); + if ($service === 'Hoststatus') { + $node->addChild($bp->createHost($host)); + } else { + $node->addChild($bp->createService($host, $service)); + } + } + } elseif ($val[0] === '@') { + if (strpos($val, ':') === false) { + throw new ConfigurationError( + "I'm unable to import full external configs, a node needs to be provided for '%s'", + $val + ); + } else { + list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2); + $node->addChild($bp->createImportedNode($config, $nodeName)); + } + } elseif ($bp->hasNode($val)) { + $node->addChild($bp->getNode($val)); + } else { + $this->missingNodes[$val][] = $node; + } + } + + $bp->addNode($name, $node); + } + + /** + * @return string + */ + public function getFilename() + { + return $this->currentFilename ?: '[given string]'; + } + + /** + * @param $msg + * @throws ConfigurationError + */ + protected function parseError($msg) + { + throw new ConfigurationError( + sprintf( + 'Parse error on %s:%s: %s', + $this->getFilename(), + $this->currentLineNumber, + $msg + ) + ); + } +} diff --git a/library/Businessprocess/Storage/LegacyConfigRenderer.php b/library/Businessprocess/Storage/LegacyConfigRenderer.php new file mode 100644 index 0000000..7e2e0b2 --- /dev/null +++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php @@ -0,0 +1,255 @@ +<?php + +namespace Icinga\Module\Businessprocess\Storage; + +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\ImportedNode; + +class LegacyConfigRenderer +{ + /** @var array */ + protected $renderedNodes; + + /** + * LecagyConfigRenderer constructor + * + * @param BpConfig $config + */ + public function __construct(BpConfig $config) + { + $this->config = $config; + } + + /** + * @return string + */ + public function render() + { + return $this->renderHeader() . $this->renderNodes(); + } + + /** + * @param BpConfig $config + * @return mixed + */ + public static function renderConfig(BpConfig $config) + { + $renderer = new static($config); + return $renderer->render(); + } + + /** + * @return string + */ + public function renderHeader() + { + $str = "### Business Process Config File ###\n#\n"; + + $meta = $this->config->getMetadata(); + foreach ($meta->getProperties() as $key => $value) { + if ($value === null) { + continue; + } + + $str .= sprintf("# %-15s : %s\n", $key, $value); + } + + $str .= "#\n###################################\n\n"; + + return $str; + } + + /** + * @return string + */ + public function renderNodes() + { + $this->renderedNodes = array(); + + $config = $this->config; + $str = ''; + + foreach ($config->getRootNodes() as $node) { + $str .= $this->requireRenderedBpNode($node); + } + + foreach ($config->getUnboundNodes() as $name => $node) { + $str .= $this->requireRenderedBpNode($node); + } + + return $str . "\n"; + } + + /** + * Rendered node definition, empty string if already rendered + * + * @param BpNode $node + * + * @return string + */ + protected function requireRenderedBpNode(BpNode $node) + { + $name = $node->getName(); + + if (array_key_exists($name, $this->renderedNodes)) { + return ''; + } else { + $this->renderedNodes[$name] = true; + return $this->renderBpNode($node); + } + } + + /** + * @param BpNode $node + * @return string + */ + protected function renderBpNode(BpNode $node) + { + $name = $node->getName(); + // Doing this before rendering children allows us to store loops + $cfg = ''; + + foreach ($node->getChildBpNodes() as $name => $child) { + if ($child instanceof ImportedNode) { + continue; + } + + $cfg .= $this->requireRenderedBpNode($child) . "\n"; + } + + $cfg .= static::renderSingleBpNode($node); + + return $cfg; + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderEqualSign(BpNode $node) + { + $op = $node->getOperator(); + if (is_numeric($op)) { + return '= ' . $op . ' of:'; + } else { + return '='; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderOperator(BpNode $node) + { + $op = $node->getOperator(); + if (is_numeric($op)) { + return '+'; + } else { + return $op; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderSingleBpNode(BpNode $node) + { + return static::renderExpression($node) + . static::renderStateOverrides($node) + . static::renderDisplay($node) + . static::renderInfoUrl($node); + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderExpression(BpNode $node) + { + return sprintf( + "%s %s %s\n", + $node->getName(), + static::renderEqualSign($node), + static::renderChildNames($node) + ); + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderChildNames(BpNode $node) + { + $op = static::renderOperator($node); + $children = $node->getChildNames(); + $str = implode(' ' . $op . ' ', array_map(function ($val) { + return preg_replace('~([\|\+&\!\%])~', '\\\\$1', $val); + }, $children)); + + if ((count($children) < 2) && $op !== '&') { + return $op . ' ' . $str; + } else { + return $str; + } + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderDisplay(BpNode $node) + { + if ($node->hasAlias() || $node->getDisplay() > 0) { + $prio = $node->getDisplay(); + return sprintf( + "display %s;%s;%s\n", + $prio, + $node->getName(), + $node->getAlias() + ); + } else { + return ''; + } + } + + public static function renderStateOverrides(BpNode $node) + { + $stateOverrides = ''; + foreach ($node->getStateOverrides() as $childName => $overrideRules) { + $overrides = []; + foreach ($overrideRules as $from => $to) { + $overrides[] = sprintf('%d-%d', $from, $to); + } + + if (! empty($overrides)) { + $stateOverrides .= '!' . $childName . '|' . join(',', $overrides); + } + } + + if (! $stateOverrides) { + return ''; + } + + return 'state_overrides ' . $node->getName() . $stateOverrides . "\n"; + } + + /** + * @param BpNode $node + * @return string + */ + public static function renderInfoUrl(BpNode $node) + { + if ($node->hasInfoUrl()) { + return sprintf( + "info_url %s;%s\n", + $node->getName(), + $node->getInfoUrl() + ); + } else { + return ''; + } + } +} diff --git a/library/Businessprocess/Storage/LegacyStorage.php b/library/Businessprocess/Storage/LegacyStorage.php new file mode 100644 index 0000000..6582ebd --- /dev/null +++ b/library/Businessprocess/Storage/LegacyStorage.php @@ -0,0 +1,207 @@ +<?php + +namespace Icinga\Module\Businessprocess\Storage; + +use DirectoryIterator; +use Icinga\Application\Hook\AuditHook; +use Icinga\Application\Icinga; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Exception\SystemPermissionException; + +class LegacyStorage extends Storage +{ + /** + * All parsed configurations + * + * @var BpConfig[] + */ + protected $configs = []; + + /** @var string */ + protected $configDir; + + public function getConfigDir() + { + if ($this->configDir === null) { + $this->prepareDefaultConfigDir(); + } + + return $this->configDir; + } + + protected function prepareDefaultConfigDir() + { + $dir = Icinga::app() + ->getModuleManager() + ->getModule('businessprocess') + ->getConfigDir(); + + // TODO: This is silly. We need Config::requireDirectory(). + if (! is_dir($dir)) { + if (! is_dir(dirname($dir))) { + if (! @mkdir(dirname($dir))) { + throw new SystemPermissionException('Could not create config directory "%s"', dirname($dir)); + } + } + if (! mkdir($dir)) { + throw new SystemPermissionException('Could not create config directory "%s"', $dir); + } + } + $dir = $dir . '/processes'; + if (! is_dir($dir)) { + if (! mkdir($dir)) { + throw new SystemPermissionException('Could not create config directory "%s"', $dir); + } + } + + $this->configDir = $dir; + } + + /** + * @inheritdoc + */ + public function listProcesses() + { + $files = array(); + + foreach ($this->listAllProcessNames() as $name) { + $meta = $this->loadMetadata($name); + if (! $meta->canRead()) { + continue; + } + + $files[$name] = $meta->getExtendedTitle(); + } + + natcasesort($files); + return $files; + } + + /** + * @inheritdoc + */ + public function listProcessNames() + { + $files = array(); + + foreach ($this->listAllProcessNames() as $name) { + $meta = $this->loadMetadata($name); + if (! $meta->canRead()) { + continue; + } + + $files[$name] = $name; + } + + natcasesort($files); + return $files; + } + + /** + * @inheritdoc + */ + public function listAllProcessNames() + { + $files = array(); + + foreach (new DirectoryIterator($this->getConfigDir()) as $file) { + if ($file->isDot()) { + continue; + } + + $filename = $file->getFilename(); + if (substr($filename, -5) === '.conf') { + $files[] = substr($filename, 0, -5); + } + } + + natcasesort($files); + return $files; + } + + /** + * @inheritdoc + */ + public function loadProcess($name) + { + if (! isset($this->configs[$name])) { + $this->configs[$name] = LegacyConfigParser::parseFile( + $name, + $this->getFilename($name) + ); + } + + return $this->configs[$name]; + } + + /** + * @inheritdoc + */ + public function storeProcess(BpConfig $process) + { + AuditHook::logActivity('businessprocess/store', "Business Process \"{$process->getName()}\" stored"); + file_put_contents( + $this->getFilename($process->getName()), + LegacyConfigRenderer::renderConfig($process) + ); + } + + /** + * @inheritdoc + */ + public function deleteProcess($name) + { + AuditHook::logActivity('businessprocess/delete', "Business Process \"{$name}\" deleted"); + return @unlink($this->getFilename($name)); + } + + /** + * @inheritdoc + */ + public function loadMetadata($name) + { + if (isset($this->configs[$name])) { + return $this->configs[$name]->getMetadata(); + } + + return LegacyConfigParser::readMetadataFromFileHeader( + $name, + $this->getFilename($name) + ); + } + + public function getSource($name) + { + return file_get_contents($this->getFilename($name)); + } + + public function getFilename($name) + { + return $this->getConfigDir() . '/' . $name . '.conf'; + } + + /** + * @param $name + * @param $string + * + * @return BpConfig + */ + public function loadFromString($name, $string) + { + return LegacyConfigParser::parseString($name, $string); + } + + /** + * @param $name + * @return bool + */ + public function hasProcess($name) + { + $file = $this->getFilename($name); + if (! is_file($file)) { + return false; + } + + return $this->loadMetadata($name)->canRead(); + } +} diff --git a/library/Businessprocess/Storage/Storage.php b/library/Businessprocess/Storage/Storage.php new file mode 100644 index 0000000..c8a07ba --- /dev/null +++ b/library/Businessprocess/Storage/Storage.php @@ -0,0 +1,107 @@ +<?php + +namespace Icinga\Module\Businessprocess\Storage; + +use Icinga\Application\Config; +use Icinga\Data\ConfigObject; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Metadata; + +abstract class Storage +{ + /** + * @var static + */ + protected static $instance; + + /** + * @var ConfigObject + */ + protected $config; + + /** + * Storage constructor. + * @param ConfigObject $config + */ + public function __construct(ConfigObject $config) + { + $this->config = $config; + $this->init(); + } + + protected function init() + { + } + + public static function getInstance() + { + if (static::$instance === null) { + static::$instance = new static(Config::module('businessprocess')->getSection('global')); + } + + return static::$instance; + } + + /** + * All processes readable by the current user + * + * The returned array has the form <process name> => <nice title>, sorted + * by title + * + * @return array + */ + abstract public function listProcesses(); + + /** + * All process names readable by the current user + * + * The returned array has the form <process name> => <process name> and is + * sorted + * + * @return array + */ + abstract public function listProcessNames(); + + /** + * All available process names, regardless of eventual restrictions + * + * @return array + */ + abstract public function listAllProcessNames(); + + /** + * Whether a configuration with the given name exists + * + * @param $name + * + * @return bool + */ + abstract public function hasProcess($name); + + /** + * @param $name + * @return BpConfig + */ + abstract public function loadProcess($name); + + /** + * Store eventual changes applied to the given configuration + * + * @param BpConfig $config + * + * @return mixed + */ + abstract public function storeProcess(BpConfig $config); + + /** + * @param $name + * @return bool Whether the process has been deleted + */ + abstract public function deleteProcess($name); + + /** + * @param string $name + * @return Metadata + */ + abstract public function loadMetadata($name); +} |