diff options
Diffstat (limited to 'library/Businessprocess/BpConfig.php')
-rw-r--r-- | library/Businessprocess/BpConfig.php | 1117 |
1 files changed, 1117 insertions, 0 deletions
diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php new file mode 100644 index 0000000..c9e70fd --- /dev/null +++ b/library/Businessprocess/BpConfig.php @@ -0,0 +1,1117 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Exception; +use Icinga\Application\Modules\Module; +use Icinga\Exception\IcingaException; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Businessprocess\Exception\NestingError; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; +use ipl\Sql\Connection as IcingaDbConnection; + +class BpConfig +{ + const SOFT_STATE = 0; + + const HARD_STATE = 1; + + /** + * Name of the configured monitoring backend + * + * @var string + */ + protected $backendName; + + /** + * Backend to retrieve states from + * + * @var MonitoringBackend|IcingaDbConnection + */ + protected $backend; + + /** + * @var LegacyStorage + */ + protected $storage; + + /** @var Metadata */ + protected $metadata; + + /** + * Business process name + * + * @var string + */ + protected $name; + + /** + * Business process title + * + * @var string + */ + protected $title; + + /** + * State type, soft or hard + * + * @var int + */ + protected $state_type; + + /** + * Warnings, usually filled at process build time + * + * @var array + */ + protected $warnings = array(); + + /** + * Errors, usually filled at process build time + * + * @var array + */ + protected $errors = array(); + + /** + * All used node objects + * + * @var array + */ + protected $nodes = array(); + + /** + * Root node objects + * + * @var array + */ + protected $root_nodes = array(); + + /** + * Imported nodes + * + * @var ImportedNode[] + */ + protected $importedNodes = []; + + /** + * Imported configs + * + * @var BpConfig[] + */ + protected $importedConfigs = []; + + /** + * All host names { 'hostA' => true, ... } + * + * @var array + */ + protected $hosts = array(); + + /** @var bool Whether catchable errors should be thrown nonetheless */ + protected $throwErrors = false; + + protected $loopDetection = array(); + + /** + * Applied state simulation + * + * @var Simulation + */ + protected $simulation; + + protected $changeCount = 0; + + protected $simulationCount = 0; + + /** @var ProcessChanges */ + protected $appliedChanges; + + /** @var bool Whether the config is faulty */ + protected $isFaulty = false; + + public function __construct() + { + } + + /** + * Retrieve metadata for this configuration + * + * @return Metadata + */ + public function getMetadata() + { + if ($this->metadata === null) { + $this->metadata = new Metadata($this->name); + } + + return $this->metadata; + } + + /** + * Set metadata + * + * @param Metadata $metadata + * + * @return $this + */ + public function setMetadata(Metadata $metadata) + { + $this->metadata = $metadata; + return $this; + } + + /** + * Apply pending process changes + * + * @param ProcessChanges $changes + * + * @return $this + */ + public function applyChanges(ProcessChanges $changes) + { + $cnt = 0; + foreach ($changes->getChanges() as $change) { + $cnt++; + $change->applyTo($this); + } + $this->changeCount = $cnt; + + $this->appliedChanges = $changes; + + return $this; + } + + /** + * Apply a state simulation + * + * @param Simulation $simulation + * + * @return $this + */ + public function applySimulation(Simulation $simulation) + { + $cnt = 0; + + foreach ($simulation->simulations() as $node => $s) { + if (! $this->hasNode($node)) { + continue; + } + $cnt++; + $this->getNode($node) + ->setState($s->state) + ->setAck($s->acknowledged) + ->setDowntime($s->in_downtime) + ->setMissing(false); + } + + $this->simulationCount = $cnt; + + return $this; + } + + /** + * Number of applied changes + * + * @return int + */ + public function countChanges() + { + return $this->changeCount; + } + + /** + * Whether changes have been applied to this configuration + * + * @return bool + */ + public function hasChanges() + { + return $this->countChanges() > 0; + } + + /** + * @param $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getHtmlId() + { + return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName()); + } + + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + public function getTitle() + { + return $this->getMetadata()->getTitle(); + } + + public function hasTitle() + { + return $this->getMetadata()->has('Title'); + } + + public function getBackendName() + { + return $this->getMetadata()->get('Backend'); + } + + public function hasBackendName() + { + return $this->getMetadata()->has('Backend'); + } + + public function setBackend($backend) + { + $this->backend = $backend; + return $this; + } + + public function getBackend() + { + if ($this->backend === null) { + if (Module::exists('icingadb') + && (! $this->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())) { + $this->backend = IcingaDbObject::fetchDb(); + } else { + $this->backend = MonitoringBackend::instance( + $this->getBackendName() + ); + } + } + + return $this->backend; + } + + public function isReferenced() + { + foreach ($this->storage()->listProcessNames() as $bpName) { + if ($bpName !== $this->getName()) { + $bp = $this->storage()->loadProcess($bpName); + foreach ($bp->getImportedNodes() as $importedNode) { + if ($importedNode->getConfigName() === $this->getName()) { + return true; + } + } + } + } + + return false; + } + + public function hasBackend() + { + return $this->backend !== null; + } + + public function hasBeenChanged() + { + return false; + } + + public function hasSimulations() + { + return $this->countSimulations() > 0; + } + + public function countSimulations() + { + return $this->simulationCount; + } + + public function clearAppliedChanges() + { + if ($this->appliedChanges !== null) { + $this->appliedChanges->clear(); + } + return $this; + } + + public function getStateType() + { + if ($this->state_type === null) { + if ($this->getMetadata()->has('Statetype')) { + switch ($this->getMetadata()->get('Statetype')) { + case 'hard': + $this->state_type = self::HARD_STATE; + break; + case 'soft': + $this->state_type = self::SOFT_STATE; + break; + } + } else { + $this->state_type = self::HARD_STATE; + } + } + + return $this->state_type; + } + + public function useSoftStates() + { + $this->state_type = self::SOFT_STATE; + return $this; + } + + public function useHardStates() + { + $this->state_type = self::HARD_STATE; + return $this; + } + + public function usesSoftStates() + { + return $this->getStateType() === self::SOFT_STATE; + } + + public function usesHardStates() + { + return $this->getStateType() === self::HARD_STATE; + } + + public function addRootNode($name) + { + $this->root_nodes[$name] = $this->getNode($name); + return $this; + } + + public function removeRootNode($name) + { + if ($this->isRootNode($name)) { + unset($this->root_nodes[$name]); + } + + return $this; + } + + public function isRootNode($name) + { + return array_key_exists($name, $this->root_nodes); + } + + /** + * @return BpNode[] + */ + public function getChildren() + { + return $this->getRootNodes(); + } + + /** + * @return int + */ + public function countChildren() + { + return count($this->root_nodes); + } + + /** + * @return BpNode[] + */ + public function getRootNodes() + { + return $this->root_nodes; + } + + public function listRootNodes() + { + return array_keys($this->root_nodes); + } + + public function getNodes() + { + return $this->nodes; + } + + public function hasNode($name) + { + if (array_key_exists($name, $this->nodes)) { + return true; + } elseif ($name[0] === '@') { + list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2); + return $this->getImportedConfig($configName)->hasNode($nodeName); + } + + return false; + } + + public function hasRootNode($name) + { + return array_key_exists($name, $this->root_nodes); + } + + public function createService($host, $service) + { + $node = new ServiceNode( + (object) array( + 'hostname' => $host, + 'service' => $service + ) + ); + $node->setBpConfig($this); + $this->nodes[$node->getName()] = $node; + $this->hosts[$host] = true; + return $node; + } + + public function createHost($host) + { + $node = new HostNode((object) array('hostname' => $host)); + $node->setBpConfig($this); + $this->nodes[$node->getName()] = $node; + $this->hosts[$host] = true; + return $node; + } + + public function calculateAllStates() + { + foreach ($this->getRootNodes() as $node) { + $node->getState(); + } + + return $this; + } + + public function clearAllStates() + { + foreach ($this->getBpNodes() as $node) { + $node->clearState(); + } + + return $this; + } + + public function listInvolvedHostNames(&$usedConfigs = null) + { + $hosts = $this->hosts; + if (! empty($this->importedNodes)) { + $usedConfigs[$this->getName()] = true; + foreach ($this->importedNodes as $node) { + if (isset($usedConfigs[$node->getConfigName()])) { + continue; + } + + $hosts += array_flip($node->getBpConfig()->listInvolvedHostNames($usedConfigs)); + } + } + + return array_keys($hosts); + } + + /** + * Create and attach a new process (BpNode) + * + * @param string $name Process name + * @param string $operator Operator (defaults to &) + * + * @return BpNode + */ + public function createBp($name, $operator = '&') + { + $node = new BpNode((object) array( + 'name' => $name, + 'operator' => $operator, + 'child_names' => array(), + )); + $node->setBpConfig($this); + + $this->addNode($name, $node); + return $node; + } + + public function createMissingBp($name) + { + return $this->createBp($name)->setMissing(); + } + + public function getMissingChildren() + { + $missing = array(); + foreach ($this->getRootNodes() as $root) { + $missing += $root->getMissingChildren(); + } + + return $missing; + } + + public function createImportedNode($config, $name = null) + { + $params = (object) array('configName' => $config); + if ($name !== null) { + $params->node = $name; + } + + $node = new ImportedNode($this, $params); + $this->importedNodes[$node->getName()] = $node; + $this->nodes[$node->getName()] = $node; + return $node; + } + + public function getImportedNodes() + { + return $this->importedNodes; + } + + public function getImportedConfig($name) + { + if (! isset($this->importedConfigs[$name])) { + try { + $import = $this->storage()->loadProcess($name); + } catch (Exception $e) { + $import = (new static()) + ->setName($name) + ->setFaulty(); + } + + if ($this->usesSoftStates()) { + $import->useSoftStates(); + } else { + $import->useHardStates(); + } + + $this->importedConfigs[$name] = $import; + } + + return $this->importedConfigs[$name]; + } + + public function listInvolvedConfigs(&$configs = null) + { + if ($configs === null) { + $configs[$this->getName()] = $this; + } + + foreach ($this->importedNodes as $node) { + if (! isset($configs[$node->getConfigName()])) { + $config = $node->getBpConfig(); + $configs[$node->getConfigName()] = $config; + $config->listInvolvedConfigs($configs); + } + } + + return $configs; + } + + /** + * @return LegacyStorage + */ + protected function storage() + { + if ($this->storage === null) { + $this->storage = LegacyStorage::getInstance(); + } + + return $this->storage; + } + + /** + * @param string $name + * @return MonitoredNode|BpNode + * @throws Exception + */ + public function getNode($name) + { + if ($name === '__unbound__') { + return $this->getUnboundBaseNode(); + } + + if (array_key_exists($name, $this->nodes)) { + return $this->nodes[$name]; + } + + if ($name[0] === '@') { + list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2); + return $this->getImportedConfig($configName)->getNode($nodeName); + } + + // Fallback: if it is a service, create an empty one: + $this->warn(sprintf('The node "%s" doesn\'t exist', $name)); + + [$name, $suffix] = self::splitNodeName($name); + if ($suffix !== null) { + if ($suffix === 'Hoststatus') { + return $this->createHost($name); + } else { + return $this->createService($name, $suffix); + } + } + + throw new Exception( + sprintf('The node "%s" doesn\'t exist', $name) + ); + } + + /** + * @return BpNode + */ + public function getUnboundBaseNode() + { + // Hint: state is useless here, but triggers parent/child "calculation" + // This is an ugly workaround and should be made obsolete + $this->calculateAllStates(); + + $names = array_keys($this->getUnboundNodes()); + $bp = new BpNode((object) array( + 'name' => '__unbound__', + 'operator' => '&', + 'child_names' => $names + )); + $bp->setBpConfig($this); + $bp->setAlias($this->translate('Unbound nodes')); + return $bp; + } + + /** + * @param $name + * @return BpNode + * + * @throws NotFoundError + */ + public function getBpNode($name) + { + if ($this->hasBpNode($name)) { + return $this->nodes[$name]; + } + + $msg = $this->isFaulty() + ? sprintf( + t('Trying to import node "%s" from faulty config file "%s.conf"'), + self::unescapeName($name), + $this->getName() + ) + : sprintf(t('Trying to access a missing business process node "%s"'), $name); + + throw new NotFoundError($msg); + } + + /** + * @param $name + * + * @return bool + */ + public function hasBpNode($name) + { + return array_key_exists($name, $this->nodes) + && $this->nodes[$name] instanceof BpNode; + } + + /** + * Set the state for a specific node + * + * @param string $name Node name + * @param int $state Desired state + * + * @return $this + */ + public function setNodeState($name, $state) + { + $this->getNode($name)->setState($state); + return $this; + } + + /** + * Add the given node to the given BpNode + * + * @param $name + * @param BpNode $node + * + * @return $this + */ + public function addNode($name, BpNode $node) + { + if (array_key_exists($name, $this->nodes)) { + $this->warn( + sprintf( + mt('businessprocess', 'Node "%s" has been defined twice'), + $name + ) + ); + } + + $this->nodes[$name] = $node; + + if ($node->getDisplay() > 0) { + if (! $this->isRootNode($name)) { + $this->addRootNode($name); + } + } else { + if ($this->isRootNode($name)) { + $this->removeRootNode($name); + } + } + + + return $this; + } + + /** + * Remove all occurrences of a specific node by name + * + * @param $name + */ + public function removeNode($name) + { + unset($this->nodes[$name]); + if (array_key_exists($name, $this->root_nodes)) { + unset($this->root_nodes[$name]); + } + + foreach ($this->getBpNodes() as $node) { + if ($node->hasChild($name)) { + $node->removeChild($name); + } + } + } + + /** + * Get all business process nodes + * + * @return BpNode[] + */ + public function getBpNodes() + { + $nodes = array(); + + foreach ($this->nodes as $node) { + if ($node instanceof BpNode) { + $nodes[$node->getName()] = $node; + } + } + + return $nodes; + } + + /** + * List all business process node names + * + * @return array + */ + public function listBpNodes() + { + $nodes = array(); + + foreach ($this->getBpNodes() as $name => $node) { + $alias = $node->getAlias(); + $nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node); + } + + return $nodes; + } + + /** + * All business process nodes defined in this config but not + * assigned to any parent + * + * @return BpNode[] + */ + public function getUnboundNodes() + { + $nodes = array(); + + foreach ($this->getBpNodes() as $name => $node) { + if ($node->hasParents()) { + continue; + } + + if ($node->getDisplay() === 0) { + $nodes[$name] = $node; + } + } + + return $nodes; + } + + /** + * @return bool + */ + public function hasWarnings() + { + return ! empty($this->warnings); + } + + /** + * @return array + */ + public function getWarnings() + { + return $this->warnings; + } + + /** + * @return bool + */ + public function hasErrors() + { + return ! empty($this->errors) || $this->isEmpty(); + } + + /** + * @return array + */ + public function getErrors() + { + $errors = $this->errors; + if ($this->isEmpty()) { + $errors[] = sprintf( + $this->translate( + 'No business process nodes for "%s" have been defined yet' + ), + $this->getTitle() + ); + } + return $errors; + } + + /** + * Translation helper + * + * @param $msg + * + * @return mixed|string + */ + public function translate($msg) + { + return mt('businessprocess', $msg); + } + + /** + * Add a message to our warning stack + * + * @param $msg + */ + protected function warn($msg) + { + $args = func_get_args(); + array_shift($args); + $this->warnings[] = vsprintf($msg, $args); + } + + /** + * @param string $msg,... + * + * @return $this + * + * @throws IcingaException + */ + public function addError($msg) + { + $args = func_get_args(); + array_shift($args); + if (! empty($args)) { + $msg = vsprintf($msg, $args); + } + if ($this->throwErrors) { + throw new IcingaException($msg); + } + + if (! in_array($msg, $this->errors)) { + $this->errors[] = $msg; + } + + return $this; + } + + /** + * Decide whether errors should be thrown or collected + * + * @param bool $throw + * + * @return $this + */ + public function throwErrors($throw = true) + { + $this->throwErrors = $throw; + return $this; + } + + /** + * Begin loop detection for the given name + * + * Will throw a NestingError in case this node will be met again below itself + * + * @param $name + * + * @throws NestingError + */ + public function beginLoopDetection($name) + { + // echo "Begin loop $name\n"; + if (array_key_exists($name, $this->loopDetection)) { + $loop = array_keys($this->loopDetection); + $loop[] = $name; + $this->loopDetection = array(); + throw new NestingError('Loop detected: %s', implode(' -> ', $loop)); + } + + $this->loopDetection[$name] = true; + } + + /** + * Remove the given name from the loop detection stack + * + * @param $name + */ + public function endLoopDetection($name) + { + // echo "End loop $this->name\n"; + unset($this->loopDetection[$name]); + } + + /** + * Whether this configuration has any Nodes + * + * @return bool + */ + public function isEmpty() + { + // This is faster + if (! empty($this->root_nodes)) { + return false; + } + + return count($this->listBpNodes()) === 0; + } + + /** + * Export the config to array + * + * @param bool $flat If false, children will be added to the array key children, else the array will be flat + * + * @return array + */ + public function toArray($flat = false) + { + $data = [ + 'name' => $this->getTitle(), + 'path' => $this->getTitle() + ]; + + $children = []; + + foreach ($this->getChildren() as $node) { + if ($flat) { + $children = array_merge($children, $node->toArray($data, $flat)); + } else { + $children[] = $node->toArray($data, $flat); + } + } + + if ($flat) { + $data = [$data]; + + if (! empty($children)) { + $data = array_merge($data, $children); + } + } else { + $data['children'] = $children; + } + + return $data; + } + + /** + * Escape the given node name + * + * @param string $name + * + * @return string + */ + public static function escapeName(string $name): string + { + return preg_replace('/((?<!\\\\);)/', '\\\\$1', $name); + } + + /** + * Unescape the given node name + * + * @param string $name + * + * @return string + */ + public static function unescapeName(string $name): string + { + return str_replace('\\;', ';', $name); + } + + /** + * Join the given two name parts together + * + * The used separator is the semicolon. If a semicolon exists in either part, it's escaped. + * + * @param string $name + * @param ?string $suffix + * + * @return string + */ + public static function joinNodeName(string $name, ?string $suffix = null): string + { + return self::escapeName($name) . ($suffix ? ";$suffix" : ''); + } + + /** + * Split the given node name into two parts + * + * The first part is always a string, with any semicolons unescaped. + * The second part may be null or a string otherwise. + * + * @param string $nodeName + * + * @return array + */ + public static function splitNodeName(string $nodeName): array + { + $parts = preg_split('/(?<!\\\\);/', $nodeName, 2); + $parts[0] = self::unescapeName($parts[0]); + + return array_pad($parts, 2, null); + } + + /** + * Set whether the config is faulty + * + * @param bool $isFaulty + * + * @return $this + */ + public function setFaulty(bool $isFaulty = true): self + { + $this->isFaulty = $isFaulty; + + return $this; + } + + /** + * Get whether the config is faulty + * + * @return bool + */ + public function isFaulty(): bool + { + return $this->isFaulty; + } +} |