diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:42:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:42:35 +0000 |
commit | 18db984057b83ca4962c89b6b79bdce6a660b58f (patch) | |
tree | 2c9f23c086b4dfcb3e7eb2ec69210206b0782d3c /library/Businessprocess | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-businessprocess-c317dc6bab668eef621846dc909e6b1af78bf49a.tar.xz icingaweb2-module-businessprocess-c317dc6bab668eef621846dc909e6b1af78bf49a.zip |
Adding upstream version 2.4.0.upstream/2.4.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
66 files changed, 9143 insertions, 0 deletions
diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php new file mode 100644 index 0000000..1e3f119 --- /dev/null +++ b/library/Businessprocess/BpConfig.php @@ -0,0 +1,1033 @@ +<?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; + + 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 int + */ + 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 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() + { + if ($this->getMetadata()->isManuallyOrdered()) { + uasort($this->root_nodes, function (BpNode $a, BpNode $b) { + $a = $a->getDisplay(); + $b = $b->getDisplay(); + return $a > $b ? 1 : ($a < $b ? -1 : 0); + }); + } else { + ksort($this->root_nodes, SORT_NATURAL | SORT_FLAG_CASE); + } + + return $this->root_nodes; + } + + public function listRootNodes() + { + $names = array_keys($this->root_nodes); + if ($this->getMetadata()->isManuallyOrdered()) { + uasort($names, function ($a, $b) { + $a = $this->root_nodes[$a]->getDisplay(); + $b = $this->root_nodes[$b]->getDisplay(); + return $a > $b ? 1 : ($a < $b ? -1 : 0); + }); + } else { + natcasesort($names); + } + + return $names; + } + + 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[$host . ';' . $service] = $node; + $this->hosts[$host] = true; + return $node; + } + + public function createHost($host) + { + $node = new HostNode((object) array('hostname' => $host)); + $node->setBpConfig($this); + $this->nodes[$host . ';Hoststatus'] = $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])) { + $import = $this->storage()->loadProcess($name); + + 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 Node + * @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)); + $pos = strpos($name, ';'); + if ($pos !== false) { + $host = substr($name, 0, $pos); + $service = substr($name, $pos + 1); + // TODO: deactivated, this scares me, test it + if ($service === 'Hoststatus') { + return $this->createHost($host); + } else { + return $this->createService($host, $service); + } + } + + 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]; + } else { + throw new NotFoundError('Trying to access a missing business process node "%s"', $name); + } + } + + /** + * @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); + } + + if ($this->getMetadata()->isManuallyOrdered()) { + uasort($nodes, function ($a, $b) { + $a = $this->nodes[$a]->getDisplay(); + $b = $this->nodes[$b]->getDisplay(); + return $a > $b ? 1 : ($a < $b ? -1 : 0); + }); + } else { + natcasesort($nodes); + } + + 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); + } + + $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; + } +} diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php new file mode 100644 index 0000000..2ea8f8e --- /dev/null +++ b/library/Businessprocess/BpNode.php @@ -0,0 +1,664 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Businessprocess\Exception\NestingError; + +class BpNode extends Node +{ + const OP_AND = '&'; + const OP_OR = '|'; + const OP_NOT = '!'; + const OP_DEGRADED = '%'; + + protected $operator = '&'; + protected $url; + protected $info_command; + protected $display = 0; + + /** @var Node[] */ + protected $children; + + /** @var array */ + protected $childNames = array(); + protected $counters; + protected $missing = null; + protected $empty = null; + protected $missingChildren; + protected $stateOverrides = []; + + protected static $emptyStateSummary = array( + 'CRITICAL' => 0, + 'CRITICAL-HANDLED' => 0, + 'WARNING' => 0, + 'WARNING-HANDLED' => 0, + 'UNKNOWN' => 0, + 'UNKNOWN-HANDLED' => 0, + 'OK' => 0, + 'PENDING' => 0, + 'MISSING' => 0, + ); + + protected static $sortStateInversionMap = array( + 4 => 0, + 3 => 0, + 2 => 2, + 1 => 1, + 0 => 4 + ); + + protected $className = 'process'; + + public function __construct($object) + { + $this->name = $object->name; + $this->operator = $object->operator; + $this->childNames = $object->child_names; + } + + public function getStateSummary() + { + if ($this->counters === null) { + $this->getState(); + $this->counters = self::$emptyStateSummary; + + foreach ($this->getChildren() as $child) { + if ($child->isMissing()) { + $this->counters['MISSING']++; + } else { + $state = $child->getStateName($this->getChildState($child)); + if ($child->isHandled() && ($state !== 'UP' && $state !== 'OK')) { + $state = $state . '-HANDLED'; + } + + if ($state === 'DOWN') { + $this->counters['CRITICAL']++; + } elseif ($state === 'DOWN-HANDLED') { + $this->counters['CRITICAL-HANDLED']++; + } elseif ($state === 'UNREACHABLE') { + $this->counters['UNKNOWN']++; + } elseif ($state === 'UNREACHABLE-HANDLED') { + $this->counters['UNKNOWN-HANDLED']++; + } elseif ($state === 'UP') { + $this->counters['OK']++; + } else { + $this->counters[$state]++; + } + } + } + } + return $this->counters; + } + + public function hasProblems() + { + if ($this->isProblem()) { + return true; + } + + $okStates = array('OK', 'UP', 'PENDING', 'MISSING'); + + foreach ($this->getStateSummary() as $state => $cnt) { + if ($cnt !== 0 && ! in_array($state, $okStates)) { + return true; + } + } + + return false; + } + + /** + * @param Node $node + * @return $this + * @throws ConfigurationError + */ + public function addChild(Node $node) + { + if ($this->children === null) { + $this->getChildren(); + } + + $name = $node->getName(); + if (array_key_exists($name, $this->children)) { + throw new ConfigurationError( + 'Node "%s" has been defined more than once', + $name + ); + } + + $this->children[$name] = $node; + $this->childNames[] = $name; + $this->reorderChildren(); + $node->addParent($this); + return $this; + } + + public function getProblematicChildren() + { + $problems = array(); + + foreach ($this->getChildren() as $child) { + if (isset($this->stateOverrides[$child->getName()])) { + $problem = $this->getChildState($child) > 0; + } else { + $problem = $child->isProblem() || ($child instanceof BpNode && $child->hasProblems()); + } + + if ($problem) { + $problems[] = $child; + } + } + + return $problems; + } + + public function hasChild($name) + { + return in_array($name, $this->getChildNames()); + } + + public function removeChild($name) + { + if (($key = array_search($name, $this->getChildNames())) !== false) { + unset($this->childNames[$key]); + + if (! empty($this->children)) { + unset($this->children[$name]); + } + } + + return $this; + } + + public function getProblemTree() + { + $tree = array(); + + foreach ($this->getProblematicChildren() as $child) { + $name = $child->getName(); + $tree[$name] = array( + 'node' => $child, + 'children' => array() + ); + if ($child instanceof BpNode) { + $tree[$name]['children'] = $child->getProblemTree(); + } + } + + return $tree; + } + + /** + * Get the problem nodes as tree reduced to the nodes which have the same state as the business process + * + * @param bool $rootCause Reduce nodes to the nodes which are responsible for the state of the business process + * + * @return array + */ + public function getProblemTreeBlame($rootCause = false) + { + $tree = []; + $nodeState = $this->getState(); + + if ($nodeState !== 0) { + foreach ($this->getChildren() as $child) { + $childState = $this->getChildState($child); + $childState = $rootCause ? $child->getSortingState($childState) : $childState; + if (($rootCause ? $this->getSortingState() : $nodeState) === $childState) { + $name = $child->getName(); + $tree[$name] = [ + 'children' => [], + 'node' => $child + ]; + if ($child instanceof BpNode) { + $tree[$name]['children'] = $child->getProblemTreeBlame($rootCause); + } + } + } + } + + return $tree; + } + + + public function isMissing() + { + if ($this->missing === null) { + $exists = false; + $bp = $this->getBpConfig(); + $bp->beginLoopDetection($this->name); + foreach ($this->getChildren() as $child) { + if (! $child->isMissing()) { + $exists = true; + } + } + $bp->endLoopDetection($this->name); + $this->missing = ! $exists && ! empty($this->getChildren()); + } + return $this->missing; + } + + public function isEmpty() + { + $bp = $this->getBpConfig(); + $empty = true; + if ($this->countChildren()) { + $bp->beginLoopDetection($this->name); + foreach ($this->getChildren() as $child) { + if ($child instanceof MonitoredNode) { + $empty = false; + break; + } elseif (!$child->isEmpty()) { + $empty = false; + } + } + $bp->endLoopDetection($this->name); + } + $this->empty = $empty; + + return $this->empty; + } + + + public function getMissingChildren() + { + if ($this->missingChildren === null) { + $missing = array(); + + foreach ($this->getChildren() as $child) { + if ($child->isMissing()) { + $missing[$child->getName()] = $child; + } + + foreach ($child->getMissingChildren() as $m) { + $missing[$m->getName()] = $m; + } + } + + $this->missingChildren = $missing; + } + + return $this->missingChildren; + } + + public function getOperator() + { + return $this->operator; + } + + public function setOperator($operator) + { + $this->assertValidOperator($operator); + $this->operator = $operator; + return $this; + } + + protected function assertValidOperator($operator) + { + switch ($operator) { + case self::OP_AND: + case self::OP_OR: + case self::OP_NOT: + case self::OP_DEGRADED: + return; + default: + if (is_numeric($operator)) { + return; + } + } + + throw new ConfigurationError( + 'Got invalid operator: %s', + $operator + ); + } + + public function setInfoUrl($url) + { + $this->url = $url; + return $this; + } + + public function hasInfoUrl() + { + return ! empty($this->url); + } + + public function getInfoUrl() + { + return $this->url; + } + + public function setInfoCommand($cmd) + { + $this->info_command = $cmd; + } + + public function hasInfoCommand() + { + return $this->info_command !== null; + } + + public function getInfoCommand() + { + return $this->info_command; + } + + public function setStateOverrides(array $overrides, $name = null) + { + if ($name === null) { + $this->stateOverrides = $overrides; + } else { + $this->stateOverrides[$name] = $overrides; + } + + return $this; + } + + public function getStateOverrides($name = null) + { + $overrides = null; + if ($name !== null) { + if (isset($this->stateOverrides[$name])) { + $overrides = $this->stateOverrides[$name]; + } + } else { + $overrides = $this->stateOverrides; + } + + return $overrides; + } + + public function getAlias() + { + return $this->alias ? preg_replace('~_~', ' ', $this->alias) : $this->name; + } + + /** + * @return int + */ + public function getState() + { + if ($this->state === null) { + try { + $this->reCalculateState(); + } catch (NestingError $e) { + $this->getBpConfig()->addError( + $this->getBpConfig()->translate('Nesting error detected: %s'), + $e->getMessage() + ); + + // Failing nodes are unknown + $this->state = 3; + } + } + + return $this->state; + } + + /** + * Get the given child's state, possibly adjusted by override rules + * + * @param Node|string $child + * @return int + */ + public function getChildState($child) + { + if (! $child instanceof Node) { + $child = $this->getChildByName($child); + } + + $childName = $child->getName(); + $childState = $child->getState(); + if (! isset($this->stateOverrides[$childName][$childState])) { + return $childState; + } + + return $this->stateOverrides[$childName][$childState]; + } + + public function getHtmlId() + { + return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName()); + } + + protected function invertSortingState($state) + { + return self::$sortStateInversionMap[$state >> self::SHIFT_FLAGS] << self::SHIFT_FLAGS; + } + + /** + * @return $this + */ + public function reCalculateState() + { + $bp = $this->getBpConfig(); + + $sort_states = array(); + $lastStateChange = 0; + + if ($this->isEmpty()) { + // TODO: delegate this to operators, should mostly fail + $this->setState(self::NODE_EMPTY); + return $this; + } + + foreach ($this->getChildren() as $child) { + $bp->beginLoopDetection($this->name); + if ($child instanceof MonitoredNode && $child->isMissing()) { + if ($child instanceof HostNode) { + $child->setState(self::ICINGA_UNREACHABLE); + } else { + $child->setState(self::ICINGA_UNKNOWN); + } + + $child->setMissing(); + } + $sort_states[] = $child->getSortingState($this->getChildState($child)); + $lastStateChange = max($lastStateChange, $child->getLastStateChange()); + $bp->endLoopDetection($this->name); + } + + $this->setLastStateChange($lastStateChange); + + switch ($this->getOperator()) { + case self::OP_AND: + $sort_state = max($sort_states); + break; + case self::OP_NOT: + $sort_state = $this->invertSortingState(max($sort_states)); + break; + case self::OP_OR: + $sort_state = min($sort_states); + break; + case self::OP_DEGRADED: + $maxState = max($sort_states); + $flags = $maxState & 0xf; + + $maxIcingaState = $this->sortStateTostate($maxState); + $warningState = ($this->stateToSortState(self::ICINGA_WARNING) << self::SHIFT_FLAGS) + $flags; + + $sort_state = ($maxIcingaState === self::ICINGA_CRITICAL) ? $warningState : $maxState; + break; + default: + // MIN: + $sort_state = 3 << self::SHIFT_FLAGS; + + if (count($sort_states) >= $this->operator) { + $actualGood = 0; + foreach ($sort_states as $s) { + if (($s >> self::SHIFT_FLAGS) === self::ICINGA_OK) { + $actualGood++; + } + } + + if ($actualGood >= $this->operator) { + // condition is fulfilled + $sort_state = self::ICINGA_OK; + } else { + // worst state if not fulfilled + $sort_state = max($sort_states); + } + } + } + if ($sort_state & self::FLAG_DOWNTIME) { + $this->setDowntime(true); + } + if ($sort_state & self::FLAG_ACK) { + $this->setAck(true); + } + + $this->state = $this->sortStateTostate($sort_state); + return $this; + } + + public function checkForLoops() + { + $bp = $this->getBpConfig(); + foreach ($this->getChildren() as $child) { + $bp->beginLoopDetection($this->name); + if ($child instanceof BpNode) { + $child->checkForLoops(); + } + $bp->endLoopDetection($this->name); + } + + return $this; + } + + public function setDisplay($display) + { + $this->display = (int) $display; + return $this; + } + + public function getDisplay() + { + return $this->display; + } + + public function setChildNames($names) + { + $this->childNames = $names; + $this->children = null; + $this->reorderChildren(); + return $this; + } + + public function hasChildren($filter = null) + { + $childNames = $this->getChildNames(); + return !empty($childNames); + } + + public function getChildNames() + { + return $this->childNames; + } + + public function getChildren($filter = null) + { + if ($this->children === null) { + $this->children = []; + $this->reorderChildren(); + foreach ($this->getChildNames() as $name) { + $this->children[$name] = $this->getBpConfig()->getNode($name); + $this->children[$name]->addParent($this); + } + } + + return $this->children; + } + + /** + * Reorder this node's children, in case manual order is not applied + */ + protected function reorderChildren() + { + if ($this->getBpConfig()->getMetadata()->isManuallyOrdered()) { + return; + } + + $childNames = $this->getChildNames(); + natcasesort($childNames); + $this->childNames = array_values($childNames); + + if (! empty($this->children)) { + $children = []; + foreach ($this->childNames as $name) { + $children[$name] = $this->children[$name]; + } + + $this->children = $children; + } + } + + /** + * return BpNode[] + */ + public function getChildBpNodes() + { + $children = array(); + + foreach ($this->getChildren() as $name => $child) { + if ($child instanceof BpNode) { + $children[$name] = $child; + } + } + + return $children; + } + + /** + * @param $childName + * @return Node + * @throws NotFoundError + */ + public function getChildByName($childName) + { + foreach ($this->getChildren() as $name => $child) { + if ($name === $childName) { + return $child; + } + } + + throw new NotFoundError('Trying to get missing child %s', $childName); + } + + protected function assertNumericOperator() + { + if (! is_numeric($this->getOperator())) { + throw new ConfigurationError('Got invalid operator: %s', $this->operator); + } + } + + public function operatorHtml() + { + switch ($this->getOperator()) { + case self::OP_AND: + return 'AND'; + break; + case self::OP_OR: + return 'OR'; + break; + case self::OP_NOT: + return 'NOT'; + break; + case self::OP_DEGRADED: + return 'DEG'; + break; + default: + // MIN + $this->assertNumericOperator(); + return 'min:' . $this->operator; + } + } + + public function getIcon() + { + $this->icon = $this->hasParents() ? 'cubes' : 'sitemap'; + return parent::getIcon(); + } +} diff --git a/library/Businessprocess/Common/EnumList.php b/library/Businessprocess/Common/EnumList.php new file mode 100644 index 0000000..a1e5b56 --- /dev/null +++ b/library/Businessprocess/Common/EnumList.php @@ -0,0 +1,168 @@ +<?php + +namespace Icinga\Module\Businessprocess\Common; + +use Icinga\Application\Modules\Module; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Businessprocess\IcingaDbObject; +use Icinga\Module\Businessprocess\MonitoringRestrictions; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; + +trait EnumList +{ + protected function enumHostForServiceList() + { + if ($this->useIcingaDbBackend()) { + $names = (new IcingaDbObject())->yieldHostnames(); + } else { + $names = $this->backend + ->select() + ->from('hostStatus', ['hostname' => 'host_name']) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->order('host_name') + ->getQuery() + ->fetchColumn(); + } + + // fetchPairs doesn't seem to work when using the same column with + // different aliases twice + $res = array(); + foreach ($names as $name) { + $res[$name] = $name; + } + + return $res; + } + + protected function enumHostList() + { + if ($this->useIcingaDbBackend()) { + $names = (new IcingaDbObject())->yieldHostnames(); + } else { + $names = $this->backend + ->select() + ->from('hostStatus', ['hostname' => 'host_name']) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->order('host_name') + ->getQuery() + ->fetchColumn(); + } + + // fetchPairs doesn't seem to work when using the same column with + // different aliases twice + $res = array(); + $suffix = ';Hoststatus'; + foreach ($names as $name) { + $res[$name . $suffix] = $name; + } + + return $res; + } + + protected function enumServiceList($host) + { + if ($this->useIcingaDbBackend()) { + $names = (new IcingaDbObject())->yieldServicenames($host); + } else { + $names = $this->backend + ->select() + ->from('serviceStatus', ['service' => 'service_description']) + ->where('host_name', $host) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->order('service_description') + ->getQuery() + ->fetchColumn(); + } + + $services = array(); + foreach ($names as $name) { + $services[$host . ';' . $name] = $name; + } + + return $services; + } + + protected function enumHostListByFilter($filter) + { + if ($this->useIcingaDbBackend()) { + $names = (new IcingaDbObject())->yieldHostnames($filter); + } else { + $names = $this->backend + ->select() + ->from('hostStatus', ['hostname' => 'host_name']) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->order('host_name') + ->getQuery() + ->fetchColumn(); + } + + // fetchPairs doesn't seem to work when using the same column with + // different aliases twice + $res = array(); + $suffix = ';Hoststatus'; + foreach ($names as $name) { + $res[$name . $suffix] = $name; + } + + return $res; + } + + protected function enumServiceListByFilter($filter) + { + $services = array(); + + if ($this->useIcingaDbBackend()) { + $objects = (new IcingaDbObject())->fetchServices($filter); + foreach ($objects as $object) { + $services[$object->host->name . ';' . $object->name] = $object->host->name . ':' . $object->name; + } + } else { + $objects = $this->backend + ->select() + ->from('serviceStatus', ['host' => 'host_name', 'service' => 'service_description']) + ->applyFilter(Filter::fromQueryString($filter)) + ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects')) + ->order('service_description') + ->getQuery() + ->fetchAll(); + foreach ($objects as $object) { + $services[$object->host . ';' . $object->service] = $object->host . ':' . $object->service; + } + } + + return $services; + } + + protected function enumHostStateList() + { + $hostStateList = [ + 0 => $this->translate('UP'), + 1 => $this->translate('DOWN'), + 99 => $this->translate('PENDING') + ]; + + return $hostStateList; + } + + protected function enumServiceStateList() + { + $serviceStateList = [ + 0 => $this->translate('OK'), + 1 => $this->translate('WARNING'), + 2 => $this->translate('CRITICAL'), + 3 => $this->translate('UNKNOWN'), + 99 => $this->translate('PENDING'), + ]; + + return $serviceStateList; + } + + protected function useIcingaDbBackend() + { + if (Module::exists('icingadb')) { + return ! $this->bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend(); + } + + return false; + } +} diff --git a/library/Businessprocess/Director/ShipConfigFiles.php b/library/Businessprocess/Director/ShipConfigFiles.php new file mode 100644 index 0000000..17b9e1f --- /dev/null +++ b/library/Businessprocess/Director/ShipConfigFiles.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Businessprocess\Director; + +use Icinga\Module\Director\Hook\ShipConfigFilesHook; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; + +class ShipConfigFiles extends ShipConfigFilesHook +{ + public function fetchFiles() + { + $files = array(); + + $storage = LegacyStorage::getInstance(); + + foreach ($storage->listProcesses() as $name => $title) { + $files['processes/' . $name . '.bp'] = $storage->getSource($name); + } + + return $files; + } +} diff --git a/library/Businessprocess/Exception/ModificationError.php b/library/Businessprocess/Exception/ModificationError.php new file mode 100644 index 0000000..430d513 --- /dev/null +++ b/library/Businessprocess/Exception/ModificationError.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Businessprocess\Exception; + +use Icinga\Exception\IcingaException; + +class ModificationError extends IcingaException +{ +} diff --git a/library/Businessprocess/Exception/NestingError.php b/library/Businessprocess/Exception/NestingError.php new file mode 100644 index 0000000..89cbf81 --- /dev/null +++ b/library/Businessprocess/Exception/NestingError.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Businessprocess\Exception; + +use Icinga\Exception\IcingaException; + +class NestingError extends IcingaException +{ +} diff --git a/library/Businessprocess/Form.php b/library/Businessprocess/Form.php new file mode 100644 index 0000000..3270b38 --- /dev/null +++ b/library/Businessprocess/Form.php @@ -0,0 +1,36 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Web\Request; +use Icinga\Web\Form as WebForm; + +class Form extends WebForm +{ + public function __construct($options = null) + { + parent::__construct($options); + $this->setup(); + } + + public function addHidden($name, $value = null) + { + $this->addElement('hidden', $name); + $this->getElement($name)->setDecorators(array('ViewHelper')); + if ($value !== null) { + $this->setDefault($name, $value); + } + return $this; + } + + public function handleRequest(Request $request = null) + { + parent::handleRequest(); + return $this; + } + + public static function construct() + { + return new static; + } +} diff --git a/library/Businessprocess/HostNode.php b/library/Businessprocess/HostNode.php new file mode 100644 index 0000000..b66f66f --- /dev/null +++ b/library/Businessprocess/HostNode.php @@ -0,0 +1,64 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Module\Businessprocess\Web\Url; + +class HostNode extends MonitoredNode +{ + protected $sortStateToStateMap = array( + 4 => self::ICINGA_DOWN, + 3 => self::ICINGA_UNREACHABLE, + 1 => self::ICINGA_PENDING, + 0 => self::ICINGA_UP + ); + + protected $stateToSortStateMap = array( + self::ICINGA_PENDING => 1, + self::ICINGA_UNREACHABLE => 3, + self::ICINGA_DOWN => 4, + self::ICINGA_UP => 0, + ); + + protected $stateNames = array( + 'UP', + 'DOWN', + 'UNREACHABLE', + 99 => 'PENDING' + ); + + protected $hostname; + + protected $className = 'host'; + + protected $icon = 'host'; + + public function __construct($object) + { + $this->name = $object->hostname . ';Hoststatus'; + $this->hostname = $object->hostname; + if (isset($object->state)) { + $this->setState($object->state); + } else { + $this->setState(0)->setMissing(); + } + } + + public function getHostname() + { + return $this->hostname; + } + + public function getUrl() + { + $params = array( + 'host' => $this->getHostname(), + ); + + if ($this->getBpConfig()->hasBackendName()) { + $params['backend'] = $this->getBpConfig()->getBackendName(); + } + + return Url::fromPath('businessprocess/host/show', $params); + } +} diff --git a/library/Businessprocess/IcingaDbObject.php b/library/Businessprocess/IcingaDbObject.php new file mode 100644 index 0000000..cad459f --- /dev/null +++ b/library/Businessprocess/IcingaDbObject.php @@ -0,0 +1,94 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Module\Icingadb\Common\Auth; +use Icinga\Module\Icingadb\Common\Database as IcingadbDatabase; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Sql\Connection as IcingaDbConnection; +use ipl\Web\Filter\QueryString; + +class IcingaDbObject +{ + use IcingadbDatabase; + + use Auth; + + /** @var BpConfig */ + protected $config; + + /** @var IcingaDbConnection */ + protected $conn; + + public function __construct() + { + $this->conn = $this->getDb(); + } + + public function fetchHosts($filter = null) + { + + $hosts = Host::on($this->conn); + + if ($filter !== null) { + $filterQuery = QueryString::parse($filter); + + $hosts->filter($filterQuery); + } + + $hosts->orderBy('host.name'); + + $this->applyIcingaDbRestrictions($hosts); + + return $hosts; + } + + public function fetchServices($filter) + { + $services = Service::on($this->conn) + ->with('host'); + + if ($filter !== null) { + $filterQuery = QueryString::parse($filter); + + $services->filter($filterQuery); + } + + $services->orderBy('service.name'); + + $this->applyIcingaDbRestrictions($services); + + return $services; + } + + public function yieldHostnames($filter = null) + { + foreach ($this->fetchHosts($filter) as $host) { + yield $host->name; + } + } + + public function yieldServicenames($host) + { + $filter = "host.name=$host"; + + foreach ($this->fetchServices($filter) as $service) { + yield $service->name; + } + } + + public static function applyIcingaDbRestrictions($query) + { + $object = new self; + $object->applyRestrictions($query); + + return $object; + } + + public static function fetchDb() + { + $object = new self; + return $object->getDb(); + } +} diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php new file mode 100644 index 0000000..3f0b460 --- /dev/null +++ b/library/Businessprocess/ImportedNode.php @@ -0,0 +1,135 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Exception; + +class ImportedNode extends BpNode +{ + /** @var BpConfig */ + protected $parentBp; + + /** @var string */ + protected $configName; + + /** @var string */ + protected $nodeName; + + /** @var BpNode */ + protected $importedNode; + + /** @var string */ + protected $className = 'process subtree'; + + /** @var string */ + protected $icon = 'download'; + + public function __construct(BpConfig $parentBp, $object) + { + $this->parentBp = $parentBp; + $this->configName = $object->configName; + $this->nodeName = $object->node; + + parent::__construct((object) [ + 'name' => '@' . $this->configName . ':' . $this->nodeName, + 'operator' => null, + 'child_names' => null + ]); + } + + /** + * @return string + */ + public function getConfigName() + { + return $this->configName; + } + + /** + * @return string + */ + public function getNodeName() + { + return $this->nodeName; + } + + public function getIdentifier() + { + return $this->getName(); + } + + public function getBpConfig() + { + if ($this->bp === null) { + $this->bp = $this->parentBp->getImportedConfig($this->configName); + } + + return $this->bp; + } + + public function getAlias() + { + if ($this->alias === null) { + $this->alias = $this->importedNode()->getAlias(); + } + + return $this->alias; + } + + public function getOperator() + { + if ($this->operator === null) { + $this->operator = $this->importedNode()->getOperator(); + } + + return $this->operator; + } + + public function getChildNames() + { + if ($this->childNames === null) { + $this->childNames = $this->importedNode()->getChildNames(); + } + + return $this->childNames; + } + + /** + * @return BpNode + */ + protected function importedNode() + { + if ($this->importedNode === null) { + try { + $this->importedNode = $this->getBpConfig()->getBpNode($this->nodeName); + } catch (Exception $e) { + return $this->createFailedNode($e); + } + } + + return $this->importedNode; + } + + /** + * @param Exception $e + * + * @return BpNode + */ + protected function createFailedNode(Exception $e) + { + $this->parentBp->addError($e->getMessage()); + $node = new BpNode((object) array( + 'name' => $this->getName(), + 'operator' => '&', + 'child_names' => [] + )); + $node->setBpConfig($this->getBpConfig()); + $node->setState(2); + $node->setMissing(false) + ->setDowntime(false) + ->setAck(false) + ->setAlias($e->getMessage()); + + return $node; + } +} diff --git a/library/Businessprocess/Metadata.php b/library/Businessprocess/Metadata.php new file mode 100644 index 0000000..b640fb8 --- /dev/null +++ b/library/Businessprocess/Metadata.php @@ -0,0 +1,264 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Application\Icinga; +use Icinga\Authentication\Auth; +use Icinga\Exception\ProgrammingError; +use Icinga\User; + +class Metadata +{ + /** @var string Configuration name */ + protected $name; + + protected $properties = array( + 'Title' => null, + 'Description' => null, + 'Owner' => null, + 'AllowedUsers' => null, + 'AllowedGroups' => null, + 'AllowedRoles' => null, + 'AddToMenu' => null, + 'Backend' => null, + 'Statetype' => null, + 'ManualOrder' => null, + // 'SLAHosts' => null + ); + + public function __construct($name) + { + $this->name = $name; + } + + public function getTitle() + { + if ($this->has('Title')) { + return $this->get('Title'); + } else { + return $this->name; + } + } + + public function getExtendedTitle() + { + $title = $this->getTitle(); + + if ($title === $this->name) { + return $title; + } else { + return sprintf('%s (%s)', $title, $this->name); + } + } + + public function getProperties() + { + return $this->properties; + } + + public function hasKey($key) + { + return array_key_exists($key, $this->properties); + } + + public function get($key, $default = null) + { + $this->assertKeyExists($key); + if ($this->properties[$key] === null) { + return $default; + } + + return $this->properties[$key]; + } + + public function set($key, $value) + { + $this->assertKeyExists($key); + $this->properties[$key] = $value; + + return $this; + } + + public function isNull($key) + { + return null === $this->get($key); + } + + public function has($key) + { + return null !== $this->get($key); + } + + protected function assertKeyExists($key) + { + if (! $this->hasKey($key)) { + throw new ProgrammingError('Trying to access invalid header key: %s', $key); + } + + return $this; + } + + public function hasRestrictions() + { + return ! ( + $this->isNull('AllowedUsers') + && $this->isNull('AllowedGroups') + && $this->isNull('AllowedRoles') + ); + } + + protected function getAuth() + { + return Auth::getInstance(); + } + + public function canModify(Auth $auth = null) + { + if ($auth === null) { + if (Icinga::app()->isCli()) { + return true; + } else { + $auth = $this->getAuth(); + } + } + + return $this->canRead($auth) && ( + $auth->hasPermission('businessprocess/modify') + || $this->ownerIs($auth->getUser()->getUsername()) + ); + } + + public function canRead(Auth $auth = null) + { + if ($auth === null) { + if (Icinga::app()->isCli()) { + return true; + } else { + $auth = $this->getAuth(); + } + } + + if ($auth->hasPermission('businessprocess/showall')) { + return true; + } + + $prefixes = $auth->getRestrictions('businessprocess/prefix'); + if (! empty($prefixes)) { + if (! $this->nameIsPrefixedWithOneOf($prefixes)) { + return false; + } + } + + if (! $this->hasRestrictions()) { + return true; + } + + if (! $auth->isAuthenticated()) { + return false; + } + + return $this->userCanRead($auth->getUser()); + } + + public function nameIsPrefixedWithOneOf(array $prefixes) + { + foreach ($prefixes as $prefix) { + if (substr($this->name, 0, strlen($prefix)) === $prefix) { + return true; + } + } + + return false; + } + + protected function userCanRead(User $user) + { + $username = $user->getUsername(); + + return $this->ownerIs($username) + || $this->isInAllowedUserList($username) + || $this->isMemberOfAllowedGroups($user) + || $this->hasOneOfTheAllowedRoles($user); + } + + public function ownerIs($username) + { + return $this->get('Owner') === $username; + } + + public function listAllowedUsers() + { + // TODO: $this->get('AllowedUsers', array()); + $list = $this->get('AllowedUsers'); + if ($list === null) { + return array(); + } else { + return $this->splitCommaSeparated($list); + } + } + + public function listAllowedGroups() + { + $list = $this->get('AllowedGroups'); + if ($list === null) { + return array(); + } else { + return $this->splitCommaSeparated($list); + } + } + + public function listAllowedRoles() + { + $list = $this->get('AllowedRoles'); + if ($list === null) { + return array(); + } else { + return $this->splitCommaSeparated($list); + } + } + + public function isInAllowedUserList($username) + { + foreach ($this->listAllowedUsers() as $allowedUser) { + if ($username === $allowedUser) { + return true; + } + } + + return false; + } + + public function isMemberOfAllowedGroups(User $user) + { + foreach ($this->listAllowedGroups() as $group) { + if ($user->isMemberOf($group)) { + return true; + } + } + + return false; + } + + public function hasOneOfTheAllowedRoles(User $user) + { + foreach ($this->listAllowedRoles() as $roleName) { + foreach ($user->getRoles() as $role) { + if ($role->getName() === $roleName) { + return true; + } + } + } + + return false; + } + + public function isManuallyOrdered() + { + return $this->get('ManualOrder') === 'yes'; + } + + protected function splitCommaSeparated($string) + { + return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); + } +} diff --git a/library/Businessprocess/Modification/NodeAction.php b/library/Businessprocess/Modification/NodeAction.php new file mode 100644 index 0000000..369c3a2 --- /dev/null +++ b/library/Businessprocess/Modification/NodeAction.php @@ -0,0 +1,179 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Exception\ModificationError; +use Icinga\Module\Businessprocess\Node; +use Icinga\Exception\ProgrammingError; + +/** + * Abstract NodeAction class + * + * Every instance of a NodeAction represents a single applied change. Changes are pushed to + * a stack and consumed from there. When persisted, NodeActions are serialized with their name, + * node name and optionally additional properties according preserveProperties. For each property + * that should be preserved, getter and setter methods have to be defined. + * + * @package Icinga\Module\Businessprocess + */ +abstract class NodeAction +{ + /** @var string Name of this action (currently create, modify, remove) */ + protected $actionName; + + /** @var string Name of the node this action applies to */ + protected $nodeName; + + /** @var array Properties which should be preserved when serializing this action */ + protected $preserveProperties = array(); + + /** + * NodeAction constructor. + * + * @param Node|string $node + */ + public function __construct($node = null) + { + if ($node !== null) { + $this->nodeName = (string) $node; + } + } + + /** + * Every NodeAction must be able to apply itself to a BusinessProcess + * + * @param BpConfig $config + * @return mixed + */ + abstract public function applyTo(BpConfig $config); + + /** + * Every NodeAction must be able to tell whether it can be applied to a BusinessProcess + * + * @param BpConfig $config + * + * @throws ModificationError + * + * @return bool + */ + abstract public function appliesTo(BpConfig $config); + + /** + * The name of the node this modification applies to + * + * @return string + */ + public function getNodeName() + { + return $this->nodeName; + } + + public function hasNode() + { + return $this->nodeName !== null; + } + + /** + * Whether this is an instance of a given action name + * + * @param string $actionName + * @return bool + */ + public function is($actionName) + { + return $this->getActionName() === $actionName; + } + + /** + * Throw a ModificationError + * + * @param string $msg + * @param mixed ... + * + * @throws ModificationError + */ + protected function error($msg) + { + $error = ModificationError::create(func_get_args()); + /** @var ModificationError $error */ + throw $error; + } + + /** + * Create an instance of a given actionName for a specific Node + * + * @param string $actionName + * @param string $nodeName + * + * @return static + */ + public static function create($actionName, $nodeName) + { + $className = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action'; + $object = new $className($nodeName); + return $object; + } + + /** + * Returns a JSON-encoded serialized NodeAction + * + * @return string + */ + public function serialize() + { + $object = (object) array( + 'actionName' => $this->getActionName(), + 'nodeName' => $this->getNodeName(), + 'properties' => array() + ); + + foreach ($this->preserveProperties as $key) { + $func = 'get' . ucfirst($key); + $object->properties[$key] = $this->$func(); + } + + return json_encode($object); + } + + /** + * Decodes a JSON-serialized NodeAction and returns an object instance + * + * @param $string + * @return NodeAction + */ + public static function unSerialize($string) + { + $object = json_decode($string, JSON_FORCE_OBJECT); + $action = self::create($object['actionName'], $object['nodeName']); + + foreach ($object['properties'] as $key => $val) { + $func = 'set' . ucfirst($key); + $action->$func($val); + } + + return $action; + } + + /** + * Returns the defined action name or determines such from the class name + * + * @return string The action name + * + * @throws ProgrammingError when no such class exists + */ + public function getActionName() + { + if ($this->actionName === null) { + if (! preg_match('/\\\Node(\w+)Action$/', get_class($this), $m)) { + throw new ProgrammingError( + '"%s" is not a NodeAction class', + get_class($this) + ); + } + $this->actionName = lcfirst($m[1]); + } + + return $this->actionName; + } +} diff --git a/library/Businessprocess/Modification/NodeAddChildrenAction.php b/library/Businessprocess/Modification/NodeAddChildrenAction.php new file mode 100644 index 0000000..5d5ab29 --- /dev/null +++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php @@ -0,0 +1,75 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; + +class NodeAddChildrenAction extends NodeAction +{ + protected $children = array(); + + protected $preserveProperties = array('children'); + + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) + { + $name = $this->getNodeName(); + + if (! $config->hasBpNode($name)) { + $this->error('Process "%s" not found', $name); + } + + return true; + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + $node = $config->getBpNode($this->getNodeName()); + + foreach ($this->children as $name) { + if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) { + if (strpos($name, ';') !== false) { + list($host, $service) = preg_split('/;/', $name, 2); + + if ($service === 'Hoststatus') { + $config->createHost($host); + } else { + $config->createService($host, $service); + } + } elseif ($name[0] === '@' && strpos($name, ':') !== false) { + list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2); + $config->createImportedNode($configName, $nodeName); + } + } + $node->addChild($config->getNode($name)); + } + + return $this; + } + + /** + * @param array|string $children + * @return $this + */ + public function setChildren($children) + { + if (is_string($children)) { + $children = array($children); + } + $this->children = $children; + return $this; + } + + /** + * @return array + */ + public function getChildren() + { + return $this->children; + } +} diff --git a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php new file mode 100644 index 0000000..9be77e9 --- /dev/null +++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php @@ -0,0 +1,29 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; + +class NodeApplyManualOrderAction extends NodeAction +{ + public function appliesTo(BpConfig $config) + { + return $config->getMetadata()->get('ManualOrder') !== 'yes'; + } + + public function applyTo(BpConfig $config) + { + $i = 0; + foreach ($config->getBpNodes() as $name => $node) { + if ($node->getDisplay() > 0) { + $node->setDisplay(++$i); + } + + if ($node->hasChildren()) { + $node->setChildNames($node->getChildNames()); + } + } + + $config->getMetadata()->set('ManualOrder', 'yes'); + } +} diff --git a/library/Businessprocess/Modification/NodeCopyAction.php b/library/Businessprocess/Modification/NodeCopyAction.php new file mode 100644 index 0000000..609d704 --- /dev/null +++ b/library/Businessprocess/Modification/NodeCopyAction.php @@ -0,0 +1,39 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; + +class NodeCopyAction extends NodeAction +{ + /** + * @param BpConfig $config + * @return bool + */ + public function appliesTo(BpConfig $config) + { + $name = $this->getNodeName(); + + if (! $config->hasBpNode($name)) { + $this->error('Process "%s" not found', $name); + } + + if ($config->hasRootNode($name)) { + $this->error('A toplevel node with name "%s" already exists', $name); + } + + return true; + } + + /** + * @param BpConfig $config + */ + public function applyTo(BpConfig $config) + { + $name = $this->getNodeName(); + $rootNodes = $config->getRootNodes(); + $config->addRootNode($name) + ->getBpNode($name) + ->setDisplay(end($rootNodes)->getDisplay() + 1); + } +} diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php new file mode 100644 index 0000000..167d3bc --- /dev/null +++ b/library/Businessprocess/Modification/NodeCreateAction.php @@ -0,0 +1,129 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Node; + +class NodeCreateAction extends NodeAction +{ + /** @var string */ + protected $parentName; + + /** @var array */ + protected $properties = array(); + + /** @var array */ + protected $preserveProperties = array('parentName', 'properties'); + + /** + * @param Node $name + */ + public function setParent(Node $name) + { + $this->parentName = $name->getName(); + } + + /** + * @return bool + */ + public function hasParent() + { + return $this->parentName !== null; + } + + /** + * @return string + */ + public function getParentName() + { + return $this->parentName; + } + + /** + * @param string $name + */ + public function setParentName($name) + { + $this->parentName = $name; + } + + /** + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * @param array $properties + * @return $this + */ + public function setProperties($properties) + { + $this->properties = (array) $properties; + return $this; + } + + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) + { + $name = $this->getNodeName(); + if ($config->hasNode($name)) { + $this->error('A node with name "%s" already exists', $name); + } + + $parent = $this->getParentName(); + if ($parent !== null && !$config->hasBpNode($parent)) { + $this->error('Parent process "%s" missing', $parent); + } + + return true; + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + $name = $this->getNodeName(); + + $properties = array( + 'name' => $name, + 'operator' => $this->properties['operator'], + ); + if (array_key_exists('childNames', $this->properties)) { + $properties['child_names'] = $this->properties['childNames']; + } else { + $properties['child_names'] = array(); + } + $node = new BpNode((object) $properties); + $node->setBpConfig($config); + + foreach ($this->getProperties() as $key => $val) { + if ($key === 'parentName') { + $config->getBpNode($val)->addChild($node); + continue; + } + $func = 'set' . ucfirst($key); + $node->$func($val); + } + + if ($node->getDisplay() > 1) { + $i = $node->getDisplay(); + foreach ($config->getRootNodes() as $_ => $rootNode) { + if ($rootNode->getDisplay() >= $node->getDisplay()) { + $rootNode->setDisplay(++$i); + } + } + } + + $config->addNode($name, $node); + + return $node; + } +} diff --git a/library/Businessprocess/Modification/NodeModifyAction.php b/library/Businessprocess/Modification/NodeModifyAction.php new file mode 100644 index 0000000..1b33094 --- /dev/null +++ b/library/Businessprocess/Modification/NodeModifyAction.php @@ -0,0 +1,121 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Node; + +class NodeModifyAction extends NodeAction +{ + protected $properties = array(); + + protected $formerProperties = array(); + + protected $preserveProperties = array('formerProperties', 'properties'); + + /** + * Set properties for a specific node + * + * Can be called multiple times + * + * @param Node $node + * @param array $properties + * + * @return $this + */ + public function setNodeProperties(Node $node, array $properties) + { + foreach (array_keys($properties) as $key) { + $this->properties[$key] = $properties[$key]; + + if (array_key_exists($key, $this->formerProperties)) { + continue; + } + + $func = 'get' . ucfirst($key); + $this->formerProperties[$key] = $node->$func(); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) + { + $name = $this->getNodeName(); + + if (! $config->hasNode($name)) { + $this->error('Node "%s" not found', $name); + } + + $node = $config->getNode($name); + + foreach ($this->properties as $key => $val) { + $currentVal = $node->{'get' . ucfirst($key)}(); + if ($this->formerProperties[$key] !== $currentVal) { + $this->error( + 'Property %s of node "%s" changed its value from "%s" to "%s"', + $key, + $name, + $this->formerProperties[$key], + $currentVal + ); + } + } + + return true; + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + $node = $config->getNode($this->getNodeName()); + + foreach ($this->properties as $key => $val) { + $func = 'set' . ucfirst($key); + $node->$func($val); + } + + return $this; + } + + /** + * @param $properties + * @return $this + */ + public function setProperties($properties) + { + $this->properties = $properties; + return $this; + } + + /** + * @param $properties + * @return $this + */ + public function setFormerProperties($properties) + { + $this->formerProperties = $properties; + return $this; + } + + /** + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * @return array + */ + public function getFormerProperties() + { + return $this->formerProperties; + } +} diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php new file mode 100644 index 0000000..5754717 --- /dev/null +++ b/library/Businessprocess/Modification/NodeMoveAction.php @@ -0,0 +1,212 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; + +class NodeMoveAction extends NodeAction +{ + /** + * @var string + */ + protected $parent; + + /** + * @var string + */ + protected $newParent; + + /** + * @var int + */ + protected $from; + + /** + * @var int + */ + protected $to; + + protected $preserveProperties = ['parent', 'newParent', 'from', 'to']; + + public function setParent($name) + { + $this->parent = $name; + } + + public function getParent() + { + return $this->parent; + } + + public function setNewParent($name) + { + $this->newParent = $name; + } + + public function getNewParent() + { + return $this->newParent; + } + + public function setFrom($from) + { + $this->from = (int) $from; + } + + public function getFrom() + { + return $this->from; + } + + public function setTo($to) + { + $this->to = (int) $to; + } + + public function getTo() + { + return $this->to; + } + + public function appliesTo(BpConfig $config) + { + if (! $config->getMetadata()->isManuallyOrdered()) { + $this->error('Process configuration is not manually ordered yet'); + } + + $name = $this->getNodeName(); + if ($this->parent !== null) { + if (! $config->hasBpNode($this->parent)) { + $this->error('Parent process "%s" missing', $this->parent); + } + $parent = $config->getBpNode($this->parent); + if (! $parent->hasChild($name)) { + $this->error('Node "%s" not found in process "%s"', $name, $this->parent); + } + + $nodes = $parent->getChildNames(); + if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) { + $this->error('Node "%s" not found at position %d', $name, $this->from); + } + } else { + if (! $config->hasRootNode($name)) { + $this->error('Toplevel process "%s" not found', $name); + } + + $nodes = $config->listRootNodes(); + if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) { + $this->error('Toplevel process "%s" not found at position %d', $name, $this->from); + } + } + + if ($this->parent !== $this->newParent) { + if ($this->newParent !== null) { + if (! $config->hasBpNode($this->newParent)) { + $this->error('New parent process "%s" missing', $this->newParent); + } elseif ($config->getBpNode($this->newParent)->hasChild($name)) { + $this->error( + 'New parent process "%s" already has a node with the name "%s"', + $this->newParent, + $name + ); + } + + $childrenCount = $config->getBpNode($this->newParent)->countChildren(); + if ($this->to > 0 && $childrenCount < $this->to) { + $this->error( + 'New parent process "%s" has not enough children. Target position %d out of range', + $this->newParent, + $this->to + ); + } + } else { + if ($config->hasRootNode($name)) { + $this->error('Process "%s" is already a toplevel process', $name); + } + + $childrenCount = $config->countChildren(); + if ($this->to > 0 && $childrenCount < $this->to) { + $this->error( + 'Process configuration has not enough toplevel processes. Target position %d out of range', + $this->to + ); + } + } + } + + return true; + } + + public function applyTo(BpConfig $config) + { + $name = $this->getNodeName(); + if ($this->parent !== null) { + $nodes = $config->getBpNode($this->parent)->getChildren(); + } else { + $nodes = $config->getRootNodes(); + } + + $node = $nodes[$name]; + $nodes = array_merge( + array_slice($nodes, 0, $this->from, true), + array_slice($nodes, $this->from + 1, null, true) + ); + if ($this->parent === $this->newParent) { + $nodes = array_merge( + array_slice($nodes, 0, $this->to, true), + [$name => $node], + array_slice($nodes, $this->to, null, true) + ); + } else { + if ($this->newParent !== null) { + $newNodes = $config->getBpNode($this->newParent)->getChildren(); + } else { + $newNodes = $config->getRootNodes(); + } + + $newNodes = array_merge( + array_slice($newNodes, 0, $this->to, true), + [$name => $node], + array_slice($newNodes, $this->to, null, true) + ); + + if ($this->newParent !== null) { + $config->getBpNode($this->newParent)->setChildNames(array_keys($newNodes)); + } else { + $config->addRootNode($name); + + $i = 0; + foreach ($newNodes as $newName => $newNode) { + /** @var BpNode $newNode */ + if ($newNode->getDisplay() > 0 || $newName === $name) { + $i += 1; + if ($newNode->getDisplay() !== $i) { + $newNode->setDisplay($i); + } + } + } + } + } + + if ($this->parent !== null) { + $config->getBpNode($this->parent)->setChildNames(array_keys($nodes)); + } else { + if ($this->newParent !== null) { + $config->removeRootNode($name); + $node->setDisplay(0); + } + + $i = 0; + foreach ($nodes as $_ => $oldNode) { + /** @var BpNode $oldNode */ + if ($oldNode->getDisplay() > 0) { + $i += 1; + if ($oldNode->getDisplay() !== $i) { + $oldNode->setDisplay($i); + } + } + } + } + } +} diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php new file mode 100644 index 0000000..3769764 --- /dev/null +++ b/library/Businessprocess/Modification/NodeRemoveAction.php @@ -0,0 +1,90 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; + +/** + * NodeRemoveAction + * + * Tracks removed nodes + * + * @package Icinga\Module\Businessprocess + */ +class NodeRemoveAction extends NodeAction +{ + protected $preserveProperties = array('parentName'); + + protected $parentName; + + /** + * @param $parentName + * @return $this + */ + public function setParentName($parentName = null) + { + $this->parentName = $parentName; + return $this; + } + + /** + * @return mixed + */ + public function getParentName() + { + return $this->parentName; + } + + /** + * @inheritdoc + */ + public function appliesTo(BpConfig $config) + { + $name = $this->getNodeName(); + $parent = $this->getParentName(); + if ($parent === null) { + if (!$config->hasNode($name)) { + $this->error('Toplevel process "%s" not found', $name); + } + } else { + if (! $config->hasNode($parent)) { + $this->error('Parent process "%s" missing', $parent); + } elseif (! $config->getBpNode($parent)->hasChild($name)) { + $this->error('Node "%s" not found in process "%s"', $name, $parent); + } + } + + return true; + } + + /** + * @inheritdoc + */ + public function applyTo(BpConfig $config) + { + $name = $this->getNodeName(); + $parentName = $this->getParentName(); + if ($parentName === null) { + $oldDisplay = $config->getBpNode($name)->getDisplay(); + $config->removeNode($name); + if ($config->getMetadata()->isManuallyOrdered()) { + foreach ($config->getRootNodes() as $_ => $node) { + $nodeDisplay = $node->getDisplay(); + if ($nodeDisplay > $oldDisplay) { + $node->setDisplay($node->getDisplay() - 1); + } elseif ($nodeDisplay === $oldDisplay) { + break; // Stop immediately to not make things worse ;) + } + } + } + } else { + $node = $config->getNode($name); + $parent = $config->getBpNode($parentName); + $parent->removeChild($name); + $node->removeParent($parentName); + if (! $node->hasParents()) { + $config->removeNode($name); + } + } + } +} diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php new file mode 100644 index 0000000..0ed574c --- /dev/null +++ b/library/Businessprocess/Modification/ProcessChanges.php @@ -0,0 +1,295 @@ +<?php + +namespace Icinga\Module\Businessprocess\Modification; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Node; +use Icinga\Web\Session\SessionNamespace as Session; + +class ProcessChanges +{ + /** @var NodeAction[] */ + protected $changes = array(); + + /** @var Session */ + protected $session; + + /** @var BpConfig */ + protected $config; + + /** @var bool */ + protected $hasBeenModified = false; + + /** @var string Session storage key for this processes changes */ + protected $sessionKey; + + /** + * ProcessChanges constructor. + * + * Direct access is not allowed + */ + private function __construct() + { + } + + /** + * @param BpConfig $bp + * @param Session $session + * + * @return ProcessChanges + */ + public static function construct(BpConfig $bp, Session $session) + { + $key = 'changes.' . $bp->getName(); + $changes = new ProcessChanges(); + $changes->sessionKey = $key; + + if ($actions = $session->get($key)) { + foreach ($actions as $string) { + $changes->push(NodeAction::unSerialize($string)); + } + } + $changes->session = $session; + $changes->config = $bp; + + return $changes; + } + + /** + * @param Node $node + * @param $properties + * + * @return $this + */ + public function modifyNode(Node $node, $properties) + { + $action = new NodeModifyAction($node); + $action->setNodeProperties($node, $properties); + return $this->push($action, true); + } + + /** + * @param Node $node + * @param $properties + * + * @return $this + */ + public function addChildrenToNode($children, Node $node = null) + { + $action = new NodeAddChildrenAction($node); + $action->setChildren($children); + return $this->push($action, true); + } + + /** + * @param Node|string $nodeName + * @param array $properties + * @param Node $parent + * + * @return $this + */ + public function createNode($nodeName, $properties, Node $parent = null) + { + $action = new NodeCreateAction($nodeName); + $action->setProperties($properties); + if ($parent !== null) { + $action->setParent($parent); + } + return $this->push($action, true); + } + + /** + * @param $nodeName + * @param Node|null $parent + * @return $this + */ + public function copyNode($nodeName) + { + $action = new NodeCopyAction($nodeName); + return $this->push($action, true); + } + + /** + * @param Node $node + * @param string $parentName + * @return $this + */ + public function deleteNode(Node $node, $parentName = null) + { + $action = new NodeRemoveAction($node); + if ($parentName !== null) { + $action->setParentName($parentName); + } + + return $this->push($action, true); + } + + /** + * Move the given node + * + * @param Node $node + * @param int $from + * @param int $to + * @param string $newParent + * @param string $parent + * + * @return $this + */ + public function moveNode(Node $node, $from, $to, $newParent, $parent = null) + { + $action = new NodeMoveAction($node); + $action->setParent($parent); + $action->setNewParent($newParent); + $action->setFrom($from); + $action->setTo($to); + + return $this->push($action, true); + } + + /** + * Apply manual order on the entire bp configuration file + * + * @return $this + */ + public function applyManualOrder() + { + return $this->push(new NodeApplyManualOrderAction(), true); + } + + /** + * Add a new action to the stack + * + * @param NodeAction $change + * @param bool $apply + * + * @return $this + */ + public function push(NodeAction $change, $apply = false) + { + if ($apply && $change->appliesTo($this->config)) { + $change->applyTo($this->config); + } + + $this->changes[] = $change; + $this->hasBeenModified = true; + return $this; + } + + /** + * Get all stacked actions + * + * @return NodeAction[] + */ + public function getChanges() + { + return $this->changes; + } + + /** + * Forget all changes and remove them from the Session + * + * @return $this + */ + public function clear() + { + $this->hasBeenModified = true; + $this->changes = array(); + $this->session->set($this->getSessionKey(), null); + return $this; + } + + /** + * Whether there are no stacked changes + * + * @return bool + */ + public function isEmpty() + { + return $this->count() === 0; + } + + /** + * Number of stacked changes + * + * @return bool + */ + public function count() + { + return count($this->changes); + } + + /** + * Get the first change on the stack, false if empty + * + * @return NodeAction|boolean + */ + public function shift() + { + if ($this->isEmpty()) { + return false; + } + + $this->hasBeenModified = true; + return array_shift($this->changes); + } + + /** + * Get the last change on the stack, false if empty + * + * @return NodeAction|boolean + */ + public function pop() + { + if ($this->isEmpty()) { + return false; + } + + $this->hasBeenModified = true; + return array_pop($this->changes); + } + + /** + * The identifier used for this processes changes in our Session storage + * + * @return string + */ + protected function getSessionKey() + { + return $this->sessionKey; + } + + protected function hasBeenModified() + { + return $this->hasBeenModified; + } + + /** + * @return array + */ + public function serialize() + { + $serialized = array(); + foreach ($this->getChanges() as $change) { + $serialized[] = $change->serialize(); + } + + return $serialized; + } + + /** + * Persist to session on destruction + */ + public function __destruct() + { + if (! $this->hasBeenModified()) { + unset($this->session); + return; + } + $session = $this->session; + $key = $this->getSessionKey(); + if (! $this->isEmpty()) { + $session->set($key, $this->serialize()); + } + unset($this->session); + } +} diff --git a/library/Businessprocess/MonitoredNode.php b/library/Businessprocess/MonitoredNode.php new file mode 100644 index 0000000..3c4167d --- /dev/null +++ b/library/Businessprocess/MonitoredNode.php @@ -0,0 +1,19 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use ipl\Html\Html; + +abstract class MonitoredNode extends Node +{ + abstract public function getUrl(); + + public function getLink() + { + if ($this->isMissing()) { + return Html::tag('a', ['href' => '#'], $this->getAlias()); + } else { + return Html::tag('a', ['href' => $this->getUrl()], $this->getAlias()); + } + } +} diff --git a/library/Businessprocess/MonitoringRestrictions.php b/library/Businessprocess/MonitoringRestrictions.php new file mode 100644 index 0000000..c7d2cef --- /dev/null +++ b/library/Businessprocess/MonitoringRestrictions.php @@ -0,0 +1,65 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Authentication\Auth; +use Icinga\Data\Filter\Filter; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\QueryException; + +class MonitoringRestrictions +{ + /** + * Return a filter for the given restriction + * + * @param string $name Name of the restriction + * + * @return Filter|null Filter object or null if the authenticated user is not restricted + * @throws ConfigurationError If the restriction contains invalid filter columns + */ + public static function getRestriction($name) + { + // Borrowed from Icinga\Module\Monitoring\Controller + $restriction = Filter::matchAny(); + $restriction->setAllowedFilterColumns(array( + 'host_name', + 'hostgroup_name', + 'instance_name', + 'service_description', + 'servicegroup_name', + function ($c) { + return preg_match('/^_(?:host|service)_/i', $c); + } + )); + + foreach (Auth::getInstance()->getRestrictions($name) as $filter) { + if ($filter === '*') { + return Filter::matchAny(); + } + + try { + $restriction->addFilter(Filter::fromQueryString($filter)); + } catch (QueryException $e) { + throw new ConfigurationError( + mt( + 'monitoring', + 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s' + ), + $name, + $filter, + implode(', ', array( + 'instance_name', + 'host_name', + 'hostgroup_name', + 'service_description', + 'servicegroup_name', + '_(host|service)_<customvar-name>' + )), + $e + ); + } + } + + return $restriction; + } +} diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php new file mode 100644 index 0000000..a9eb44c --- /dev/null +++ b/library/Businessprocess/Node.php @@ -0,0 +1,546 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Exception\ProgrammingError; +use ipl\Html\Html; + +abstract class Node +{ + const FLAG_DOWNTIME = 1; + const FLAG_ACK = 2; + const FLAG_MISSING = 4; + const FLAG_NONE = 8; + const SHIFT_FLAGS = 4; + + const ICINGA_OK = 0; + const ICINGA_WARNING = 1; + const ICINGA_CRITICAL = 2; + const ICINGA_UNKNOWN = 3; + const ICINGA_UP = 0; + const ICINGA_DOWN = 1; + const ICINGA_UNREACHABLE = 2; + const ICINGA_PENDING = 99; + const NODE_EMPTY = 128; + + /** @var bool Whether to treat acknowledged hosts/services always as UP/OK */ + protected static $ackIsOk = false; + + /** @var bool Whether to treat hosts/services in downtime always as UP/OK */ + protected static $downtimeIsOk = false; + + protected $sortStateToStateMap = array( + 4 => self::ICINGA_CRITICAL, + 3 => self::ICINGA_UNKNOWN, + 2 => self::ICINGA_WARNING, + 1 => self::ICINGA_PENDING, + 0 => self::ICINGA_OK + ); + + protected $stateToSortStateMap = array( + self::ICINGA_PENDING => 1, + self::ICINGA_UNKNOWN => 3, + self::ICINGA_CRITICAL => 4, + self::ICINGA_WARNING => 2, + self::ICINGA_OK => 0, + self::NODE_EMPTY => 0 + ); + + /** @var string Alias of the node */ + protected $alias; + + /** + * Main business process object + * + * @var BpConfig + */ + protected $bp; + + /** + * Parent nodes + * + * @var array + */ + protected $parents = array(); + + /** + * Node identifier + * + * @var string + */ + protected $name; + + /** + * Node state + * + * @var int + */ + protected $state; + + /** + * Whether this nodes state has been acknowledged + * + * @var bool + */ + protected $ack; + + /** + * Whether this node is in a scheduled downtime + * + * @var bool + */ + protected $downtime; + + // obsolete + protected $duration; + + /** + * This node's icon + * + * @var string + */ + protected $icon; + + /** + * Last state change, unix timestamp + * + * @var int + */ + protected $lastStateChange; + + protected $missing = false; + + protected $empty = false; + + protected $className = 'unknown'; + + protected $stateNames = array( + 'OK', + 'WARNING', + 'CRITICAL', + 'UNKNOWN', + 99 => 'PENDING', + 128 => 'EMPTY' + ); + + /** + * Set whether to treat acknowledged hosts/services always as UP/OK + * + * @param bool $ackIsOk + */ + public static function setAckIsOk($ackIsOk = true) + { + self::$ackIsOk = $ackIsOk; + } + + /** + * Set whether to treat hosts/services in downtime always as UP/OK + * + * @param bool $downtimeIsOk + */ + public static function setDowntimeIsOk($downtimeIsOk = true) + { + self::$downtimeIsOk = $downtimeIsOk; + } + + public function setBpConfig(BpConfig $bp) + { + $this->bp = $bp; + return $this; + } + + public function getBpConfig() + { + return $this->bp; + } + + public function setMissing($missing = true) + { + $this->missing = $missing; + return $this; + } + + public function isProblem() + { + return $this->getState() > 0; + } + + public function hasBeenChanged() + { + return false; + } + + public function isMissing() + { + return $this->missing; + } + + public function hasMissingChildren() + { + return count($this->getMissingChildren()) > 0; + } + + public function getMissingChildren() + { + return array(); + } + + public function hasInfoUrl() + { + return false; + } + + public function setState($state) + { + $this->state = (int) $state; + $this->missing = false; + return $this; + } + + /** + * Forget my state + * + * @return $this + */ + public function clearState() + { + $this->state = null; + return $this; + } + + public function setAck($ack = true) + { + $this->ack = $ack; + return $this; + } + + public function setDowntime($downtime = true) + { + $this->downtime = $downtime; + return $this; + } + + public function getStateName($state = null) + { + $states = $this->enumStateNames(); + if ($state === null) { + return $states[ $this->getState() ]; + } else { + return $states[ $state ]; + } + } + + public function enumStateNames() + { + return $this->stateNames; + } + + public function getState() + { + if ($this->state === null) { + throw new ProgrammingError( + sprintf( + 'Node %s is unable to retrieve it\'s state', + $this->name + ) + ); + } + + return $this->state; + } + + public function getSortingState($state = null) + { + if ($state === null) { + $state = $this->getState(); + } + + if (self::$ackIsOk && $this->isAcknowledged()) { + $state = self::ICINGA_OK; + } + + if (self::$downtimeIsOk && $this->isInDowntime()) { + $state = self::ICINGA_OK; + } + + $sort = $this->stateToSortState($state); + $sort = ($sort << self::SHIFT_FLAGS) + + ($this->isInDowntime() ? self::FLAG_DOWNTIME : 0) + + ($this->isAcknowledged() ? self::FLAG_ACK : 0); + if (! ($sort & (self::FLAG_DOWNTIME | self::FLAG_ACK))) { + $sort |= self::FLAG_NONE; + } + + return $sort; + } + + public function getLastStateChange() + { + return $this->lastStateChange; + } + + public function setLastStateChange($timestamp) + { + $this->lastStateChange = $timestamp; + return $this; + } + + public function addParent(Node $parent) + { + $this->parents[] = $parent; + return $this; + } + + public function getDuration() + { + return $this->duration; + } + + public function isHandled() + { + return $this->isInDowntime() || $this->isAcknowledged(); + } + + public function isInDowntime() + { + if ($this->downtime === null) { + $this->getState(); + } + return $this->downtime; + } + + public function isAcknowledged() + { + if ($this->ack === null) { + $this->getState(); + } + return $this->ack; + } + + public function getChildren($filter = null) + { + return array(); + } + + public function countChildren($filter = null) + { + return count($this->getChildren($filter)); + } + + public function hasChildren($filter = null) + { + return $this->countChildren($filter) > 0; + } + + public function isEmpty() + { + return $this->countChildren() === 0; + } + + public function hasAlias() + { + return $this->alias !== null; + } + + /** + * Get the alias of the node + * + * @return string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Set the alias of the node + * + * @param string $alias + * + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + + return $this; + } + + public function hasParents() + { + return count($this->parents) > 0; + } + + public function hasParentName($name) + { + foreach ($this->getParents() as $parent) { + if ($parent->getName() === $name) { + return true; + } + } + + return false; + } + + public function removeParent($name) + { + $this->parents = array_filter( + $this->parents, + function (BpNode $parent) use ($name) { + return $parent->getName() !== $name; + } + ); + + return $this; + } + + /** + * @return BpNode[] + */ + public function getParents() + { + return $this->parents; + } + + /** + * @param BpConfig $rootConfig + * + * @return array + */ + public function getPaths($rootConfig = null) + { + $differentConfig = false; + if ($rootConfig === null) { + $rootConfig = $this->getBpConfig(); + } else { + $differentConfig = $this->getBpConfig()->getName() !== $rootConfig->getName(); + } + + $paths = []; + foreach ($this->parents as $parent) { + foreach ($parent->getPaths($rootConfig) as $path) { + $path[] = $differentConfig ? $this->getIdentifier() : $this->getName(); + $paths[] = $path; + } + } + + if (! $this instanceof ImportedNode && $this->getBpConfig()->hasRootNode($this->getName())) { + $paths[] = [$differentConfig ? $this->getIdentifier() : $this->getName()]; + } elseif (! $this->hasParents()) { + $paths[] = ['__unbound__', $differentConfig ? $this->getIdentifier() : $this->getName()]; + } + + return $paths; + } + + protected function stateToSortState($state) + { + if (array_key_exists($state, $this->stateToSortStateMap)) { + return $this->stateToSortStateMap[$state]; + } + + throw new ProgrammingError( + 'Got invalid state for node %s: %s', + $this->getName(), + var_export($state, 1) . var_export($this->stateToSortStateMap, 1) + ); + } + + protected function sortStateTostate($sortState) + { + $sortState = $sortState >> self::SHIFT_FLAGS; + if (array_key_exists($sortState, $this->sortStateToStateMap)) { + return $this->sortStateToStateMap[$sortState]; + } + + throw new ProgrammingError('Got invalid sorting state %s', $sortState); + } + + public function getObjectClassName() + { + return $this->className; + } + + public function getLink() + { + return Html::tag('a', ['href' => '#', 'class' => 'toggle'], Html::tag('i', [ + 'class' => 'icon icon-down-dir' + ])); + } + + public function getIcon() + { + return Html::tag('i', ['class' => 'icon icon-' . ($this->icon ?: 'attention-circled')]); + } + + public function operatorHtml() + { + return ' '; + } + + public function getName() + { + return $this->name; + } + + public function getIdentifier() + { + return '@' . $this->getBpConfig()->getName() . ':' . $this->getName(); + } + + public function __toString() + { + return $this->getName(); + } + + public function __destruct() + { + unset($this->parents); + } + + /** + * Export the node to array + * + * @param array $parent The node's parent. Used to construct the path to the node + * @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(array $parent = null, $flat = false) + { + $data = [ + 'name' => $this->getAlias(), + 'state' => $this->getStateName(), + 'since' => $this->getLastStateChange(), + 'in_downtime' => $this->isInDowntime() ? true : false + ]; + + if ($parent !== null) { + $data['path'] = $parent['path'] . '!' . $this->getAlias(); + } else { + $data['path'] = $this->getAlias(); + } + + $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; + } +} diff --git a/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php b/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php new file mode 100644 index 0000000..27f4551 --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb; + +use Icinga\Module\Icingadb\Hook\HostActionsHook; +use Icinga\Module\Icingadb\Model\Host; +use ipl\Web\Widget\Link; + +class HostActions extends HostActionsHook +{ + public function getActionsForObject(Host $host): array + { + $label = mt('businessprocess', 'Business Impact'); + return array( + new Link( + $label, + 'businessprocess/node/impact?name=' + . rawurlencode($host->name . ';Hoststatus') + ) + ); + } +} diff --git a/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php new file mode 100644 index 0000000..1ff37d3 --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php @@ -0,0 +1,10 @@ +<?php + +namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb; + +use Icinga\Module\Icingadb\Hook\IcingadbSupportHook; + +class IcingadbSupport extends IcingadbSupportHook +{ + +} diff --git a/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php b/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php new file mode 100644 index 0000000..24e6829 --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb; + +use Icinga\Module\Icingadb\Hook\ServiceActionsHook; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Web\Widget\Link; + +class ServiceActions extends ServiceActionsHook +{ + public function getActionsForObject(Service $service): array + { + $label = mt('businessprocess', 'Business Impact'); + return array( + new Link( + $label, + sprintf( + 'businessprocess/node/impact?name=%s', + rawurlencode( + sprintf('%s;%s', $service->host->name, $service->name) + ) + ) + ) + ); + } +} diff --git a/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php b/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php new file mode 100644 index 0000000..57ce8f5 --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php @@ -0,0 +1,18 @@ +<?php + +namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring; + +use Icinga\Module\Monitoring\Hook\HostActionsHook; +use Icinga\Module\Monitoring\Object\Host; + +class HostActions extends HostActionsHook +{ + public function getActionsForHost(Host $host) + { + $label = mt('businessprocess', 'Business Impact'); + return array( + $label => 'businessprocess/node/impact?name=' + . rawurlencode($host->getName() . ';Hoststatus') + ); + } +} diff --git a/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php b/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php new file mode 100644 index 0000000..69a93ae --- /dev/null +++ b/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php @@ -0,0 +1,25 @@ +<?php + +namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring; + +use Exception; +use Icinga\Application\Config; +use Icinga\Module\Monitoring\Hook\ServiceActionsHook; +use Icinga\Module\Monitoring\Object\Service; +use Icinga\Web\Url; + +class ServiceActions extends ServiceActionsHook +{ + public function getActionsForService(Service $service) + { + $label = mt('businessprocess', 'Business Impact'); + return array( + $label => sprintf( + 'businessprocess/node/impact?name=%s', + rawurlencode( + sprintf('%s;%s', $service->getHost()->getName(), $service->getName()) + ) + ) + ); + } +} diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php new file mode 100644 index 0000000..56c41aa --- /dev/null +++ b/library/Businessprocess/Renderer/Breadcrumb.php @@ -0,0 +1,78 @@ +<?php + +namespace Icinga\Module\Businessprocess\Renderer; + +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile; +use Icinga\Module\Businessprocess\Web\Url; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class Breadcrumb extends BaseHtmlElement +{ + protected $tag = 'ul'; + + protected $defaultAttributes = array( + 'class' => 'breadcrumb', + 'data-base-target' => '_main' + ); + + /** + * @param Renderer $renderer + * @return static + */ + public static function create(Renderer $renderer) + { + $bp = $renderer->getBusinessProcess(); + $breadcrumb = new static; + $bpUrl = $renderer->getBaseUrl(); + if ($bpUrl->getParam('action') === 'delete') { + $bpUrl->remove('action'); + } + + $breadcrumb->add(Html::tag('li')->add( + Html::tag( + 'a', + [ + 'href' => Url::fromPath('businessprocess'), + 'title' => mt('businessprocess', 'Show Overview') + ], + Html::tag('i', ['class' => 'icon icon-home']) + ) + )); + $breadcrumb->add(Html::tag('li')->add( + Html::tag('a', ['href' => $bpUrl], $bp->getTitle()) + )); + $path = $renderer->getCurrentPath(); + + $parts = array(); + while ($nodeName = array_pop($path)) { + $node = $bp->getNode($nodeName); + $renderer->setParentNode($node); + array_unshift( + $parts, + static::renderNode($node, $path, $renderer) + ); + } + $breadcrumb->add($parts); + + return $breadcrumb; + } + + /** + * @param BpNode $node + * @param array $path + * @param Renderer $renderer + * + * @return NodeTile + */ + protected static function renderNode(BpNode $node, $path, Renderer $renderer) + { + // TODO: something more generic than NodeTile? + $renderer = clone($renderer); + $renderer->lock()->setIsBreadcrumb(); + $p = new NodeTile($renderer, $node, $path); + $p->setTag('li'); + return $p; + } +} diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php new file mode 100644 index 0000000..58e1c4d --- /dev/null +++ b/library/Businessprocess/Renderer/Renderer.php @@ -0,0 +1,398 @@ +<?php + +namespace Icinga\Module\Businessprocess\Renderer; + +use Icinga\Exception\ProgrammingError; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\ImportedNode; +use Icinga\Module\Businessprocess\MonitoredNode; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Web\Url; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; +use ipl\Web\Widget\StateBadge; + +abstract class Renderer extends HtmlDocument +{ + /** @var BpConfig */ + protected $config; + + /** @var BpNode */ + protected $parent; + + /** @var bool Administrative actions are hidden unless unlocked */ + protected $locked = true; + + /** @var Url */ + protected $url; + + /** @var Url */ + protected $baseUrl; + + /** @var array */ + protected $path = array(); + + /** @var bool */ + protected $isBreadcrumb = false; + + /** + * Renderer constructor. + * + * @param BpConfig $config + * @param BpNode|null $parent + */ + public function __construct(BpConfig $config, BpNode $parent = null) + { + $this->config = $config; + $this->parent = $parent; + } + + /** + * @return BpConfig + */ + public function getBusinessProcess() + { + return $this->config; + } + + /** + * Whether this will render all root nodes + * + * @return bool + */ + public function wantsRootNodes() + { + return $this->parent === null; + } + + /** + * Whether this will only render parts of given config + * + * @return bool + */ + public function rendersSubNode() + { + return $this->parent !== null; + } + + public function rendersImportedNode() + { + return $this->parent !== null && $this->parent->getBpConfig()->getName() !== $this->config->getName(); + } + + public function setParentNode(BpNode $node) + { + $this->parent = $node; + return $this; + } + + /** + * @return BpNode + */ + public function getParentNode() + { + return $this->parent; + } + + /** + * @return BpNode[] + */ + public function getParentNodes() + { + if ($this->wantsRootNodes()) { + return array(); + } + + return $this->parent->getParents(); + } + + /** + * @return BpNode[] + */ + public function getChildNodes() + { + if ($this->wantsRootNodes()) { + return $this->config->getRootNodes(); + } else { + return $this->parent->getChildren(); + } + } + + /** + * @return int + */ + public function countChildNodes() + { + if ($this->wantsRootNodes()) { + return $this->config->countChildren(); + } else { + return $this->parent->countChildren(); + } + } + + /** + * @param $summary + * @return BaseHtmlElement + */ + public function renderStateBadges($summary, $totalChildren) + { + $elements = []; + + $itemCount = Html::tag( + 'span', + [ + 'class' => [ + 'item-count', + ] + ], + sprintf(mtp('businessprocess', '%u Child', '%u Children', $totalChildren), $totalChildren) + ); + + $elements[] = array_filter([ + $this->createBadgeGroup($summary, 'CRITICAL'), + $this->createBadgeGroup($summary, 'UNKNOWN'), + $this->createBadgeGroup($summary, 'WARNING'), + $this->createBadge($summary, 'MISSING'), + $this->createBadge($summary, 'PENDING') + ]); + + if (!empty($elements)) { + $container = Html::tag('ul', ['class' => 'state-badges']); + $container->add($itemCount); + foreach ($elements as $element) { + $container->add($element); + } + + return $container; + } + return null; + } + + protected function createBadge($summary, $state) + { + if ($summary[$state] !== 0) { + return Html::tag('li', new StateBadge($summary[$state], strtolower($state))); + } + + return null; + } + + protected function createBadgeGroup($summary, $state) + { + $content = []; + if ($summary[$state] !== 0) { + $content[] = Html::tag('li', new StateBadge($summary[$state], strtolower($state))); + } + + if ($summary[$state . '-HANDLED'] !== 0) { + $content[] = Html::tag('li', new StateBadge($summary[$state . '-HANDLED'], strtolower($state), true)); + } + + if (empty($content)) { + return null; + } + + return Html::tag('li', Html::tag('ul', $content)); + } + + public function getNodeClasses(Node $node) + { + if ($node->isMissing()) { + $classes = array('missing'); + } else { + if ($node->isEmpty() && ! $node instanceof MonitoredNode) { + $classes = array('empty'); + } else { + $classes = [strtolower($node->getStateName( + $this->parent !== null ? $this->parent->getChildState($node) : null + ))]; + } + if ($node->hasMissingChildren()) { + $classes[] = 'missing-children'; + } + } + + if ($node->isHandled()) { + $classes[] = 'handled'; + } + + if ($node instanceof BpNode) { + $classes[] = 'process-node'; + } else { + $classes[] = 'monitored-node'; + } + // TODO: problem? + return $classes; + } + + /** + * Return the url to the given node's source configuration + * + * @param BpNode $node + * + * @return Url + */ + public function getSourceUrl(BpNode $node) + { + if ($node instanceof ImportedNode) { + $name = $node->getNodeName(); + $paths = $node->getBpConfig()->getBpNode($name)->getPaths(); + } else { + $name = $node->getName(); + $paths = $node->getPaths(); + } + + $url = clone $this->getUrl(); + $url->setParams([ + 'config' => $node->getBpConfig()->getName(), + 'node' => $name + ]); + // This depends on the fact that the node's root path is the last element in $paths + $url->getParams()->addValues('path', array_slice(array_pop($paths), 0, -1)); + if (! $this->isLocked()) { + $url->getParams()->add('unlocked', true); + } + + return $url; + } + + /** + * @param Node $node + * @param $path + * @return string + */ + public function getId(Node $node, $path) + { + return md5((empty($path) ? '' : implode(';', $path)) . $node->getName()); + } + + public function setPath(array $path) + { + $this->path = $path; + return $this; + } + + /** + * @return array + */ + public function getPath() + { + return $this->path; + } + + public function getCurrentPath() + { + $path = $this->getPath(); + if ($this->rendersSubNode()) { + $path[] = $this->rendersImportedNode() + ? $this->parent->getIdentifier() + : $this->parent->getName(); + } + + return $path; + } + + /** + * @param Url $url + * @return $this + */ + public function setUrl(Url $url) + { + $this->url = $url->without(array( + 'action', + 'deletenode', + 'deleteparent', + 'editnode', + 'simulationnode', + 'view' + )); + $this->setBaseUrl($this->url); + return $this; + } + + /** + * @param Url $url + * @return $this + */ + protected function setBaseUrl(Url $url) + { + $this->baseUrl = $url->without(array('node', 'path')); + return $this; + } + + public function getUrl() + { + return $this->url; + } + + /** + * @return Url + * @throws ProgrammingError + */ + public function getBaseUrl() + { + if ($this->baseUrl === null) { + throw new ProgrammingError('Renderer has no baseUrl'); + } + + return clone($this->baseUrl); + } + + /** + * @return bool + */ + public function isLocked() + { + return $this->locked; + } + + /** + * @return $this + */ + public function lock() + { + $this->locked = true; + return $this; + } + + /** + * @return $this + */ + public function unlock() + { + $this->locked = false; + return $this; + } + + /** + * TODO: Get rid of this + * + * @return $this + */ + public function setIsBreadcrumb() + { + $this->isBreadcrumb = true; + return $this; + } + + public function isBreadcrumb() + { + return $this->isBreadcrumb; + } + + protected function createUnboundParent(BpConfig $bp) + { + return $bp->getNode('__unbound__'); + } + + /** + * Just to be on the safe side + */ + public function __destruct() + { + unset($this->parent); + unset($this->config); + } +} diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php new file mode 100644 index 0000000..f1f779e --- /dev/null +++ b/library/Businessprocess/Renderer/TileRenderer.php @@ -0,0 +1,90 @@ +<?php + +namespace Icinga\Module\Businessprocess\Renderer; + +use Icinga\Module\Businessprocess\ImportedNode; +use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile; +use Icinga\Module\Businessprocess\Web\Form\CsrfToken; +use ipl\Html\Html; + +class TileRenderer extends Renderer +{ + /** + * @inheritdoc + */ + public function render() + { + $bp = $this->config; + $nodesDiv = Html::tag( + 'div', + [ + 'class' => ['sortable', 'tiles', $this->howMany()], + 'data-base-target' => '_self', + 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists + 'data-csrf-token' => CsrfToken::generate() + ] + ); + + if ($this->wantsRootNodes()) { + $nodesDiv->getAttributes()->add( + 'data-action-url', + $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl() + ); + } else { + $nodeName = $this->parent instanceof ImportedNode + ? $this->parent->getNodeName() + : $this->parent->getName(); + $nodesDiv->getAttributes() + ->add('data-node-name', $nodeName) + ->add('data-action-url', $this->getUrl() + ->with([ + 'config' => $this->parent->getBpConfig()->getName(), + 'node' => $nodeName + ]) + ->getAbsoluteUrl()); + } + + $nodes = $this->getChildNodes(); + + $path = $this->getCurrentPath(); + foreach ($nodes as $name => $node) { + $this->add(new NodeTile($this, $node, $path)); + } + + if ($this->wantsRootNodes()) { + $unbound = $this->createUnboundParent($bp); + if ($unbound->hasChildren()) { + $this->add(new NodeTile($this, $unbound)); + } + } + + $nodesDiv->add($this->getContent()); + $this->setContent($nodesDiv); + + return parent::render(); + } + + /** + * A CSS class giving a rough indication of how many nodes we have + * + * This is used to show larger tiles when there are few and smaller + * ones if there are many. + * + * @return string + */ + protected function howMany() + { + $count = $this->countChildNodes(); + $howMany = 'normal'; + + if ($count <= 6) { + $howMany = 'few'; + } elseif ($count > 12) { + $howMany = 'many'; + } + + return $howMany; + } +} diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php new file mode 100644 index 0000000..67bb4a6 --- /dev/null +++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php @@ -0,0 +1,357 @@ +<?php + +namespace Icinga\Module\Businessprocess\Renderer\TileRenderer; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\HostNode; +use Icinga\Module\Businessprocess\ImportedNode; +use Icinga\Module\Businessprocess\MonitoredNode; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Renderer\Renderer; +use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Web\Url; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Web\Widget\StateBall; + +class NodeTile extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $renderer; + + protected $name; + + protected $node; + + protected $path; + + /** + * @var BaseHtmlElement + */ + private $actions; + + /** + * NodeTile constructor. + * @param Renderer $renderer + * @param $name + * @param Node $node + * @param null $path + */ + public function __construct(Renderer $renderer, Node $node, $path = null) + { + $this->renderer = $renderer; + $this->node = $node; + $this->path = $path; + } + + protected function actions() + { + if ($this->actions === null) { + $this->addActions(); + } + return $this->actions; + } + + protected function addActions() + { + $this->actions = Html::tag( + 'div', + [ + 'class' => 'actions' + ] + ); + + return $this->add($this->actions); + } + + public function render() + { + $renderer = $this->renderer; + $node = $this->node; + + $attributes = $this->getAttributes(); + $attributes->add('class', $renderer->getNodeClasses($node)); + $attributes->add('id', $renderer->getId($node, $this->path)); + if (! $renderer->isLocked()) { + $attributes->add('data-node-name', $node->getName()); + } + + if (! $renderer->isBreadcrumb()) { + $this->addDetailsActions(); + + if (! $renderer->isLocked()) { + $this->addActionLinks(); + } + } + if (! $node instanceof ImportedNode || $node->getBpConfig()->hasNode($node->getName())) { + $link = $this->getMainNodeLink(); + if ($renderer->isBreadcrumb()) { + $state = strtolower($node->getStateName()); + if ($node->isHandled()) { + $state = $state . ' handled'; + } + $link->prepend((new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([ + 'title' => sprintf( + '%s %s', + $state, + DateFormatter::timeSince($node->getLastStateChange()) + ) + ])); + } + + $this->add($link); + } else { + $this->add(Html::tag( + 'a', + Html::tag( + 'span', + ['style' => 'font-size: 75%'], + sprintf('Trying to access a missing business process node "%s"', $node->getNodeName()) + ) + )); + } + + if ($this->renderer->rendersSubNode() + && $this->renderer->getParentNode()->getChildState($node) !== $node->getState() + ) { + $this->add( + (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM)) + ->addAttributes([ + 'class' => 'overridden-state', + 'title' => sprintf( + '%s', + $node->getStateName() + ) + ]) + ); + } + + if ($node instanceof BpNode && !$renderer->isBreadcrumb()) { + $this->add($renderer->renderStateBadges($node->getStateSummary(), $node->countChildren())); + } + + return parent::render(); + } + + protected function getMainNodeUrl(Node $node) + { + if ($node instanceof BpNode) { + return $this->makeBpUrl($node); + } else { + /** @var MonitoredNode $node */ + return $node->getUrl(); + } + } + + protected function buildBaseNodeUrl(Node $node) + { + $url = $this->renderer->getBaseUrl(); + + $p = $url->getParams(); + if ($node instanceof ImportedNode + && $this->renderer->getBusinessProcess()->getName() === $node->getBpConfig()->getName() + ) { + $p->set('node', $node->getNodeName()); + } elseif ($this->renderer->rendersImportedNode()) { + $p->set('node', $node->getIdentifier()); + } else { + $p->set('node', $node->getName()); + } + + if (! empty($this->path)) { + $p->addValues('path', $this->path); + } + + return $url; + } + + protected function makeBpUrl(BpNode $node) + { + return $this->buildBaseNodeUrl($node); + } + + /** + * @return BaseHtmlElement + */ + protected function getMainNodeLink() + { + $node = $this->node; + $url = $this->getMainNodeUrl($node); + if ($node instanceof MonitoredNode) { + $link = Html::tag('a', ['href' => $url, 'data-base-target' => '_next'], $node->getAlias()); + } else { + $link = Html::tag('a', ['href' => $url], $node->getAlias()); + } + + return $link; + } + + protected function addDetailsActions() + { + $node = $this->node; + $url = $this->getMainNodeUrl($node); + + if ($node instanceof BpNode) { + $this->actions()->add(Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tile'), + 'title' => mt('businessprocess', 'Show tiles for this subtree') + ], + Html::tag('i', ['class' => 'icon icon-dashboard']) + ))->add(Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tree'), + 'title' => mt('businessprocess', 'Show this subtree as a tree') + ], + Html::tag('i', ['class' => 'icon icon-sitemap']) + )); + if ($node instanceof ImportedNode) { + if ($node->getBpConfig()->hasNode($node->getName())) { + $this->actions()->add(Html::tag( + 'a', + [ + 'data-base-target' => '_next', + 'href' => $this->renderer->getSourceUrl($node)->getAbsoluteUrl(), + 'title' => mt( + 'businessprocess', + 'Show this process as part of its original configuration' + ) + ], + Html::tag('i', ['class' => 'icon icon-forward']) + )); + } + } + + $url = $node->getInfoUrl(); + + if ($url !== null) { + $link = Html::tag( + 'a', + [ + 'href' => $url, + 'class' => 'node-info', + 'title' => sprintf('%s: %s', mt('businessprocess', 'More information'), $url) + ], + Html::tag('i', ['class' => 'icon icon-info-circled']) + ); + if (preg_match('#^http(?:s)?://#', $url)) { + $link->addAttributes(['target' => '_blank']); + } + $this->actions()->add($link); + } + } else { + // $url = $this->makeMonitoredNodeUrl($node); + if ($node instanceof ServiceNode) { + $this->actions()->add(Html::tag( + 'a', + ['href' => $node->getUrl(), 'data-base-target' => '_next'], + Html::tag('i', ['class' => 'icon icon-service']) + )); + } elseif ($node instanceof HostNode) { + $this->actions()->add(Html::tag( + 'a', + ['href' => $node->getUrl(), 'data-base-target' => '_next'], + Html::tag('i', ['class' => 'icon icon-host']) + )); + } + } + } + + protected function addActionLinks() + { + $parent = $this->renderer->getParentNode(); + if ($parent !== null) { + $baseUrl = Url::fromPath('businessprocess/process/show', [ + 'config' => $parent->getBpConfig()->getName(), + 'node' => $parent instanceof ImportedNode + ? $parent->getNodeName() + : $parent->getName(), + 'unlocked' => true + ]); + } else { + $baseUrl = Url::fromPath('businessprocess/process/show', [ + 'config' => $this->node->getBpConfig()->getName(), + 'unlocked' => true + ]); + } + + if ($this->node instanceof MonitoredNode) { + $this->actions()->add(Html::tag( + 'a', + [ + 'href' => $baseUrl + ->with('action', 'simulation') + ->with('simulationnode', $this->node->getName()), + 'title' => mt( + 'businessprocess', + 'Show the business impact of this node by simulating a specific state' + ) + ], + Html::tag('i', ['class' => 'icon icon-magic']) + )); + + $this->actions()->add(Html::tag( + 'a', + [ + 'href' => $baseUrl + ->with('action', 'editmonitored') + ->with('editmonitorednode', $this->node->getName()), + 'title' => mt('businessprocess', 'Modify this monitored node') + ], + Html::tag('i', ['class' => 'icon icon-edit']) + )); + } + + if ($this->renderer->getBusinessProcess()->getMetadata()->canModify() + && $this->node->getBpConfig()->getName() === $this->renderer->getBusinessProcess()->getName() + && $this->node->getName() !== '__unbound__' + ) { + if ($this->node instanceof BpNode) { + $this->actions()->add(Html::tag( + 'a', + [ + 'href' => $baseUrl + ->with('action', 'edit') + ->with('editnode', $this->node->getName()), + 'title' => mt('businessprocess', 'Modify this business process node') + ], + Html::tag('i', ['class' => 'icon icon-edit']) + )); + + $addUrl = $baseUrl->with([ + 'node' => $this->node->getName(), + 'action' => 'add' + ]); + $addUrl->getParams()->addValues('path', $this->path); + $this->actions()->add(Html::tag( + 'a', + [ + 'href' => $addUrl, + 'title' => mt('businessprocess', 'Add a new sub-node to this business process') + ], + Html::tag('i', ['class' => 'icon icon-plus']) + )); + } + } + + if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()) { + $params = array( + 'action' => 'delete', + 'deletenode' => $this->node->getName(), + ); + + $this->actions()->add(Html::tag( + 'a', + [ + 'href' => $baseUrl->with($params), + 'title' => mt('businessprocess', 'Delete this node') + ], + Html::tag('i', ['class' => 'icon icon-cancel']) + )); + } + } +} diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php new file mode 100644 index 0000000..c71a4f9 --- /dev/null +++ b/library/Businessprocess/Renderer/TreeRenderer.php @@ -0,0 +1,357 @@ +<?php + +namespace Icinga\Module\Businessprocess\Renderer; + +use Icinga\Date\DateFormatter; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\ImportedNode; +use Icinga\Module\Businessprocess\Node; +use Icinga\Module\Businessprocess\Web\Form\CsrfToken; +use Icinga\Module\Icingadb\Model\State; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Web\Widget\StateBall; + +class TreeRenderer extends Renderer +{ + /** + * @inheritdoc + */ + public function render() + { + $bp = $this->config; + $htmlId = $bp->getHtmlId(); + $tree = Html::tag( + 'ul', + [ + 'id' => $htmlId, + 'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'], + 'data-sortable-disabled' => $this->isLocked() ? 'true' : 'false', + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-direction' => 'vertical', + 'data-sortable-group' => json_encode([ + 'name' => $this->wantsRootNodes() ? 'root' : $htmlId, + 'put' => 'function:rowPutAllowed' + ]), + 'data-sortable-invert-swap' => 'true', + 'data-is-root-config' => $this->wantsRootNodes() ? 'true' : 'false', + 'data-csrf-token' => CsrfToken::generate() + ], + $this->renderBp($bp) + ); + if ($this->wantsRootNodes()) { + $tree->getAttributes()->add( + 'data-action-url', + $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl() + ); + } else { + $nodeName = $this->parent instanceof ImportedNode + ? $this->parent->getNodeName() + : $this->parent->getName(); + $tree->getAttributes() + ->add('data-node-name', $nodeName) + ->add('data-action-url', $this->getUrl() + ->with([ + 'config' => $this->parent->getBpConfig()->getName(), + 'node' => $nodeName + ]) + ->getAbsoluteUrl()); + } + + $this->add($tree); + return parent::render(); + } + + /** + * @param BpConfig $bp + * @return string + */ + public function renderBp(BpConfig $bp) + { + $html = array(); + if ($this->wantsRootNodes()) { + $nodes = $bp->getChildren(); + } else { + $nodes = $this->parent->getChildren(); + } + + foreach ($nodes as $name => $node) { + if ($node instanceof BpNode) { + $html[] = $this->renderNode($bp, $node); + } else { + $html[] = $this->renderChild($bp, $this->parent, $node); + } + } + + return $html; + } + + protected function getStateClassNames(Node $node) + { + $state = strtolower($node->getStateName()); + + if ($node->isMissing()) { + return array('missing'); + } elseif ($state === 'ok') { + if ($node->hasMissingChildren()) { + return array('ok', 'missing-children'); + } else { + return array('ok'); + } + } else { + return array('problem', $state); + } + } + + /** + * @param Node $node + * @param array $path + * @param BpNode $parent + * @return BaseHtmlElement[] + */ + public function getNodeIcons(Node $node, array $path = null, BpNode $parent = null) + { + $icons = []; + if (empty($path) && $node instanceof BpNode) { + $icons[] = Html::tag('i', ['class' => 'icon icon-sitemap']); + } else { + $icons[] = $node->getIcon(); + } + $state = strtolower($node->getStateName($parent !== null ? $parent->getChildState($node) : null)); + if ($node->isHandled()) { + $state = $state . ' handled'; + } + $icons[] = (new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([ + 'title' => sprintf( + '%s %s', + $state, + DateFormatter::timeSince($node->getLastStateChange()) + ) + ]); + if ($node->isInDowntime()) { + $icons[] = Html::tag('i', ['class' => 'icon icon-plug']); + } + if ($node->isAcknowledged()) { + $icons[] = Html::tag('i', ['class' => 'icon icon-ok']); + } + return $icons; + } + + public function getOverriddenState($fakeState, Node $node) + { + $overriddenState = Html::tag('div', ['class' => 'overridden-state']); + $overriddenState->add( + (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM)) + ->addAttributes([ + 'title' => sprintf( + '%s', + $node->getStateName() + ) + ]) + ); + $overriddenState->add(Html::tag('i', ['class' => 'icon icon-right-small'])); + $overriddenState->add( + (new StateBall(strtolower($node->getStateName($fakeState)), StateBall::SIZE_MEDIUM)) + ->addAttributes([ + 'title' => sprintf( + '%s', + $node->getStateName($fakeState) + ), + 'class' => 'last' + ]) + ); + + return $overriddenState; + } + + /** + * @param BpConfig $bp + * @param Node $node + * @param array $path + * + * @return string + */ + public function renderNode(BpConfig $bp, Node $node, $path = array()) + { + $htmlId = $this->getId($node, $path); + $li = Html::tag( + 'li', + [ + 'id' => $htmlId, + 'class' => ['bp', 'movable', $node->getObjectClassName()], + 'data-node-name' => $node instanceof ImportedNode + ? $node->getNodeName() + : $node->getName() + ] + ); + $attributes = $li->getAttributes(); + $attributes->add('class', $this->getStateClassNames($node)); + if ($node->isHandled()) { + $attributes->add('class', 'handled'); + } + if ($node instanceof BpNode) { + $attributes->add('class', 'operator'); + } else { + $attributes->add('class', 'node'); + } + + $div = Html::tag('div'); + $li->add($div); + + $div->add($node->getLink()); + $div->add($this->getNodeIcons($node, $path)); + + $div->add(Html::tag('span', null, $node->getAlias())); + + if ($node instanceof BpNode) { + $div->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml())); + } + + if ($node instanceof BpNode && $node->hasInfoUrl()) { + $div->add($this->createInfoAction($node)); + } + + $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName(); + if (! $this->isLocked() && !$differentConfig) { + $div->add($this->getActionIcons($bp, $node)); + } elseif ($differentConfig) { + $div->add($this->actionIcon( + 'forward', + $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(), + mt('businessprocess', 'Show this process as part of its original configuration') + )->addAttributes(['data-base-target' => '_next'])); + } + + $ul = Html::tag('ul', [ + 'class' => ['bp', 'sortable'], + 'data-sortable-disabled' => ($this->isLocked() || $differentConfig) ? 'true' : 'false', + 'data-sortable-invert-swap' => 'true', + 'data-sortable-data-id-attr' => 'id', + 'data-sortable-draggable' => '.movable', + 'data-sortable-direction' => 'vertical', + 'data-sortable-group' => json_encode([ + 'name' => $htmlId, // Unique, so that the function below is the only deciding factor + 'put' => 'function:rowPutAllowed' + ]), + 'data-csrf-token' => CsrfToken::generate(), + 'data-action-url' => $this->getUrl() + ->with([ + 'config' => $node->getBpConfig()->getName(), + 'node' => $node instanceof ImportedNode + ? $node->getNodeName() + : $node->getName() + ]) + ->getAbsoluteUrl() + ]); + $li->add($ul); + + $path[] = $differentConfig ? $node->getIdentifier() : $node->getName(); + foreach ($node->getChildren() as $name => $child) { + if ($child instanceof BpNode) { + $ul->add($this->renderNode($bp, $child, $path)); + } else { + $ul->add($this->renderChild($bp, $node, $child, $path)); + } + } + + return $li; + } + + protected function renderChild($bp, BpNode $parent, Node $node, $path = null) + { + $li = Html::tag('li', [ + 'class' => 'movable', + 'id' => $this->getId($node, $path ?: []), + 'data-node-name' => $node->getName() + ]); + + $li->add($this->getNodeIcons($node, $path, $parent)); + + $link = $node->getLink(); + $link->getAttributes()->set('data-base-target', '_next'); + $li->add($link); + + if (($overriddenState = $parent->getChildState($node)) !== $node->getState()) { + $li->add($this->getOverriddenState($overriddenState, $node)); + } + + if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) { + $li->add($this->getActionIcons($bp, $node)); + } + + return $li; + } + + protected function getActionIcons(BpConfig $bp, Node $node) + { + if ($node instanceof BpNode) { + if ($bp->getMetadata()->canModify()) { + return [$this->createEditAction($bp, $node), $this->renderAddNewNode($node)]; + } else { + return ''; + } + } else { + return $this->createSimulationAction($bp, $node); + } + } + + protected function createEditAction(BpConfig $bp, BpNode $node) + { + return $this->actionIcon( + 'edit', + $this->getUrl()->with(array( + 'action' => 'edit', + 'editnode' => $node->getName() + )), + mt('businessprocess', 'Modify this node') + ); + } + + protected function createSimulationAction(BpConfig $bp, Node $node) + { + return $this->actionIcon( + 'magic', + $this->getUrl()->with(array( + //'config' => $bp->getName(), + 'action' => 'simulation', + 'simulationnode' => $node->getName() + )), + mt('businessprocess', 'Simulate a specific state') + ); + } + + protected function createInfoAction(BpNode $node) + { + $url = $node->getInfoUrl(); + return $this->actionIcon( + 'help', + $url, + sprintf('%s: %s', mt('businessprocess', 'More information'), $url) + )->addAttributes(['target' => '_blank']); + } + + protected function actionIcon($icon, $url, $title) + { + return Html::tag( + 'a', + [ + 'href' => $url, + 'title' => $title, + 'class' => 'action-link' + ], + Html::tag('i', ['class' => 'icon icon-' . $icon]) + ); + } + + protected function renderAddNewNode($parent) + { + return $this->actionIcon( + 'plus', + $this->getUrl() + ->with('action', 'add') + ->with('node', $parent->getName()), + mt('businessprocess', 'Add a new business process node') + ); + } +} diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php new file mode 100644 index 0000000..6160bce --- /dev/null +++ b/library/Businessprocess/ServiceNode.php @@ -0,0 +1,84 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Module\Businessprocess\Web\Url; + +class ServiceNode extends MonitoredNode +{ + protected $hostname; + + /** @var string Alias of the host */ + protected $hostAlias; + + protected $service; + + protected $className = 'service'; + + protected $icon = 'service'; + + public function __construct($object) + { + $this->name = $object->hostname . ';' . $object->service; + $this->hostname = $object->hostname; + $this->service = $object->service; + if (isset($object->state)) { + $this->setState($object->state); + } else { + $this->setState(0)->setMissing(); + } + } + + public function getHostname() + { + return $this->hostname; + } + + /** + * Get the host alias + * + * @return string + */ + public function getHostAlias() + { + return $this->hostAlias; + } + + /** + * Set the host alias + * + * @param string $hostAlias + * + * @return $this + */ + public function setHostAlias($hostAlias) + { + $this->hostAlias = $hostAlias; + + return $this; + } + + public function getServiceDescription() + { + return $this->service; + } + + public function getAlias() + { + return $this->getHostAlias() . ': ' . $this->alias; + } + + public function getUrl() + { + $params = array( + 'host' => $this->getHostname(), + 'service' => $this->getServiceDescription() + ); + + if ($this->getBpConfig()->hasBackendName()) { + $params['backend'] = $this->getBpConfig()->getBackendName(); + } + + return Url::fromPath('businessprocess/service/show', $params); + } +} diff --git a/library/Businessprocess/Simulation.php b/library/Businessprocess/Simulation.php new file mode 100644 index 0000000..1bc9d1d --- /dev/null +++ b/library/Businessprocess/Simulation.php @@ -0,0 +1,185 @@ +<?php + +namespace Icinga\Module\Businessprocess; + +use Icinga\Exception\ProgrammingError; +use Icinga\Web\Session\SessionNamespace; + +class Simulation +{ + const DEFAULT_SESSION_KEY = 'bp-simulations'; + + /** + * @var SessionNamespace + */ + protected $session; + + /** + * @var string + */ + protected $sessionKey; + + /** + * @var array + */ + protected $simulations = array(); + + /** + * Simulation constructor. + * @param array $simulations + */ + public function __construct(array $simulations = array()) + { + $this->simulations = $simulations; + } + + /** + * @param array $simulations + * @return static + */ + public static function create(array $simulations = array()) + { + return new static($simulations); + } + + /** + * @param SessionNamespace $session + * @param null $sessionKey + * @return $this + */ + public static function fromSession(SessionNamespace $session, $sessionKey = null) + { + return static::create() + ->setSessionKey($sessionKey) + ->persistToSession($session); + } + + /** + * @param string $key + * @return $this + */ + public function setSessionKey($key = null) + { + if ($key === null) { + $this->sessionKey = Simulation::DEFAULT_SESSION_KEY; + } else { + $this->sessionKey = $key; + } + + return $this; + } + + /** + * @param SessionNamespace $session + * @return $this + */ + public function persistToSession(SessionNamespace $session) + { + $this->session = $session; + $this->simulations = $this->session->get($this->sessionKey, array()); + return $this; + } + + /** + * @return array + */ + public function simulations() + { + return $this->simulations; + } + + /** + * @param $simulations + * @return $this + */ + protected function setSimulations($simulations) + { + $this->simulations = $simulations; + if ($this->session !== null) { + $this->session->set($this->sessionKey, $simulations); + } + + return $this; + } + + /** + * @return $this + */ + public function clear() + { + $this->simulations = array(); + if ($this->session !== null) { + $this->session->set($this->sessionKey, array()); + } + + return $this; + } + + /** + * @return int + */ + public function count() + { + return count($this->simulations()); + } + + /** + * @return bool + */ + public function isEmpty() + { + return $this->count() === 0; + } + + /** + * @param $node + * @param $properties + */ + public function set($node, $properties) + { + $simulations = $this->simulations(); + $simulations[$node] = $properties; + $this->setSimulations($simulations); + } + + /** + * @param $name + * @return bool + */ + public function hasNode($name) + { + $simulations = $this->simulations(); + return array_key_exists($name, $simulations); + } + + /** + * @param $name + * @return mixed + * @throws ProgrammingError + */ + public function getNode($name) + { + $simulations = $this->simulations(); + if (! array_key_exists($name, $simulations)) { + throw new ProgrammingError('Trying to access invalid node %s', $name); + } + return $simulations[$name]; + } + + /** + * @param $node + * @return bool + */ + public function remove($node) + { + $simulations = $this->simulations(); + if (array_key_exists($node, $simulations)) { + unset($simulations[$node]); + $this->setSimulations($simulations); + + return true; + } else { + return false; + } + } +} diff --git a/library/Businessprocess/State/IcingaDbState.php b/library/Businessprocess/State/IcingaDbState.php new file mode 100644 index 0000000..f33d4a4 --- /dev/null +++ b/library/Businessprocess/State/IcingaDbState.php @@ -0,0 +1,145 @@ +<?php + +namespace Icinga\Module\Businessprocess\State; + +use Exception; +use Icinga\Application\Benchmark; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\IcingaDbObject; +use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Module\Icingadb\Model\Host; +use Icinga\Module\Icingadb\Model\Service; +use ipl\Sql\Connection as IcingaDbConnection; +use ipl\Stdlib\Filter; + +class IcingaDbState +{ + /** @var BpConfig */ + protected $config; + + /** @var IcingaDbConnection */ + protected $backend; + + public function __construct(BpConfig $config) + { + $this->config = $config; + $this->backend = IcingaDbObject::fetchDb(); + } + + public static function apply(BpConfig $config) + { + $self = new static($config); + $self->retrieveStatesFromBackend(); + + return $config; + } + + public function retrieveStatesFromBackend() + { + $config = $this->config; + + try { + $this->reallyRetrieveStatesFromBackend(); + } catch (Exception $e) { + $config->addError( + $config->translate('Could not retrieve process state: %s'), + $e->getMessage() + ); + } + } + + public function reallyRetrieveStatesFromBackend() + { + $config = $this->config; + + Benchmark::measure(sprintf( + 'Retrieving states for business process %s using Icinga DB backend', + $config->getName() + )); + + $hosts = $config->listInvolvedHostNames(); + if (empty($hosts)) { + return $this; + } + + $queryHost = Host::on($this->backend)->with('state'); + IcingaDbObject::applyIcingaDbRestrictions($queryHost); + + $queryHost->filter(Filter::equal('host.name', $hosts)); + + $hostObject = $queryHost->getModel()->getTableName(); + + Benchmark::measure('Retrieved states for ' . $queryHost->count() . ' hosts in ' . $config->getName()); + + $queryService = Service::on($this->backend)->with([ + 'state', + 'host', + 'host.state' + ]); + + $queryService->filter(Filter::equal('host.name', $hosts)); + + IcingaDbObject::applyIcingaDbRestrictions($queryService); + + Benchmark::measure('Retrieved states for ' . $queryService->count() . ' services in ' . $config->getName()); + + $configs = $config->listInvolvedConfigs(); + + $serviceObject = $queryService->getModel()->getTableName(); + + foreach ($configs as $cfg) { + foreach ($queryService as $row) { + $this->handleDbRow($row, $cfg, $serviceObject); + } + foreach ($queryHost as $row) { + $this->handleDbRow($row, $cfg, $hostObject); + } + } + + Benchmark::measure('Got states for business process ' . $config->getName()); + + return $this; + } + + protected function handleDbRow($row, BpConfig $config, $objectName) + { + if ($objectName === 'service') { + $key = $row->host->name . ';' . $row->name; + } else { + $key = $row->name . ';Hoststatus'; + } + + // We fetch more states than we need, so skip unknown ones + if (! $config->hasNode($key)) { + return; + } + + $node = $config->getNode($key); + + if ($this->config->usesHardStates()) { + if ($row->state->hard_state !== null) { + $node->setState($row->state->hard_state)->setMissing(false); + } + } else { + if ($row->state->soft_state !== null) { + $node->setState($row->state->soft_state)->setMissing(false); + } + } + + if ($row->state->last_state_change !== null) { + $node->setLastStateChange($row->state->last_state_change/1000); + } + if ($row->state->in_downtime) { + $node->setDowntime(true); + } + if ($row->state->is_acknowledged) { + $node->setAck(true); + } + + $node->setAlias($row->display_name); + + if ($node instanceof ServiceNode) { + $node->setHostAlias($row->host->display_name); + } + } +} diff --git a/library/Businessprocess/State/MonitoringState.php b/library/Businessprocess/State/MonitoringState.php new file mode 100644 index 0000000..d317528 --- /dev/null +++ b/library/Businessprocess/State/MonitoringState.php @@ -0,0 +1,151 @@ +<?php + +namespace Icinga\Module\Businessprocess\State; + +use Exception; +use Icinga\Application\Benchmark; +use Icinga\Data\Filter\Filter; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\ServiceNode; +use Icinga\Module\Monitoring\Backend\MonitoringBackend; + +class MonitoringState +{ + /** @var BpConfig */ + protected $config; + + /** @var MonitoringBackend */ + protected $backend; + + private function __construct(BpConfig $config) + { + $this->config = $config; + $this->backend = $config->getBackend(); + } + + public static function apply(BpConfig $config) + { + $self = new static($config); + $self->retrieveStatesFromBackend(); + return $config; + } + + public function retrieveStatesFromBackend() + { + $config = $this->config; + + try { + $this->reallyRetrieveStatesFromBackend(); + } catch (Exception $e) { + $config->addError( + $config->translate('Could not retrieve process state: %s'), + $e->getMessage() + ); + } + } + + public function reallyRetrieveStatesFromBackend() + { + $config = $this->config; + + Benchmark::measure('Retrieving states for business process ' . $config->getName()); + $backend = $this->backend; + + if ($config->usesHardStates()) { + $hostStateColumn = 'host_hard_state'; + $hostStateChangeColumn = 'host_last_hard_state_change'; + $serviceStateColumn = 'service_hard_state'; + $serviceStateChangeColumn = 'service_last_hard_state_change'; + } else { + $hostStateColumn = 'host_state'; + $hostStateChangeColumn = 'host_last_state_change'; + $serviceStateColumn = 'service_state'; + $serviceStateChangeColumn = 'service_last_state_change'; + } + + $hosts = $config->listInvolvedHostNames(); + if (empty($hosts)) { + return $this; + } + + $hostFilter = Filter::expression('host_name', '=', $hosts); + + $hostStatus = $backend->select()->from('hostStatus', array( + 'hostname' => 'host_name', + 'last_state_change' => $hostStateChangeColumn, + 'in_downtime' => 'host_in_downtime', + 'ack' => 'host_acknowledged', + 'state' => $hostStateColumn, + 'display_name' => 'host_display_name' + ))->applyFilter($hostFilter)->getQuery()->fetchAll(); + + Benchmark::measure('Retrieved states for ' . count($hostStatus) . ' hosts in ' . $config->getName()); + + // NOTE: we intentionally filter by host_name ONLY + // Tests with host IN ... AND service IN shows longer query times + // while retrieving 1635 (in 5ms) vs. 1388 (in ~430ms) services + $serviceStatus = $backend->select()->from('serviceStatus', array( + 'hostname' => 'host_name', + 'service' => 'service_description', + 'last_state_change' => $serviceStateChangeColumn, + 'in_downtime' => 'service_in_downtime', + 'ack' => 'service_acknowledged', + 'state' => $serviceStateColumn, + 'display_name' => 'service_display_name', + 'host_display_name' => 'host_display_name' + ))->applyFilter($hostFilter)->getQuery()->fetchAll(); + + Benchmark::measure('Retrieved states for ' . count($serviceStatus) . ' services in ' . $config->getName()); + + $configs = $config->listInvolvedConfigs(); + foreach ($configs as $cfg) { + foreach ($serviceStatus as $row) { + $this->handleDbRow($row, $cfg); + } + foreach ($hostStatus as $row) { + $this->handleDbRow($row, $cfg); + } + } + + // TODO: Union, single query? + Benchmark::measure('Got states for business process ' . $config->getName()); + + return $this; + } + + protected function handleDbRow($row, BpConfig $config) + { + $key = $row->hostname; + if (property_exists($row, 'service')) { + $key .= ';' . $row->service; + } else { + $key .= ';Hoststatus'; + } + + // We fetch more states than we need, so skip unknown ones + if (! $config->hasNode($key)) { + return; + } + + $node = $config->getNode($key); + + if ($row->state !== null) { + $node->setState($row->state)->setMissing(false); + } + if ($row->last_state_change !== null) { + $node->setLastStateChange($row->last_state_change); + } + if ((int) $row->in_downtime === 1) { + $node->setDowntime(true); + } + if ((int) $row->ack === 1) { + $node->setAck(true); + } + + $node->setAlias($row->display_name); + + if ($node instanceof ServiceNode) { + $node->setHostAlias($row->host_display_name); + } + } +} 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); +} diff --git a/library/Businessprocess/Test/BaseTestCase.php b/library/Businessprocess/Test/BaseTestCase.php new file mode 100644 index 0000000..807905d --- /dev/null +++ b/library/Businessprocess/Test/BaseTestCase.php @@ -0,0 +1,76 @@ +<?php + +namespace Icinga\Module\Businessprocess\Test; + +use Icinga\Application\Config; +use Icinga\Application\ApplicationBootstrap; +use Icinga\Application\Icinga; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; +use Icinga\Module\Businessprocess\Web\FakeRequest; +use PHPUnit_Framework_TestCase; + +abstract class BaseTestCase extends PHPUnit_Framework_TestCase +{ + /** @var ApplicationBootstrap */ + private static $app; + + /** + * @inheritdoc + */ + public function setUp() + { + $this->app(); + FakeRequest::setConfiguredBaseUrl('/icingaweb2/'); + } + + protected function emptyConfigSection() + { + return Config::module('businessprocess')->getSection('global'); + } + + /*** + * @return BpConfig + */ + protected function makeLoop() + { + return $this->makeInstance()->loadFromString( + 'loop', + "a = b\nb = c\nc = a\nd = a" + ); + } + + /** + * @return LegacyStorage + */ + protected function makeInstance() + { + return new LegacyStorage($this->emptyConfigSection()); + } + + /** + * @param null $subDir + * @return string + */ + protected function getTestsBaseDir($subDir = null) + { + $dir = dirname(dirname(dirname(__DIR__))) . '/test'; + if ($subDir === null) { + return $dir; + } else { + return $dir . '/' . ltrim($subDir, '/'); + } + } + + /** + * @return ApplicationBootstrap + */ + protected function app() + { + if (self::$app === null) { + self::$app = Icinga::app(); + } + + return self::$app; + } +} diff --git a/library/Businessprocess/Test/Bootstrap.php b/library/Businessprocess/Test/Bootstrap.php new file mode 100644 index 0000000..4141c16 --- /dev/null +++ b/library/Businessprocess/Test/Bootstrap.php @@ -0,0 +1,29 @@ +<?php + +namespace Icinga\Module\Businessprocess\Test; + +use Icinga\Application\Cli; + +class Bootstrap +{ + public static function cli($basedir = null) + { + error_reporting(E_ALL | E_STRICT); + if ($basedir === null) { + $basedir = dirname(dirname(dirname(__DIR__))); + } + $testsDir = $basedir . '/test'; + require_once 'Icinga/Application/Cli.php'; + + if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) { + $configDir = $_SERVER['ICINGAWEB_CONFIGDIR']; + } else { + $configDir = $testsDir . '/config'; + } + + Cli::start($testsDir, $configDir) + ->getModuleManager() + ->loadModule('ipl', $basedir . '/vendor/ipl') + ->loadModule('businessprocess', $basedir); + } +} diff --git a/library/Businessprocess/Web/Component/ActionBar.php b/library/Businessprocess/Web/Component/ActionBar.php new file mode 100644 index 0000000..94458dc --- /dev/null +++ b/library/Businessprocess/Web/Component/ActionBar.php @@ -0,0 +1,15 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use ipl\Html\BaseHtmlElement; + +class ActionBar extends BaseHtmlElement +{ + protected $contentSeparator = ' '; + + /** @var string */ + protected $tag = 'div'; + + protected $defaultAttributes = array('class' => 'action-bar'); +} diff --git a/library/Businessprocess/Web/Component/BpDashboardTile.php b/library/Businessprocess/Web/Component/BpDashboardTile.php new file mode 100644 index 0000000..17d3a0c --- /dev/null +++ b/library/Businessprocess/Web/Component/BpDashboardTile.php @@ -0,0 +1,49 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Web\Url; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use ipl\Html\Text; + +class BpDashboardTile extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'dashboard-tile']; + + public function __construct(BpConfig $bp, $title, $description, $icon, $url, $urlParams = null, $attributes = null) + { + if (! isset($attributes['href'])) { + $attributes['href'] = Url::fromPath($url, $urlParams ?: []); + } + + $this->add(Html::tag( + 'div', + ['class' => 'bp-link', 'data-base-target' => '_main'], + Html::tag('a', $attributes, Html::tag('i', ['class' => 'icon icon-' . $icon])) + ->add(Html::tag('span', ['class' => 'header'], $title)) + ->add($description) + )); + + $tiles = Html::tag('div', ['class' => 'bp-root-tiles']); + + foreach ($bp->getChildren() as $node) { + $state = strtolower($node->getStateName()); + + $tiles->add(Html::tag( + 'a', + [ + 'href' => Url::fromPath($url, $urlParams ?: [])->with(['node' => $node->getName()]), + 'class' => "badge state-{$state}", + 'title' => $node->getAlias() + ], + Text::create(' ')->setEscaped() + )); + } + + $this->add($tiles); + } +} diff --git a/library/Businessprocess/Web/Component/Content.php b/library/Businessprocess/Web/Component/Content.php new file mode 100644 index 0000000..6d14197 --- /dev/null +++ b/library/Businessprocess/Web/Component/Content.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use ipl\Html\BaseHtmlElement; + +class Content extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $contentSeparator = "\n"; + + protected $defaultAttributes = array('class' => 'content'); +} diff --git a/library/Businessprocess/Web/Component/Controls.php b/library/Businessprocess/Web/Component/Controls.php new file mode 100644 index 0000000..259cbbb --- /dev/null +++ b/library/Businessprocess/Web/Component/Controls.php @@ -0,0 +1,14 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use ipl\Html\BaseHtmlElement; + +class Controls extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $contentSeparator = "\n"; + + protected $defaultAttributes = array('class' => 'controls'); +} diff --git a/library/Businessprocess/Web/Component/Dashboard.php b/library/Businessprocess/Web/Component/Dashboard.php new file mode 100644 index 0000000..58506df --- /dev/null +++ b/library/Businessprocess/Web/Component/Dashboard.php @@ -0,0 +1,126 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use Icinga\Application\Modules\Module; +use Icinga\Authentication\Auth; +use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport; +use Icinga\Module\Businessprocess\State\IcingaDbState; +use Icinga\Module\Businessprocess\State\MonitoringState; +use Icinga\Module\Businessprocess\Storage\Storage; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class Dashboard extends BaseHtmlElement +{ + /** @var string */ + protected $contentSeparator = "\n"; + + /** @var string */ + protected $tag = 'div'; + + protected $defaultAttributes = array( + 'class' => 'overview-dashboard', + 'data-base-target' => '_next' + ); + + /** @var Auth */ + protected $auth; + + /** @var Storage */ + protected $storage; + + /** + * Dashboard constructor. + * @param Auth $auth + * @param Storage $storage + */ + protected function __construct(Auth $auth, Storage $storage) + { + $this->auth = $auth; + $this->storage = $storage; + // TODO: Auth? + $processes = $storage->listProcessNames(); + $this->add( + Html::tag('h1', null, mt('businessprocess', 'Welcome to your Business Process Overview')) + ); + $this->add(Html::tag( + 'p', + null, + mt( + 'businessprocess', + 'From here you can reach all your defined Business Process' + . ' configurations, create new or modify existing ones' + ) + )); + if ($auth->hasPermission('businessprocess/create')) { + $this->add( + new DashboardAction( + mt('businessprocess', 'Create'), + mt('businessprocess', 'Create a new Business Process configuration'), + 'plus', + 'businessprocess/process/create', + null, + array('class' => 'addnew') + ) + )->add( + new DashboardAction( + mt('businessprocess', 'Upload'), + mt('businessprocess', 'Upload an existing Business Process configuration'), + 'upload', + 'businessprocess/process/upload', + null, + array('class' => 'addnew') + ) + ); + } elseif (empty($processes)) { + $this->add( + Html::tag('div') + ->add(Html::tag('h1', null, mt('businessprocess', 'Not available'))) + ->add(Html::tag('p', null, mt( + 'businessprocess', + 'No Business Process has been defined for you' + ))) + ); + } + + foreach ($processes as $name) { + $meta = $storage->loadMetadata($name); + $title = $meta->get('Title'); + if ($title) { + $title = sprintf('%s (%s)', $title, $name); + } else { + $title = $name; + } + + $bp = $storage->loadProcess($name); + + if (Module::exists('icingadb') && + (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend()) + ) { + IcingaDbState::apply($bp); + } else { + MonitoringState::apply($bp); + } + + $this->add(new BpDashboardTile( + $bp, + $title, + $meta->get('Description'), + 'sitemap', + 'businessprocess/process/show', + array('config' => $name) + )); + } + } + + /** + * @param Auth $auth + * @param Storage $storage + * @return static + */ + public static function create(Auth $auth, Storage $storage) + { + return new static($auth, $storage); + } +} diff --git a/library/Businessprocess/Web/Component/DashboardAction.php b/library/Businessprocess/Web/Component/DashboardAction.php new file mode 100644 index 0000000..5ed1845 --- /dev/null +++ b/library/Businessprocess/Web/Component/DashboardAction.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use Icinga\Web\Url; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class DashboardAction extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = array('class' => 'action'); + + public function __construct($title, $description, $icon, $url, $urlParams = null, $attributes = null) + { + if (! isset($attributes['href'])) { + $attributes['href'] = Url::fromPath($url, $urlParams ?: []); + } + + $this->add(Html::tag('a', $attributes) + ->add(Html::tag('i', ['class' => 'icon icon-' . $icon])) + ->add(Html::tag('span', ['class' => 'header'], $title)) + ->add($description)); + } +} diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php new file mode 100644 index 0000000..6f192dc --- /dev/null +++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php @@ -0,0 +1,148 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use Icinga\Authentication\Auth; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Renderer\Renderer; +use Icinga\Module\Businessprocess\Renderer\TreeRenderer; +use Icinga\Web\Url; +use ipl\Html\Html; + +class RenderedProcessActionBar extends ActionBar +{ + public function __construct(BpConfig $config, Renderer $renderer, Auth $auth, Url $url) + { + $meta = $config->getMetadata(); + + if ($renderer instanceof TreeRenderer) { + $link = Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tile'), + 'title' => mt('businessprocess', 'Switch to Tile view') + ] + ); + } else { + $link = Html::tag( + 'a', + [ + 'href' => $url->with('mode', 'tree'), + 'title' => mt('businessprocess', 'Switch to Tree view') + ] + ); + } + + $link->add([ + Html::tag('i', ['class' => 'icon icon-dashboard' . ($renderer instanceof TreeRenderer ? '' : ' active')]), + Html::tag('i', ['class' => 'icon icon-sitemap' . ($renderer instanceof TreeRenderer ? ' active' : '')]) + ]); + + $this->add( + Html::tag('div', ['class' => 'view-toggle']) + ->add(Html::tag('span', null, mt('businessprocess', 'View'))) + ->add($link) + ); + + $this->add(Html::tag( + 'a', + [ + 'data-base-target' => '_main', + 'href' => $url->with('showFullscreen', true), + 'title' => mt('businessprocess', 'Switch to fullscreen mode'), + 'class' => 'icon-resize-full-alt' + ], + mt('businessprocess', 'Fullscreen') + )); + + $hasChanges = $config->hasSimulations() || $config->hasBeenChanged(); + + if ($renderer->isLocked()) { + if (! $renderer->wantsRootNodes() && $renderer->rendersImportedNode()) { + $span = Html::tag('span', [ + 'class' => 'disabled', + 'title' => mt( + 'businessprocess', + 'Imported processes can only be changed in their original configuration' + ) + ]); + $span->add(Html::tag('i', ['class' => 'icon icon-lock'])) + ->add(mt('businessprocess', 'Editing Locked')); + $this->add($span); + } else { + $this->add(Html::tag( + 'a', + [ + 'href' => $url->with('unlocked', true), + 'title' => mt('businessprocess', 'Click to unlock editing for this process'), + 'class' => 'icon-lock' + ], + mt('businessprocess', 'Unlock Editing') + )); + } + } elseif (! $hasChanges) { + $this->add(Html::tag( + 'a', + [ + 'href' => $url->without('unlocked')->without('action'), + 'title' => mt('businessprocess', 'Click to lock editing for this process'), + 'class' => 'icon-lock-open' + ], + mt('businessprocess', 'Lock Editing') + )); + } + + if (($hasChanges || ! $renderer->isLocked()) && $meta->canModify()) { + if ($renderer->wantsRootNodes()) { + $this->add(Html::tag( + 'a', + [ + 'data-base-target' => '_next', + 'href' => Url::fromPath('businessprocess/process/config', $this->currentProcessParams($url)), + 'title' => mt('businessprocess', 'Modify this process'), + 'class' => 'icon-wrench' + ], + mt('businessprocess', 'Config') + )); + } else { + $this->add(Html::tag( + 'a', + [ + 'href' => $url->with([ + 'action' => 'edit', + 'editnode' => $url->getParam('node') + ])->getAbsoluteUrl(), + 'title' => mt('businessprocess', 'Modify this process'), + 'class' => 'icon-wrench' + ], + mt('businessprocess', 'Config') + )); + } + } + + if (($hasChanges || (! $renderer->isLocked())) && $meta->canModify()) { + $this->add(Html::tag( + 'a', + [ + 'href' => $url->with('action', 'add'), + 'title' => mt('businessprocess', 'Add a new business process node'), + 'class' => 'icon-plus button-link' + ], + mt('businessprocess', 'Add Node') + )); + } + } + + protected function currentProcessParams(Url $url) + { + $urlParams = $url->getParams(); + $params = array(); + foreach (array('config', 'node') as $name) { + if ($value = $urlParams->get($name)) { + $params[$name] = $value; + } + } + + return $params; + } +} diff --git a/library/Businessprocess/Web/Component/Tabs.php b/library/Businessprocess/Web/Component/Tabs.php new file mode 100644 index 0000000..aaa444e --- /dev/null +++ b/library/Businessprocess/Web/Component/Tabs.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use ipl\Html\ValidHtml; + +class Tabs extends WtfTabs implements ValidHtml +{ +} diff --git a/library/Businessprocess/Web/Component/WtfTabs.php b/library/Businessprocess/Web/Component/WtfTabs.php new file mode 100644 index 0000000..8f2250f --- /dev/null +++ b/library/Businessprocess/Web/Component/WtfTabs.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Component; + +use Icinga\Web\Widget\Tabs; + +/** + * Class WtfTabs + * + * TODO: Please remove this as soon as we drop support for PHP 5.3.x + * This works around https://bugs.php.net/bug.php?id=43200 and fixes + * https://github.com/Icinga/icingaweb2-module-businessprocess/issues/81 + * + * @package Icinga\Module\Businessprocess\Web\Component + */ +class WtfTabs extends Tabs +{ + public function render() + { + return parent::render(); + } +} diff --git a/library/Businessprocess/Web/Controller.php b/library/Businessprocess/Web/Controller.php new file mode 100644 index 0000000..e9719e4 --- /dev/null +++ b/library/Businessprocess/Web/Controller.php @@ -0,0 +1,269 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web; + +use Icinga\Application\Icinga; +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\Modification\ProcessChanges; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; +use Icinga\Module\Businessprocess\Storage\Storage; +use Icinga\Module\Businessprocess\Web\Component\ActionBar; +use Icinga\Module\Businessprocess\Web\Component\Controls; +use Icinga\Module\Businessprocess\Web\Component\Content; +use Icinga\Module\Businessprocess\Web\Component\Tabs; +use Icinga\Module\Businessprocess\Web\Form\FormLoader; +use Icinga\Web\Controller as ModuleController; +use Icinga\Web\Notification; +use Icinga\Web\View; +use ipl\Html\Html; + +class Controller extends ModuleController +{ + /** @var View */ + public $view; + + /** @var BpConfig */ + protected $bp; + + /** @var Tabs */ + protected $mytabs; + + /** @var Storage */ + private $storage; + + /** @var bool */ + protected $showFullscreen; + + /** @var Url */ + private $url; + + public function init() + { + $m = Icinga::app()->getModuleManager(); + if (! $m->hasLoaded('monitoring') && $m->hasInstalled('monitoring')) { + $m->loadModule('monitoring'); + } + $this->controls(); + $this->content(); + $this->url(); + $this->view->showFullscreen + = $this->showFullscreen + = (bool) $this->_helper->layout()->showFullscreen; + + $this->setViewScript('default'); + } + + /** + * @return Url + */ + protected function url() + { + if ($this->url === null) { + $this->url = Url::fromPath( + $this->getRequest()->getUrl()->getPath() + )->setParams($this->getRequest()->getUrl()->getParams()); + } + + return $this->url; + } + + /** + * @return ActionBar + */ + protected function actions() + { + if ($this->view->actions === null) { + $this->view->actions = new ActionBar(); + } + + return $this->view->actions; + } + + /** + * @return Controls + */ + protected function controls() + { + if ($this->view->controls === null) { + $controls = $this->view->controls = new Controls(); + if ($this->view->compact) { + $controls->getAttributes()->add('class', 'compact'); + } + } + + return $this->view->controls; + } + + /** + * @return Content + */ + protected function content() + { + if ($this->view->content === null) { + $content = $this->view->content = new Content(); + if ($this->view->compact) { + $content->getAttributes()->add('class', 'compact'); + } + } + + return $this->view->content; + } + + /** + * @param $label + * @return Tabs + */ + protected function singleTab($label) + { + return $this->tabs()->add( + 'tab', + array( + 'label' => $label, + 'url' => $this->getRequest()->getUrl() + ) + )->activate('tab'); + } + + /** + * @return Tabs + */ + protected function defaultTab() + { + return $this->singleTab($this->translate('Business Process')); + } + + /** + * @return Tabs + */ + protected function overviewTab() + { + return $this->tabs()->add( + 'overview', + array( + 'label' => $this->translate('Business Process'), + 'url' => 'businessprocess' + ) + )->activate('overview'); + } + + /** + * @return Tabs + */ + protected function tabs() + { + // Todo: do not add to view once all of them render controls() + if ($this->mytabs === null) { + $tabs = new Tabs(); + //$this->controls()->add($tabs); + $this->mytabs = $tabs; + } + + return $this->mytabs; + } + + protected function session() + { + return $this->Window()->getSessionNamespace('businessprocess'); + } + + protected function setViewScript($name) + { + $this->_helper->viewRenderer->setNoController(true); + $this->_helper->viewRenderer->setScriptAction($name); + return $this; + } + + protected function setTitle($title) + { + $args = func_get_args(); + array_shift($args); + $this->view->title = vsprintf($title, $args); + return $this; + } + + protected function addTitle($title) + { + $args = func_get_args(); + array_shift($args); + $this->view->title = vsprintf($title, $args); + $this->controls()->add(Html::tag('h1', null, $this->view->title)); + return $this; + } + + protected function loadModifiedBpConfig() + { + $bp = $this->loadBpConfig(); + $changes = ProcessChanges::construct($bp, $this->session()); + if ($this->params->get('dismissChanges')) { + Notification::success( + sprintf( + $this->translate('%d pending change(s) have been dropped'), + $changes->count() + ) + ); + $changes->clear(); + $this->redirectNow($this->url()->without('dismissChanges')->without('unlocked')); + } + $bp->applyChanges($changes); + return $bp; + } + + protected function doNotRender() + { + $this->_helper->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + return $this; + } + + protected function loadBpConfig() + { + $name = $this->params->get('config'); + $storage = $this->storage(); + + if (! $storage->hasProcess($name)) { + $this->httpNotFound( + $this->translate('No such process config: "%s"'), + $name + ); + } + + $modifications = $this->session()->get('modifications', array()); + if (array_key_exists($name, $modifications)) { + $bp = $storage->loadFromString($name, $modifications[$name]); + } else { + $bp = $storage->loadProcess($name); + } + + // allow URL parameter to override configured state type + if (null !== ($stateType = $this->params->get('state_type'))) { + if ($stateType === 'soft') { + $bp->useSoftStates(); + } + if ($stateType === 'hard') { + $bp->useHardStates(); + } + } + + $this->view->bpconfig = $this->bp = $bp; + $this->view->configName = $bp->getName(); + + return $bp; + } + + public function loadForm($name) + { + return FormLoader::load($name, $this->Module()); + } + + /** + * @return LegacyStorage|Storage + */ + protected function storage() + { + if ($this->storage === null) { + $this->storage = LegacyStorage::getInstance(); + } + + return $this->storage; + } +} diff --git a/library/Businessprocess/Web/FakeRequest.php b/library/Businessprocess/Web/FakeRequest.php new file mode 100644 index 0000000..4e54117 --- /dev/null +++ b/library/Businessprocess/Web/FakeRequest.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web; + +use Icinga\Exception\ProgrammingError; +use Icinga\Web\Request; + +class FakeRequest extends Request +{ + /** @var string */ + private static $baseUrl; + + public static function setConfiguredBaseUrl($url) + { + self::$baseUrl = $url; + } + + public function getBaseUrl($raw = false) + { + if (self::$baseUrl === null) { + throw new ProgrammingError('Cannot determine base URL on CLI if not configured'); + } else { + return self::$baseUrl; + } + } +} diff --git a/library/Businessprocess/Web/Form/BpConfigBaseForm.php b/library/Businessprocess/Web/Form/BpConfigBaseForm.php new file mode 100644 index 0000000..ddfc851 --- /dev/null +++ b/library/Businessprocess/Web/Form/BpConfigBaseForm.php @@ -0,0 +1,72 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form; + +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Authentication\Auth; +use Icinga\Module\Businessprocess\Storage\LegacyStorage; +use Icinga\Module\Businessprocess\BpConfig; + +abstract class BpConfigBaseForm extends QuickForm +{ + /** @var LegacyStorage */ + protected $storage; + + /** @var BpConfig */ + protected $config; + + protected function listAvailableBackends() + { + $keys = []; + $moduleManager = Icinga::app()->getModuleManager(); + if ($moduleManager->hasEnabled('monitoring')) { + $keys = array_keys(Config::module('monitoring', 'backends')->toArray()); + $keys = array_combine($keys, $keys); + } + + return $keys; + } + + public function setStorage(LegacyStorage $storage) + { + $this->storage = $storage; + return $this; + } + + public function setProcessConfig(BpConfig $config) + { + $this->config = $config; + return $this; + } + + protected function prepareMetadata(BpConfig $config) + { + $meta = $config->getMetadata(); + $auth = Auth::getInstance(); + $meta->set('Owner', $auth->getUser()->getUsername()); + + if ($auth->hasPermission('businessprocess/showall')) { + return true; + } + + $prefixes = $auth->getRestrictions('businessprocess/prefix'); + if (! empty($prefixes) && ! $meta->nameIsPrefixedWithOneOf($prefixes)) { + if (count($prefixes) === 1) { + $this->getElement('name')->addError(sprintf( + $this->translate('Please prefix the name with "%s"'), + current($prefixes) + )); + } else { + $this->getElement('name')->addError(sprintf( + $this->translate('Please prefix the name with one of "%s"'), + implode('", "', $prefixes) + )); + } + + return false; + } + + return true; + } +} diff --git a/library/Businessprocess/Web/Form/CsrfToken.php b/library/Businessprocess/Web/Form/CsrfToken.php new file mode 100644 index 0000000..9eb24ef --- /dev/null +++ b/library/Businessprocess/Web/Form/CsrfToken.php @@ -0,0 +1,53 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form; + +class CsrfToken +{ + /** + * Check whether the given token is valid + * + * @param string $token Token + * + * @return bool + */ + public static function isValid($token) + { + if (strpos($token, '|') === false) { + return false; + } + + list($seed, $token) = explode('|', $token); + + if (!is_numeric($seed)) { + return false; + } + + return $token === hash('sha256', self::getSessionId() . $seed); + } + + /** + * Create a new token + * + * @return string + */ + public static function generate() + { + $seed = mt_rand(); + $token = hash('sha256', self::getSessionId() . $seed); + + return sprintf('%s|%s', $seed, $token); + } + + /** + * Get current session id + * + * TODO: we should do this through our App or Session object + * + * @return string + */ + protected static function getSessionId() + { + return session_id(); + } +} diff --git a/library/Businessprocess/Web/Form/Element/Checkbox.php b/library/Businessprocess/Web/Form/Element/Checkbox.php new file mode 100644 index 0000000..7975b82 --- /dev/null +++ b/library/Businessprocess/Web/Form/Element/Checkbox.php @@ -0,0 +1,8 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form\Element; + +class Checkbox extends \Icinga\Web\Form\Element\Checkbox +{ + +} diff --git a/library/Businessprocess/Web/Form/Element/FormElement.php b/library/Businessprocess/Web/Form/Element/FormElement.php new file mode 100644 index 0000000..7647a5e --- /dev/null +++ b/library/Businessprocess/Web/Form/Element/FormElement.php @@ -0,0 +1,9 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form\Element; + +use Zend_Form_Element_Xhtml; + +class FormElement extends Zend_Form_Element_Xhtml +{ +} diff --git a/library/Businessprocess/Web/Form/Element/SimpleNote.php b/library/Businessprocess/Web/Form/Element/SimpleNote.php new file mode 100644 index 0000000..9f757f2 --- /dev/null +++ b/library/Businessprocess/Web/Form/Element/SimpleNote.php @@ -0,0 +1,22 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form\Element; + +class SimpleNote extends FormElement +{ + public $helper = 'formSimpleNote'; + + /** + * Always ignore this element + * @codingStandardsIgnoreStart + * + * @var boolean + */ + protected $_ignore = true; + // @codingStandardsIgnoreEnd + + public function isValid($value, $context = null) + { + return true; + } +} diff --git a/library/Businessprocess/Web/Form/Element/StateOverrides.php b/library/Businessprocess/Web/Form/Element/StateOverrides.php new file mode 100644 index 0000000..c2216c0 --- /dev/null +++ b/library/Businessprocess/Web/Form/Element/StateOverrides.php @@ -0,0 +1,55 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form\Element; + +class StateOverrides extends FormElement +{ + public $helper = 'formStateOverrides'; + + /** @var array The overridable states */ + protected $states; + + /** + * Set the overridable states + * + * @param array $states + * + * @return $this + */ + public function setStates(array $states) + { + $this->states = $states; + + return $this; + } + + /** + * Get the overridable states + * + * @return array + */ + public function getStates() + { + return $this->states; + } + + public function init() + { + $this->setIsArray(true); + } + + public function setValue($value) + { + $cleanedValue = []; + + if (! empty($value)) { + foreach ($value as $from => $to) { + if ((int) $from !== (int) $to) { + $cleanedValue[$from] = $to; + } + } + } + + return parent::setValue($cleanedValue); + } +} diff --git a/library/Businessprocess/Web/Form/FormLoader.php b/library/Businessprocess/Web/Form/FormLoader.php new file mode 100644 index 0000000..965da4b --- /dev/null +++ b/library/Businessprocess/Web/Form/FormLoader.php @@ -0,0 +1,37 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form; + +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; +use Icinga\Exception\ProgrammingError; + +class FormLoader +{ + public static function load($name, Module $module = null) + { + if ($module === null) { + $basedir = Icinga::app()->getApplicationDir('forms'); + $ns = '\\Icinga\\Web\\Forms\\'; + } else { + $basedir = $module->getFormDir(); + $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Forms\\'; + } + if (preg_match('~^[a-z0-9/]+$~i', $name)) { + $parts = preg_split('~/~', $name); + $class = ucfirst(array_pop($parts)) . 'Form'; + $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class); + if (file_exists($file)) { + require_once($file); + $class = $ns . $class; + $options = array(); + if ($module !== null) { + $options['icingaModule'] = $module; + } + + return new $class($options); + } + } + throw new ProgrammingError(sprintf('Cannot load %s (%s), no such form', $name, $file)); + } +} diff --git a/library/Businessprocess/Web/Form/QuickBaseForm.php b/library/Businessprocess/Web/Form/QuickBaseForm.php new file mode 100644 index 0000000..3ef7b66 --- /dev/null +++ b/library/Businessprocess/Web/Form/QuickBaseForm.php @@ -0,0 +1,167 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form; + +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; +use ipl\Html\ValidHtml; +use Zend_Form; + +abstract class QuickBaseForm extends Zend_Form implements ValidHtml +{ + /** + * The Icinga module this form belongs to. Usually only set if the + * form is initialized through the FormLoader + * + * @var Module + */ + protected $icingaModule; + + protected $icingaModuleName; + + private $hintCount = 0; + + public function __construct($options = null) + { + $this->callZfConstructor($this->handleOptions($options)) + ->initializePrefixPaths(); + } + + protected function callZfConstructor($options = null) + { + parent::__construct($options); + return $this; + } + + protected function initializePrefixPaths() + { + $this->addPrefixPathsForBusinessprocess(); + if ($this->icingaModule && $this->icingaModuleName !== 'businessprocess') { + $this->addPrefixPathsForModule($this->icingaModule); + } + } + + protected function addPrefixPathsForBusinessprocess() + { + $module = Icinga::app() + ->getModuleManager() + ->loadModule('businessprocess') + ->getModule('businessprocess'); + + $this->addPrefixPathsForModule($module); + } + + public function addPrefixPathsForModule(Module $module) + { + $basedir = sprintf( + '%s/%s/Web/Form', + $module->getLibDir(), + ucfirst($module->getName()) + ); + + $this->addPrefixPaths(array( + array( + 'prefix' => __NAMESPACE__ . '\\Element\\', + 'path' => $basedir . '/Element', + 'type' => static::ELEMENT + ) + )); + + return $this; + } + + public function addHidden($name, $value = null) + { + $this->addElement('hidden', $name); + $el = $this->getElement($name); + $el->setDecorators(array('ViewHelper')); + if ($value !== null) { + $this->setDefault($name, $value); + $el->setValue($value); + } + + return $this; + } + + // TODO: Should be an element + public function addHtmlHint($html, $options = array()) + { + return $this->addHtml('<div class="hint">' . $html . '</div>', $options); + } + + public function addHtml($html, $options = array()) + { + if (array_key_exists('name', $options)) { + $name = $options['name']; + unset($options['name']); + } else { + $name = '_HINT' . ++$this->hintCount; + } + + $this->addElement('simpleNote', $name, $options); + $this->getElement($name) + ->setValue($html) + ->setIgnore(true) + ->setDecorators(array('ViewHelper')); + + return $this; + } + + public function optionalEnum($enum, $nullLabel = null) + { + if ($nullLabel === null) { + $nullLabel = $this->translate('- please choose -'); + } + + return array(null => $nullLabel) + $enum; + } + + protected function handleOptions($options = null) + { + if ($options === null) { + return $options; + } + + if (array_key_exists('icingaModule', $options)) { + /** @var Module icingaModule */ + $this->icingaModule = $options['icingaModule']; + $this->icingaModuleName = $this->icingaModule->getName(); + unset($options['icingaModule']); + } + + return $options; + } + + public function setIcingaModule(Module $module) + { + $this->icingaModule = $module; + return $this; + } + + protected function loadForm($name, Module $module = null) + { + if ($module === null) { + $module = $this->icingaModule; + } + + return FormLoader::load($name, $module); + } + + protected function valueIsEmpty($value) + { + if (is_array($value)) { + return empty($value); + } + + return strlen($value) === 0; + } + + public function translate($string) + { + if ($this->icingaModuleName === null) { + return t($string); + } else { + return mt($this->icingaModuleName, $string); + } + } +} diff --git a/library/Businessprocess/Web/Form/QuickForm.php b/library/Businessprocess/Web/Form/QuickForm.php new file mode 100644 index 0000000..c39b34b --- /dev/null +++ b/library/Businessprocess/Web/Form/QuickForm.php @@ -0,0 +1,502 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form; + +use Icinga\Application\Icinga; +use Icinga\Exception\ProgrammingError; +use Icinga\Web\Notification; +use Icinga\Web\Request; +use Icinga\Web\Response; +use Icinga\Web\Url; +use Exception; + +/** + * QuickForm wants to be a base class for simple forms + */ +abstract class QuickForm extends QuickBaseForm +{ + const ID = '__FORM_NAME'; + + const CSRF = '__FORM_CSRF'; + + /** + * The name of this form + */ + protected $formName; + + /** + * Whether the form has been sent + */ + protected $hasBeenSent; + + /** + * Whether the form has been sent + */ + protected $hasBeenSubmitted; + + /** + * The submit caption, element - still tbd + */ + // protected $submit; + + /** + * Our request + */ + protected $request; + + /** + * @var Url + */ + protected $successUrl; + + protected $successMessage; + + protected $submitLabel; + + protected $submitButtonName; + + protected $deleteButtonName; + + protected $fakeSubmitButtonName; + + /** + * Whether form elements have already been created + */ + protected $didSetup = false; + + protected $isApiRequest = false; + + public function __construct($options = null) + { + parent::__construct($options); + + $this->setMethod('post'); + $this->getActionFromRequest() + ->createIdElement() + ->regenerateCsrfToken() + ->setPreferredDecorators(); + } + + protected function getActionFromRequest() + { + $this->setAction(Url::fromRequest()); + return $this; + } + + protected function setPreferredDecorators() + { + $this->setAttrib('class', 'autofocus icinga-controls'); + $this->setDecorators( + array( + 'Description', + array('FormErrors', array('onlyCustomFormErrors' => true)), + 'FormElements', + 'Form' + ) + ); + + return $this; + } + + protected function addSubmitButtonIfSet() + { + if (false === ($label = $this->getSubmitLabel())) { + return; + } + + if ($this->submitButtonName && $el = $this->getElement($this->submitButtonName)) { + return; + } + + $el = $this->createElement('submit', $label) + ->setLabel($label) + ->setDecorators(array('ViewHelper')); + $this->submitButtonName = $el->getName(); + $this->addElement($el); + + $fakeEl = $this->createElement('submit', '_FAKE_SUBMIT') + ->setLabel($label) + ->setDecorators(array('ViewHelper')); + $this->fakeSubmitButtonName = $fakeEl->getName(); + $this->addElement($fakeEl); + + $this->addDisplayGroup( + array($this->fakeSubmitButtonName), + 'fake_button', + array( + 'decorators' => array('FormElements'), + 'order' => 1, + ) + ); + + $grp = array( + $this->submitButtonName, + $this->deleteButtonName + ); + $this->addDisplayGroup($grp, 'buttons', array( + 'decorators' => array( + 'FormElements', + array('HtmlTag', array('tag' => 'dl')), + 'DtDdWrapper', + ), + 'order' => 1000, + )); + } + + protected function addSimpleDisplayGroup($elements, $name, $options) + { + if (! array_key_exists('decorators', $options)) { + $options['decorators'] = array( + 'FormElements', + array('HtmlTag', array('tag' => 'dl')), + 'Fieldset', + ); + } + + return $this->addDisplayGroup($elements, $name, $options); + } + + protected function createIdElement() + { + $this->detectName(); + $this->addHidden(self::ID, $this->getName()); + $this->getElement(self::ID)->setIgnore(true); + return $this; + } + + public function getSentValue($name, $default = null) + { + $request = $this->getRequest(); + if ($request->isPost() && $this->hasBeenSent()) { + return $request->getPost($name); + } else { + return $default; + } + } + + public function getSubmitLabel() + { + if ($this->submitLabel === null) { + return $this->translate('Submit'); + } + + return $this->submitLabel; + } + + public function setSubmitLabel($label) + { + $this->submitLabel = $label; + return $this; + } + + public function setApiRequest($isApiRequest = true) + { + $this->isApiRequest = $isApiRequest; + return $this; + } + + public function isApiRequest() + { + return $this->isApiRequest; + } + + public function regenerateCsrfToken() + { + if (! $element = $this->getElement(self::CSRF)) { + $this->addHidden(self::CSRF, CsrfToken::generate()); + $element = $this->getElement(self::CSRF); + } + $element->setIgnore(true); + + return $this; + } + + public function removeCsrfToken() + { + $this->removeElement(self::CSRF); + return $this; + } + + public function setSuccessUrl($url, $params = null) + { + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + if ($params !== null) { + $url->setParams($params); + } + $this->successUrl = $url; + return $this; + } + + public function getSuccessUrl() + { + $url = $this->successUrl ?: $this->getAction(); + if (! $url instanceof Url) { + $url = Url::fromPath($url); + } + + return $url; + } + + protected function beforeSetup() + { + } + + public function setup() + { + } + + protected function onSetup() + { + } + + public function setAction($action) + { + if ($action instanceof Url) { + $action = $action->getAbsoluteUrl('&'); + } + + return parent::setAction($action); + } + + public function hasBeenSubmitted() + { + if ($this->hasBeenSubmitted === null) { + $req = $this->getRequest(); + if ($req->isPost()) { + if (! $this->hasSubmitButton()) { + return $this->hasBeenSubmitted = $this->hasBeenSent(); + } + + $this->hasBeenSubmitted = $this->pressedButton( + $this->fakeSubmitButtonName, + $this->getSubmitLabel() + ) || $this->pressedButton( + $this->submitButtonName, + $this->getSubmitLabel() + ); + } else { + $this->hasBeenSubmitted = false; + } + } + + return $this->hasBeenSubmitted; + } + + protected function hasSubmitButton() + { + return $this->submitButtonName !== null; + } + + protected function pressedButton($name, $label) + { + $req = $this->getRequest(); + if (! $req->isPost()) { + return false; + } + + $req = $this->getRequest(); + $post = $req->getPost(); + + return array_key_exists($name, $post) + && $post[$name] === $label; + } + + protected function beforeValidation($data = array()) + { + } + + public function prepareElements() + { + if (! $this->didSetup) { + $this->beforeSetup(); + $this->setup(); + $this->addSubmitButtonIfSet(); + $this->onSetup(); + $this->didSetup = true; + } + + return $this; + } + + public function handleRequest(Request $request = null) + { + if ($request === null) { + $request = $this->getRequest(); + } else { + $this->setRequest($request); + } + + $this->prepareElements(); + + if ($this->hasBeenSent()) { + $post = $request->getPost(); + if ($this->hasBeenSubmitted()) { + $this->beforeValidation($post); + if ($this->isValid($post)) { + try { + $this->onSuccess(); + } catch (Exception $e) { + $this->addException($e); + $this->onFailure(); + } + } else { + $this->onFailure(); + } + } else { + $this->setDefaults($post); + } + } else { + // Well... + } + + return $this; + } + + public function addException(Exception $e, $elementName = null) + { + $file = preg_split('/[\/\\\]/', $e->getFile(), -1, PREG_SPLIT_NO_EMPTY); + $file = array_pop($file); + $msg = sprintf( + '%s (%s:%d)', + $e->getMessage(), + $file, + $e->getLine() + ); + + if ($el = $this->getElement($elementName)) { + $el->addError($msg); + } else { + $this->addError($msg); + } + } + + public function onSuccess() + { + $this->redirectOnSuccess(); + } + + public function setSuccessMessage($message) + { + $this->successMessage = $message; + return $this; + } + + public function getSuccessMessage($message = null) + { + if ($message !== null) { + return $message; + } + if ($this->successMessage === null) { + return t('Form has successfully been sent'); + } + return $this->successMessage; + } + + public function redirectOnSuccess($message = null) + { + if ($this->isApiRequest()) { + // TODO: Set the status line message? + $this->successMessage = $this->getSuccessMessage($message); + return; + } + + $url = $this->getSuccessUrl(); + $this->notifySuccess($this->getSuccessMessage($message)); + $this->redirectAndExit($url); + } + + public function onFailure() + { + } + + public function notifySuccess($message = null) + { + if ($message === null) { + $message = t('Form has successfully been sent'); + } + Notification::success($message); + return $this; + } + + public function notifyError($message) + { + Notification::error($message); + return $this; + } + + protected function redirectAndExit($url) + { + /** @var Response $response */ + $response = Icinga::app()->getFrontController()->getResponse(); + $response->redirectAndExit($url); + } + + protected function setHttpResponseCode($code) + { + Icinga::app()->getFrontController()->getResponse()->setHttpResponseCode($code); + return $this; + } + + protected function onRequest() + { + } + + public function setRequest(Request $request) + { + if ($this->request !== null) { + throw new ProgrammingError('Unable to set request twice'); + } + + $this->request = $request; + $this->prepareElements(); + $this->onRequest(); + return $this; + } + + /** + * @return Request + */ + public function getRequest() + { + if ($this->request === null) { + /** @var Request $request */ + $request = Icinga::app()->getFrontController()->getRequest(); + $this->setRequest($request); + } + return $this->request; + } + + public function hasBeenSent() + { + if ($this->hasBeenSent === null) { + + /** @var Request $req */ + if ($this->request === null) { + $req = Icinga::app()->getFrontController()->getRequest(); + } else { + $req = $this->request; + } + + if ($req->isPost()) { + $post = $req->getPost(); + $this->hasBeenSent = array_key_exists(self::ID, $post) && + $post[self::ID] === $this->getName(); + } else { + $this->hasBeenSent = false; + } + } + + return $this->hasBeenSent; + } + + protected function detectName() + { + if ($this->formName !== null) { + $this->setName($this->formName); + } else { + $this->setName(get_class($this)); + } + } +} diff --git a/library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php b/library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php new file mode 100644 index 0000000..9676de0 --- /dev/null +++ b/library/Businessprocess/Web/Form/Validator/NoDuplicateChildrenValidator.php @@ -0,0 +1,57 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web\Form\Validator; + +use Icinga\Module\Businessprocess\BpConfig; +use Icinga\Module\Businessprocess\BpNode; +use Icinga\Module\Businessprocess\Forms\EditNodeForm; +use Icinga\Module\Businessprocess\Web\Form\QuickForm; +use Zend_Validate_Abstract; + +class NoDuplicateChildrenValidator extends Zend_Validate_Abstract +{ + const CHILD_FOUND = 'childFound'; + + /** @var QuickForm */ + protected $form; + + /** @var BpConfig */ + protected $bp; + + /** @var BpNode */ + protected $parent; + + /** @var string */ + protected $label; + + public function __construct(QuickForm $form, BpConfig $bp, BpNode $parent = null) + { + $this->form = $form; + $this->bp = $bp; + $this->parent = $parent; + + $this->_messageVariables['label'] = 'label'; + $this->_messageTemplates = [ + self::CHILD_FOUND => mt('businessprocess', '%label% is already defined in this process') + ]; + } + + public function isValid($value) + { + if ($this->parent === null) { + $found = $this->bp->hasRootNode($value); + } elseif ($this->form instanceof EditNodeForm && $this->form->getNode()->getName() === $value) { + $found = false; + } else { + $found = $this->parent->hasChild($value); + } + + if (! $found) { + return true; + } + + $this->label = $this->form->getElement('children')->getMultiOptions()[$value]; + $this->_error(self::CHILD_FOUND); + return false; + } +} diff --git a/library/Businessprocess/Web/Url.php b/library/Businessprocess/Web/Url.php new file mode 100644 index 0000000..3c036d4 --- /dev/null +++ b/library/Businessprocess/Web/Url.php @@ -0,0 +1,26 @@ +<?php + +namespace Icinga\Module\Businessprocess\Web; + +use Icinga\Application\Icinga; +use Icinga\Web\Url as WebUrl; + +/** + * Class Url + * + * The main purpose of this class is to get unit tests running on CLI + * + * @package Icinga\Module\Businessprocess\Web + */ +class Url extends WebUrl +{ + protected static function getRequest() + { + $app = Icinga::app(); + if ($app->isCli()) { + return new FakeRequest(); + } else { + return $app->getRequest(); + } + } +} |