icingaweb2-module-businessp.../library/Businessprocess/Storage/LegacyConfigParser.php
Johannes Meyer 49ebbc4cdb Apply state overrides on demand instead of directly
Internally non-process children are only instantiated once.
This means when applying state overrides directly they're
used everywhere and do not differ between the containing
process. State overrides are now applied explicitly and
on demand, decoupling them from children.
2020-07-01 08:54:34 +02:00

409 lines
11 KiB
PHP

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