From d2e693ec2a2b94e9f92160db2e3fe1a619fd8b30 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Mon, 20 Oct 2014 16:26:06 +0200 Subject: [PATCH] BpApp: initial import of legacy version --- application/clicommands/CheckCommand.php | 87 +++++ application/controllers/ProcessController.php | 140 ++++++++ .../views/scripts/process/history.phtml | 94 ++++++ application/views/scripts/process/show.phtml | 239 ++++++++++++++ .../views/scripts/process/source.phtml | 22 ++ configuration.php | 8 + library/Bpapp/BpNode.php | 187 +++++++++++ library/Bpapp/BusinessProcess.php | 309 ++++++++++++++++++ library/Bpapp/HostNode.php | 21 ++ library/Bpapp/Node.php | 158 +++++++++ library/Bpapp/ServiceNode.php | 33 ++ module.info | 5 + public/css/module.less | 192 +++++++++++ public/img/ack.gif | Bin 0 -> 564 bytes public/img/downtime.gif | Bin 0 -> 601 bytes public/img/help.gif | Bin 0 -> 1057 bytes public/img/icon_collapse.png | Bin 0 -> 178 bytes public/img/icon_expand.png | Bin 0 -> 177 bytes public/img/op_and.png | Bin 0 -> 680 bytes public/img/op_min1.png | Bin 0 -> 574 bytes public/img/op_min2.png | Bin 0 -> 626 bytes public/img/op_min3.png | Bin 0 -> 652 bytes public/img/op_min4.png | Bin 0 -> 606 bytes public/img/op_min5.png | Bin 0 -> 633 bytes public/img/op_min6.png | Bin 0 -> 664 bytes public/img/op_min7.png | Bin 0 -> 631 bytes public/img/op_min8.png | Bin 0 -> 660 bytes public/img/op_min9.png | Bin 0 -> 647 bytes public/img/op_or.png | Bin 0 -> 591 bytes public/js/module.js | 144 ++++++++ 30 files changed, 1639 insertions(+) create mode 100644 application/clicommands/CheckCommand.php create mode 100644 application/controllers/ProcessController.php create mode 100644 application/views/scripts/process/history.phtml create mode 100644 application/views/scripts/process/show.phtml create mode 100644 application/views/scripts/process/source.phtml create mode 100644 configuration.php create mode 100644 library/Bpapp/BpNode.php create mode 100644 library/Bpapp/BusinessProcess.php create mode 100644 library/Bpapp/HostNode.php create mode 100644 library/Bpapp/Node.php create mode 100644 library/Bpapp/ServiceNode.php create mode 100644 module.info create mode 100644 public/css/module.less create mode 100644 public/img/ack.gif create mode 100644 public/img/downtime.gif create mode 100644 public/img/help.gif create mode 100644 public/img/icon_collapse.png create mode 100644 public/img/icon_expand.png create mode 100644 public/img/op_and.png create mode 100644 public/img/op_min1.png create mode 100644 public/img/op_min2.png create mode 100644 public/img/op_min3.png create mode 100644 public/img/op_min4.png create mode 100644 public/img/op_min5.png create mode 100644 public/img/op_min6.png create mode 100644 public/img/op_min7.png create mode 100644 public/img/op_min8.png create mode 100644 public/img/op_min9.png create mode 100644 public/img/op_or.png create mode 100644 public/js/module.js diff --git a/application/clicommands/CheckCommand.php b/application/clicommands/CheckCommand.php new file mode 100644 index 0000000..90d889f --- /dev/null +++ b/application/clicommands/CheckCommand.php @@ -0,0 +1,87 @@ +app->getModuleManager()->loadModule('monitoring'); + $this->config = Config::module($this->moduleName); + $this->readConfig(); + $this->prepareBackend(); + } + + /** + * Check a specific process + * + * Blabla + */ + public function processAction() + { + $bp = BusinessProcess::parse($this->filename); + $node = $bp->getNode($this->params->shift()); + if ($this->params->get('soft-states')) { + $bp->useSoftStates(); + } + $bp->retrieveStatesFromBackend($this->backend); + printf("Business Process %s: %s\n", $node->getStateName(), $node->getAlias()); + exit($node->getState()); + + } + + // TODO: Remove this + protected function prepareBackend() + { + if ($this->backend === null) { + $name = $this->config->{'global'}->get('default_backend'); + if (isset($this->bpconf->backend)) { + $name = $this->bpconf->backend; + } + $this->backend = Backend::createBackend($name); + } + return $this->backend; + } + + // TODO: Remove this + protected function readConfig() + { + $this->views = array(); + $this->aliases = array(); + foreach ($this->config as $key => $val) { + if (! preg_match('~^view-(.+)$~', $key, $match)) continue; + $this->views[$match[1]] = (object) $val->toArray(); + $this->aliases[(string) $val->title] = $match[1]; + if ($val->aliases) { + foreach (preg_split('~\s*,\s*~', $val->aliases, -1, PREG_SPLIT_NO_EMPTY) as $alias) { + $this->aliases[$alias] = $match[1]; + } + } + } + $bpname = $this->params->get('bp', key($this->views)); + + if (array_key_exists($bpname, $this->aliases)) { + $bpname = $this->aliases[$bpname]; + } + if (! array_key_exists($bpname, $this->views)) { + throw new Exception('Got invalid bp name: ' . $bpname); + } + $this->bpconf = $this->views[$bpname]; + $this->bpname = $bpname; + + $this->filename = $this->config->global->bp_config_dir + . '/' . $this->bpconf->file . '.conf'; + } +} diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php new file mode 100644 index 0000000..7988cc7 --- /dev/null +++ b/application/controllers/ProcessController.php @@ -0,0 +1,140 @@ +requireJs('bpaddon.js'); + $this->config = Config::module('bpapp'); + $this->readConfig(); + $this->prepareBackend(); + $this->view->showMenu = $this->_getParam('menu', 'enabled') === 'enabled'; + $this->view->tabs = $this->createTabs(); + } + + protected function prepareBackend() + { + if ($this->backend === null) { + $name = $this->config->{'global'}->default_backend; + if (isset($this->bpconf->backend)) { + $name = $this->bpconf->backend; + } + $this->view->backend = $name; + $this->backend = Backend::createBackend($name); + } + return $this->backend; + } + + protected function createTabs() + { + // $tabs = $this->widget('tabs'); + $tabs = Widget::create('tabs'); + $action = $this->_request->getActionName(); + foreach ($this->views as $bpname => $bpconf) { + $tabs->add($bpname, array( + 'url' => 'bpapp/process/' . $action, + 'urlParams' => array('bp' => $bpname), + 'title' => $bpconf->title, + )); + } + $tabs->activate($this->bpname); + return $tabs; + } + + protected function readConfig() + { + $this->views = array(); + $this->aliases = array(); + foreach ($this->config as $key => $val) { + if (! preg_match('~^view-(.+)$~', $key, $match)) continue; + $this->views[$match[1]] = (object) $val->toArray(); + $this->aliases[(string) $val->title] = $match[1]; + if ($val->aliases) { + foreach (preg_split('~\s*,\s*~', $val->aliases, -1, PREG_SPLIT_NO_EMPTY) as $alias) { + $this->aliases[$alias] = $match[1]; + } + } + } + $this->view->views = $this->views; + $bpname = $this->_getParam('bp', key($this->views)); + + if (array_key_exists($bpname, $this->aliases)) { + $bpname = $this->aliases[$bpname]; + } + if (! array_key_exists($bpname, $this->views)) { + throw new Exception('Got invalid bp name: ' . $bpname); + } + $this->bpconf = $this->views[$bpname]; + $this->view->bpname = $bpname; + $this->bpname = $bpname; + + $this->filename = $this->config->global->bp_config_dir + . '/' . $this->bpconf->file . '.conf'; + } + + public function sourceAction() + { + $this->view->title = 'Source: ' . $this->bpconf->title; + $this->view->source = file_get_contents($this->filename); + } + + + public function historyAction() + { + $bp = BusinessProcess::parse($this->filename); + echo '
' . print_r($bp, 1) . '
'; + exit; + } + + public function showAction() + { + $this->setAutoRefreshInterval(10); + + $this->view->opened = $this->_getParam('opened'); + $this->view->compact = $this->_getParam('view') === 'compact'; + $bpconf = $this->bpconf; + $this->view->title = 'Process: ' . $bpconf->title; + + $bp = BusinessProcess::parse($this->filename); + if ($this->_getParam('state_type') === 'soft' + || (isset($bpconf->states) && $bpconf->states === 'soft')) { + $bp->useSoftStates(); + } + $bp->retrieveStatesFromBackend($this->backend); + $this->view->bp = $bp; + + if (isset($bpconf->slahosts)) { + $sla_hosts = preg_split('~\s*,s*~', $bpconf->slahosts, -1, PREG_SPLIT_NO_EMPTY); + if (isset($bpconf->sla_year)) { + $start = mktime(0, 0, 0, 1, 1, $bpconf->sla_year); + $end = mktime(23, 59, 59, 1, 0, $bpconf->sla_year + 1); + } else { + $start = mktime(0, 0, 0, 1, 1, (int) date('Y')); + $end = null; + // Bis zum Jahresende hochrechnen: + // $end = mktime(23, 59, 59, 1, 0, (int) date('Y') + 1); + } + $this->view->slas = $this->backend + ->module('BpAddon') + ->getBpSlaValues($sla_hosts, $start, $end); + } else { + $this->view->slas = array(); + } + + $this->view->available_bps = $this->views; + } +} + diff --git a/application/views/scripts/process/history.phtml b/application/views/scripts/process/history.phtml new file mode 100644 index 0000000..34a20b3 --- /dev/null +++ b/application/views/scripts/process/history.phtml @@ -0,0 +1,94 @@ + + + + + + + +
 
+ +
+ + + + + + +
 
+
+ +
+history as $entry) { +if ($entry->hostname !== $last_host || $entry->service !== $last_service) { + +echo '' . "\n"; +} +$cnt++; +if ($cnt > 10000) break; + $duration = $entry->timestamp - $current_offset; + if ($next_color === null) { + $color = stateColor($entry->last_state); + } else { + $color = $next_color; + } + $next_color = stateColor($entry->state); + + if ($entry->state == 0) { + $offset = ceil($duration / 3600 / 6); + } else { + $offset = floor($duration / 3600 / 6); + } + echo '
 
'; + $current_offset += $duration; + + $last_host = $entry->hostname; +$last_service = $entry->service; + +} + + +?>
diff --git a/application/views/scripts/process/show.phtml b/application/views/scripts/process/show.phtml new file mode 100644 index 0000000..9eb2a77 --- /dev/null +++ b/application/views/scripts/process/show.phtml @@ -0,0 +1,239 @@ +showMenu) { + + if ($this->compact) { + + } else { + +?> +
+tabs ?> +
+ +getState() == 0) { + $opened = false; + } + if ($level > 0) $opened = false; + $opened = false; + + +// Example "open three problem levels" + if ($node instanceof BpNode && $node->getState() > 0 && $level < 2) { +// $opened = true; + } + + if ($hidden) { + $opened = false; + } + + $id = $id_prefix . $node->getAlias(); + + if (array_key_exists(md5($id), $opened_list)) { + $opened_list = $opened_list[md5($id)]; + $opened = true; + } else { + $opened_list = array(); + } + if (! $opened) { + $extra = ' collapsed'; + } else { + $extra = ''; + } + + $htm .= ''; + + + + if ($node->isMissing()) { + $state_classes = 'state missing'; + } else { + $state_classes = 'state ' . stateName($node->getState()); + } + if ($node->isInDowntime() || $node->isAcknowledged()) { + $state_classes .= ' handled'; + } + + if ($node instanceof BpNode) { + $htm .= '\n"; + + if ($node->hasChildren()) { + $htm .= '' + . '\n"; + } + } else { + $htm .= '\n"; + } + + $htm .= '
' + . ' ' + . preg_replace('~^\d{2}\.\s*~', '', $node->getAlias()); +/* . ': ' + . $node->getState() + . '/' + . $node->getSortingState(); + */ + if ($node->isInDowntime()) $htm .= ' '; + if ($node->isAcknowledged()) $htm .= ' '; + + if ($node->hasUrl()) { + $htm .= ' '; + } + + // Summaries, PIE +if (! $self->compact) { + $summary = $node->getStateSummary(); + $sumtxt = array(); + foreach ($summary as $k => $v) { + if ($v > 0) { + $sumtxt[] = $v . 'x ' . stateName($k); + } + } + $sumtxt = implode(', ', $sumtxt); + if ($sumtxt === '') $sumtxt = '-'; + if ($level === 0) { + $htm .= '' + . implode(',', $node->getStateSummary()) + . ''; + } +} + // END of PIE + $alias = $node->getAlias(); + if (array_key_exists($alias, $slas)) { + $sla = $slas[$alias]; + $sla_style = ''; + if ($sla->level === null) $sla->level = 0; + if ($sla->value < $sla->level) { + $sla_style = ' color: red; font-weight: bold;'; + } elseif ($sla->value < $sla->level * 1.002) { + $sla_style = ' color: orange;'; + } + $htm .= sprintf(' [SLA] %0.3f%% (Soll: %0.2f%%)', $sla->value, $sla->level); + } + + + // Problem info: + if ($node->getState() > 0) { + $problems = array(); + foreach ($node->getChildren() as $child) { + if ($child->getState() > 0) { + $problems[] = '' . htmlspecialchars($child->getAlias()) . ''; + } + } + $htm .= ':

' . implode(', ', $problems) . '

'; + } + // END of Problem Info + + $htm .= "
hasInfoCommand() ? ' rowspan="2"' : '') . '>' + . $node->getOperator() + . ''; +//$htm .= ''; + + foreach ($node->getChildren() as $name => $child) { + $htm .= showNode($self, $child, $slas, $opened_list, $id_prefix . $id . '_', $level + 1, ! $opened); + } + $htm .= "
'; + if ($node->isInDowntime()) $htm .= ' '; + if ($node->isAcknowledged()) $htm .= ' '; + $htm .= stateName($node->getState()) + . '' + . $node->getHostname() + . '' + . ($node instanceof ServiceNode + ? ' / ' . $node->getServiceDescription() . '' + : '') +/* DEBUG + . ': ' + . $node->getState() + . '/' + . $node->getSortingState()*/ + ; + $htm .= "
'; + $htm .= "\n"; + return $htm; +} + +echo '
'; + +if (! is_array($this->opened)) { $this->opened = array(); } + +?>bp->getRootNodes() as $name => $node): ?> +slas, $this->opened, 'bp_') ?> + +bp->hasWarnings()): ?> +

Warnings

+bp->getWarnings() as $warning): ?> +escape($warning) ?>
+ + +
diff --git a/application/views/scripts/process/source.phtml b/application/views/scripts/process/source.phtml new file mode 100644 index 0000000..58066ff --- /dev/null +++ b/application/views/scripts/process/source.phtml @@ -0,0 +1,22 @@ +
+ +Render + +tabs ?> +
+
+source);
+$len = ceil(log(count($lines), 10));
+
+foreach ($lines as $line) {
+    $cnt++;
+    printf("%0" . $len . "d: %s\n", $cnt, $this->escape($line));
+}
+
+?>
+
diff --git a/configuration.php b/configuration.php new file mode 100644 index 0000000..749e561 --- /dev/null +++ b/configuration.php @@ -0,0 +1,8 @@ +menuSection($this->translate('Availability'), array( + 'icon' => 'img/icons/servicegroup.png', + 'priority' => 40 +)); +$section->add($this->translate('Business Processes'))->setUrl('bpapp/process/show'); + diff --git a/library/Bpapp/BpNode.php b/library/Bpapp/BpNode.php new file mode 100644 index 0000000..bc1b846 --- /dev/null +++ b/library/Bpapp/BpNode.php @@ -0,0 +1,187 @@ +bp = $bp; + $this->name = $object->name; + $this->operator = $object->operator; + $this->setChildNames($object->child_names); + } + + public function getStateSummary() + { + if ($this->counters === null) { + $this->getState(); + $this->counters = array(0, 0, 0, 0); + foreach ($this->children as $child) { + if ($child instanceof BpNode) { + $counters = $child->getStateSummary(); + foreach ($counters as $k => $v) { + $this->counters[$k] += $v; + } + } else { + $state = $child->getState(); + $this->counters[$state]++; + } + } + } + return $this->counters; + } + + public function getOperator() + { + return $this->operator; + } + + public function setUrl($url) + { + $this->url = $url; + } + + public function hasUrl() + { + return $this->url !== null; + } + + public function setInfoCommand($cmd) + { + $this->info_command = $cmd; + } + + public function hasInfoCommand() + { + return $this->info_command !== null; + } + + public function getUrl() + { + return $this->url; + } + + public function getInfoCommand() + { + return $this->info_command; + } + + public function getAlias() + { + return $this->alias; + } + + public function setAlias($name) + { + $this->alias = $name; + return $this; + } + + public function getState() + { + if ($this->state === null) { + $this->calculateState(); + } + return $this->state; + } + + protected function calculateState() + { + $sort_states = array(); + foreach ($this->getChildren() as $child) { + $sort_states[] = $child->getSortingState(); + } + switch ($this->operator) { + case self::OP_AND: + $state = max($sort_states); + break; + case self::OP_OR: + $state = min($sort_states); + break; + default: + // MIN: + if (! is_numeric($this->operator)) { + throw new Exception( + sprintf( + 'Got invalid operator: %s', + $this->operator + ) + ); + } + sort($sort_states); + + // default -> unknown + $state = 2 << self::SHIFT_FLAGS; + + for ($i = 1; $i <= $this->operator; $i++) { + $state = array_shift($sort_states); + } + } + if ($state & self::FLAG_DOWNTIME) { + $this->setDowntime(true); + } + if ($state & self::FLAG_ACK) { + $this->setAck(true); + } + $state = $state >> self::SHIFT_FLAGS; + + if ($state === 3) { + $this->state = 2; + } elseif ($state === 2) { + $this->state = 3; + } else { + $this->state = $state; + } + } + + public function hasChildren() + { + return count($this->children) > 0; + } + + public function setDisplay($display) + { + $this->display = $display; + return $this; + } + + public function setChildNames($names) + { + $this->child_names = $names; + $this->children = null; + return $this; + } + + public function getChildren() + { + if ($this->children === null) { + $this->children = array(); + natsort($this->child_names); + foreach ($this->child_names as $name) { + $this->children[$name] = $this->bp->getNode($name); + } + } + return $this->children; + } +} diff --git a/library/Bpapp/BusinessProcess.php b/library/Bpapp/BusinessProcess.php new file mode 100644 index 0000000..18d796f --- /dev/null +++ b/library/Bpapp/BusinessProcess.php @@ -0,0 +1,309 @@ +object_ids); + } +*/ + + public static function parse($filename) + { + $bp = new BusinessProcess(); + $bp->filename = $filename; + $bp->doParse(); + return $bp; + } + + public function useSoftStates() + { + $this->state_type = self::SOFT_STATE; + return $this; + } + + public function useHardStates() + { + $this->state_type = self::HARD_STATE; + return $this; + } + + protected function doParse() + { + $fh = @fopen($this->filename, 'r'); + if (! $fh) { + throw new Exception('Could not open ' . $this->filename); + } + + $this->parsing_line_number = 0; + while ($line = fgets($fh)) { + $line = trim($line); + + $this->parsing_line_number++; + + if (preg_match('~^#~', $line)) { + continue; + } + + if (preg_match('~^$~', $line)) { + continue; + } + + if (preg_match('~^display~', $line)) { + list($display, $name, $desc) = preg_split('~\s*;\s*~', substr($line, 8), 3); + $node = $this->getNode($name)->setAlias($desc)->setDisplay($display); + if ($display > 0) { + $this->root_nodes[$name] = $node; + } + } + + if (preg_match('~^external_info~', $line)) { + list($name, $script) = preg_split('~\s*;\s*~', substr($line, 14), 2); + $node = $this->getNode($name)->setInfoCommand($script); + } + + if (preg_match('~^info_url~', $line)) { + list($name, $url) = preg_split('~\s*;\s*~', substr($line, 9), 2); + $node = $this->getNode($name)->setUrl($url); + } + + if (strpos($line, '=') === false) { + continue; + } + + 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+)\s*of:\s*(.+?)$~', $value, $m)) { + $this->parseError('syntax: = of: + [+ ]*'); + } + $op_name = $m[1]; + $value = $m[2]; + } + $cmps = preg_split('~\s*\\' . $op . '\s*~', $value); + + foreach ($cmps as & $val) { + if (strpos($val, ';') !== false) { + list($host, $service) = preg_split('~;~', $val, 2); + $this->all_checks[$val] = 1; + $this->hosts[$host] = 1; + } + } + $node = new BpNode($this, (object) array( + 'name' => $name, + 'operator' => $op_name, + 'child_names' => $cmps + )); + $this->addNode($name, $node); + } + + fclose($fh); + unset($this->parsing_line_number); + } + + public function retrieveStatesFromBackend($backend) + { + $this->backend = $backend; + // TODO: Split apart, create a dedicated function. + // Separate "parse-logic" from "retrieve-state-logic" + // Allow DB-based backend + // Use IcingaWeb2 Multi-Backend-Support + $check_results = array(); + $hostFilter = array_keys($this->hosts); + if ($this->state_type === self::HARD_STATE) { + $db_states = $this->backend/*->module('Bpapp')*/ + ->fetchHardStatesForBpHosts(array_keys($this->hosts)); + } else { +// TOM 2014 +// $db_states = $this->backend/*->module('Bpapp')*/ +// ->fetchSoftStatesForBpHosts(array_keys($this->hosts)); + $hostStatus = $backend->select()->from( + 'hostStatus', + array( + 'hostname' => 'host_name', + 'in_downtime' => 'host_in_downtime', + 'ack' => 'host_acknowledged', + 'state' => 'host_state' + ) + )->where('host_name', $hostFilter)->getQuery()->fetchAll(); + $serviceStatus = $backend->select()->from( + 'serviceStatus', + array( + 'hostname' => 'host_name', + 'service' => 'service_description', + 'in_downtime' => 'service_in_downtime', + 'ack' => 'service_acknowledged', + 'state' => 'service_state' + ) + )->where('host_name', $hostFilter)->getQuery()->fetchAll(); + + } + + foreach ($serviceStatus + $hostStatus as $row) { + $key = $row->hostname; + if ($row->service) { + $key .= ';' . $row->service; + // Ignore unused services, we are fetching more than we need + if (! array_key_exists($key, $this->all_checks)) { + continue; + } + $node = new ServiceNode($this, $row); + // $this->object_ids[$row->object_id] = 1; + } else { + $key .= ';Hoststatus'; + if (! array_key_exists($key, $this->all_checks)) { + continue; + } + $node = new HostNode($this, $row); + // $this->object_ids[$row->object_id] = 1; + } + if ($row->state === null) { + $node = new ServiceNode( + $this, + (object) array( + 'hostname' => $row->hostname, + 'service' => $row->service, + 'state' => 0 + ) + ); + $node->setMissing(); + } + if ((int) $row->in_downtime === 1) { + $node->setDowntime(true); + } + if ((int) $row->ack === 1) { + $node->setAck(true); + } + $this->addNode($key, $node); + } + ksort($this->root_nodes); + return $this; + } + + public function getRootNodes() + { + return $this->root_nodes; + } + + public function hasNode($name) + { + return array_key_exists($name, $this->nodes); + } + + public function getNode($name) + { + if (array_key_exists($name, $this->nodes)) { + return $this->nodes[$name]; + } + + // 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); + $node = new ServiceNode( + $this, + (object) array( + 'hostname' => $host, + 'service' => $service, + 'state' => 0 + ) + ); + $node->setMissing(); + return $node; + } + + throw new Exception( + sprintf('The node "%s" doesn\'t exist', $name) + ); + } + + protected function addNode($name, Node $node) + { + if (array_key_exists($name, $this->nodes)) { + $this->warn( + sprintf( + 'Node "%s" has been defined twice', + $name + ) + ); + } + $this->nodes[$name] = $node; + return $this; + } + + public function hasWarnings() + { + return ! empty($this->warnings); + } + + public function getWarnings() + { + return $this->warnings; + } + + protected function warn($msg) + { + if (isset($this->parsing_line_number)) { + $this->warnings[] = sprintf( + 'Parser waring on %s:%s: %s', + $this->filename, + $this->parsing_line_number, + $msg + ); + } else { + $this->warnings[] = $msg; + } + } + + protected function parseError($msg) + { + throw new Exception( + sprintf( + 'Parse error on %s:%s: %s', + $this->filename, + $this->parsing_line_number, + $msg + ) + ); + } +} diff --git a/library/Bpapp/HostNode.php b/library/Bpapp/HostNode.php new file mode 100644 index 0000000..fe70dc4 --- /dev/null +++ b/library/Bpapp/HostNode.php @@ -0,0 +1,21 @@ +name = $object->hostname . ';Hoststate'; + $this->hostname = $object->hostname; + $this->bp = $bp; + $this->setState($object->state); + } + + public function getHostname() + { + return $this->hostname; + } +} diff --git a/library/Bpapp/Node.php b/library/Bpapp/Node.php new file mode 100644 index 0000000..38e8114 --- /dev/null +++ b/library/Bpapp/Node.php @@ -0,0 +1,158 @@ +missing = $missing; + return $this; + } + + public function isMissing() + { + return $this->missing; + } + + public function addChild(Node $node) + { + if (array_key_exists((string) $node, $this->children)) { + throw new Exception( + sprintf( + 'Node "%s" has been defined more than once', + $node + ) + ); + } + $this->childs[(string) $node] = $node; + $node->setParent($this); + return $this; + } + + public function setState($state) + { + $this->state = (int) $state; + 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() + { + return self::$state_names[ $this->getState() ]; + } + + public function getState() + { + if ($this->state === null) { + throw new Exception( + sprintf( + 'Node %s is unable to retrieve it\'s state', + $this->name + ) + ); + } + return $this->state; + } + + public function getSortingState() + { + $state = $this->getState(); + if ($state === 3) { + $state = 2; + } elseif ($state === 2) { + $state = 3; + } + $state = ($state << self::SHIFT_FLAGS) + + ($this->isInDowntime() ? self::FLAG_DOWNTIME : 0) + + ($this->isAcknowledged() ? self::FLAG_ACK : 0); + if (! ($state & (self::FLAG_DOWNTIME | self::FLAG_ACK))) { + $state |= self::FLAG_NONE; + } + return $state; + } + + public function setParent(Node $parent) + { + $this->parent = $parent; + return $this; + } + + public function getDuration() + { + return $this->duration; + } + + 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 hasChildren() + { + return false; + } + + public function __destruct() + { + // required to avoid memleeks in PHP < 5.3: + $this->parent = null; + $this->children = array(); + } + + public function __toString() + { + return $this->name; + } +} diff --git a/library/Bpapp/ServiceNode.php b/library/Bpapp/ServiceNode.php new file mode 100644 index 0000000..9f93957 --- /dev/null +++ b/library/Bpapp/ServiceNode.php @@ -0,0 +1,33 @@ +name = $object->hostname . ';' . $object->service; + $this->hostname = $object->hostname; + $this->service = $object->service; + $this->bp = $bp; + $this->setState($object->state); + } + + public function getHostname() + { + return $this->hostname; + } + + public function getServiceDescription() + { + return $this->service; + } + + public function getAlias() + { + return $this->hostname . ': ' . $this->service; + } +} diff --git a/module.info b/module.info new file mode 100644 index 0000000..309beaa --- /dev/null +++ b/module.info @@ -0,0 +1,5 @@ +Name: BPapp +Version: 0.0.1 +Depends: monitoring +Description: BPapp is a viewer for BPaddon business process config files + That's it diff --git a/public/css/module.less b/public/css/module.less new file mode 100644 index 0000000..5a36ca4 --- /dev/null +++ b/public/css/module.less @@ -0,0 +1,192 @@ + +.content a { + color: #333; + text-decoration: none; +} + +.content a:hover { + text-decoration: underline; +} + +table.businessprocess, table.businessprocess table { + border-collapse: collapse; + margin: 0; + padding: 0; + width: 100%; +} + +table.businessprocess { + width: 100%; +} + +table.businessprocess tr { + margin: 0; + padding: 0; + font-family: Verdana, Helvetica, Arial, sans-serif; + font-size: 0.97em; +} + +table.businessprocess tr tr tr tr { + font-size: 1em; +} + +table.businessprocess th, table.businessprocess td { + margin: 0; + padding: 0.3em 0 0 0.3em; + text-align: left; + vertical-align: middle; + line-height: 1.7em; +} + +table.businessprocess th { +/* IE? */ + padding: 0.2em 1em 0.2em 1em; + cursor: pointer; + -moz-user-select: none; + -ms-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; +} + +table.businessprocess th.hovered.state { + text-decoration: underline; +} + +a img { + border: none; +} + +table.businessprocess th.bptitle { + border-radius: 0.5em 0.5em 0.5em 0; + -moz-border-radius: 0.5em 0.5em 0.5em 0; +} + +table.collapsed > tbody > tr > th.bptitle { + border-radius: 0.5em; + -moz-border-radius: 0.5em; +} + +table.businessprocess th.operator { + width: 1em; + padding: 0.5em; + text-align: center; + vertical-align: middle; + border-radius: 0 0 0.5em 0.5em; + -moz-border-radius: 0 0 0.5em 0.5em; + +} + +table.businessprocess td.service { + border-radius: 0.5em; + -moz-border-radius: 0.5; + width: 6em; + text-align: center; + font-weight: bold; +} + +table.businessprocess td.service img { + float: left; +} + +table.businessprocess, table.businessprocess table { + border-top: 2px solid transparent; + border-left: 2px solid transparent; +} + +table.businessprocess .state { + -moz-user-select: none; + -ms-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; +} + +.state.unknown { + background-color: @colorUnknown; +} + +.state.critical { + background-color: @colorCritical; + color: white; +} + +.state.critical.handled { + background-color: @colorCriticalHandled; + color: #000; +} + +.state.warning { + background-color: @colorWarning; +} + +.state.unknown.handled { + background-color: @colorUnknownHandled; +} + +.state.pending { + background-color: @colorPending; +} + +.state.warning.handled { + background-color: @colorWarningHandled; + color: #000; +} + +.state.ok { + background-color: @colorOk; + color: #fff; +} + +th.hovered.state.unknown { + background-color: #aac; +} + +table.businessprocess th p.problems { + font-weight: normal; + font-size: 0.7em; + margin: 0; + padding: 0; + display: none; +} + +.collapsed > tbody > tr > th > p.problems { + display: inline; +} + +table.businessprocess th p.problems span { + padding: 3px; +} + +table.businessprocess th.hovered p.problems > .state { + text-decoration: none; +} + +span.collapsible { + background-image: url("../img/bpapp/icon_collapse.png"); + background-repeat: no-repeat; + width: 1.7em; + height: 1.7em; + background-position: left center; + display: block; + float: left; +} + +.collapsed span.collapsible { + background: url("../img/bpapp/icon_expand.png"); + background-repeat: no-repeat; + background-position: left center; +} + +.collapsed tr.children { + display: none; +} + +.inlinepie { + display: none; + width: 2em; + float: right; + margin-top: 0.1em; + text-align: center; + vertical-align: middle; + color: transparent; +} + diff --git a/public/img/ack.gif b/public/img/ack.gif new file mode 100644 index 0000000000000000000000000000000000000000..cda95a881cef82a88652f560994793d1203e74b8 GIT binary patch literal 564 zcmZ?wbhEHb6krfwc*elcx$@zREzjm`f41VltJQ~IZ9egK*V%Wjl^5NsFM8Hq^sc?= zTYuTV@p54E)$sPKv7MJvC*I7Te!Fnyo#Hw7O6J_Fn0K#oJ`mk)Tynp9_NC^f_gj}g zY+w1Xd+pUFG_A_sHoqMhJIG z>+9?7?d|F5>FVn0?Ck95=xA?mZ*6UDX=!P0ZvOxOKLfRa;!hSv28I*{9gxAGIALI) z*pSlH+!7QV($d)+Y-=6X)fsGO;~zeuCDhhAA|)y&c6yk-M`U!ML&EfMM^|4zZ_mU| zS-A)|7dLk=pQKI(4HYG)fVhOjnH*em&apT7I>({ScyLR>J)hk!7T)uqy(xpomFJ8QG;llaz=MNt~eDL7G{rmUt z+qZA;-o1PF?Ag70_s*R=w{6?Db?es6n>Vjtzkcb`rHdCYUbJY@!i5VLELbpq{`}dq zXV027Yv#MTLch1qB89`T2QydAYf{IXOAm+1XiHS(%xc z85tS>|NmzgtU&Q63nK%A4}%WKfuJ~HVBgl@)70G3+QuiTpeQBK*2F9)%+AWnE+Ws; z$|%mk#=*nG#m=eB*&@Zs&cn+uASftpVA#wn!p6I1ov@0dmbqk;#NxGrLc*#}F4hK` zO-ihMyF}ESU0mID?9Q>n!CHFor!i6zZi$8#>2;+-ujl( z%~m=u3V1z*RJ{}KY;g2w;FQ#IF|bHEut*`ONF}6PIka3QtWGtuK|Qig zEviv7rb#QdSv$5>JGMUpsArUfN{+w8;jUGYvCm80O72 z$(?VKx6n9$o>~4ZlY)6>1+z^HmYNnWGb>$YR<_){Y^izqa)+{oj%AB&$~V|nthK4! zWLLS?zIugC^;YYe^)@vdY-_gJ)~>g&+vHHc*|A}>Q^OXAhCPlAyIq>Lc{Hu}XxiZ1 zbkMQ+kZbdHx9086%?F)Y4!XANb#K|>+vc2Sa-gNA&Is?>iC!MEysj z`VYkQ?~R&pIC{e2s0qj7C+>@#bR=%lvA9Vm<0qd=nRYgH`k}NLr_*Pg&YpcdXZD5M zImdG60@3l@xfk>1UM!q{rfA{0vW2&5mtC!0cDr`@wYufE8&}Oga7}rBZmw-3 z&&-*I8(1YAi~oe|+Okqe-l}=W8i5y&_-C8Ulv`Pu%o#k(tRdo$a^SLa;zpB27CH)h z3eVIpJ=F7o!M$giZtR|#jfYoW5YtV5P;x=R{nQk#unj2_TTXaQ)eL%+b6|s7r;vKU zq8)`ItX;yYKG#w<6g)k_r{LK2MKMH$V}h*riiimd7J2eYn-^ppD?IGhtFEc^r}{#( zTaSd1l#3oCO|{#XzwV5N6C)*?bKsnBwW; z7@{%p>cxY+4GID*2aZN?%+;AYXKumV6>}ZbON#6~M4HuXO6|YjI9YdYrsOdr-d*|^ zcFq$p>krvw^*lB-x#xP8m$&1G_k0X%ICpZjiUcvN$hv*+#megM4y)hIow-xK_BF%C X;OeHJiR(>(_Az+6`njxgN@xNAM}t1{ literal 0 HcmV?d00001 diff --git a/public/img/icon_expand.png b/public/img/icon_expand.png new file mode 100644 index 0000000000000000000000000000000000000000..19862cf34f1ba9fdb0530bdcd6562f76512e7a6a GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1SBVD?P>#3oCO|{#XzwV5N6C)*?bKsnC$7| z7@{$8>m^6g1_K`E3p~mXmRM*8S*dzR1{DaM>-mx^Ui7bBZGEfCvrRr$j@fHhKDB00 zYSOYb{p`a%D>L@=8RbR0tz!?IjgCHE!kqW!dMVF}lBP7iA8)eCP6c{Si?4gYxO~P+ V5v?nEEI{iRJYD@<);T3K0RSo2JBt7S literal 0 HcmV?d00001 diff --git a/public/img/op_and.png b/public/img/op_and.png new file mode 100644 index 0000000000000000000000000000000000000000..e4683b45b39f35ecfc4d3df0a3475dbc54c52a84 GIT binary patch literal 680 zcmV;Z0$2TsP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z2_^#mJpd^H000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}00065Nkl7L%Z1cxHOT^S9LMzMwtuJ&ZnxXFq5L*rzuz-$Fc{)mCX|X42MHVk_4Sj2e;deN~MAy*UJXSaq#(ksMTr@A5ahxMIsSOBoegS z?WoyoQoUaPHVmawNh+1<(Fp*8Ah6Q>*$#&TI-L%jPA5A}qtSQ_As&yzYPDiK9{(~I ztyYTxBuTmhip3&49uEqI0t7)ox7%ft>-8Eon+*W4-|rc~Xf)md7K;Ug!2qAhbh%vY zh^0~qu~-ZMaJ$_e63PHP&ttV(F~@ehMKBmdu~S->P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z4jD9T7O4pU000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0004*NkljgKy1>l)mFf>^o*aRuT6M6k8d-d^y5h=s%jq)-HH zJh#y?-m4*ll@|{L{v?xcX2LmtB1jT%Na9U>0bjru-~%9pfDpnP&~CS}+wGp-^pkYC z-EL7R6i_Oa(Cv2NIF8;PNb-0*vRbVY03(qI-MS8WDOs!4kWQzuSS--#bYR=I=Y;F^ z8jVH+#bObOL;`5FTC{EZOs7+Z!(jrrA70CI4z z%;$5Gq|oWgp->3t^BDkeI-Ot`2DAWfQ{5i>pfFu7m#16t4RBjL8jb1$CX%joV~Q)?>QcinaO0- zY6gRWzb&{m91a!0vMhao<2VWsi^bG+-&r6DDWx*f%~tSg+Ta&*vGB$Mw4MKm9}b0mm1Rvk`2cO#lD@ M07*qoM6N<$g0?{Oga7~l literal 0 HcmV?d00001 diff --git a/public/img/op_min2.png b/public/img/op_min2.png new file mode 100644 index 0000000000000000000000000000000000000000..3088eb69475521ffa7e4596d16c1918ed0a6fdc2 GIT binary patch literal 626 zcmV-&0*(ENP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z4jKxETIXQ^000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005aNklxk*y}#UZd(OEP0D#_*NN?%`_y9hD9{{FllA6 zKp?;dZQDknP{3p|aR=zS4nfd06`F#eP8yAipjR^r=h$Iuq=z&!C;UdFr7}Bok%3;@if2H z>2$(qG(s#EV>!_2bezTXN;V3DfK)1lZnx{0!}S$B9uIlFUZ?Y?5RS(q#^W*K@i^N} zuh)AmghvgBLk7?^jUQlH76XJrA?AO%766baiY$@ti6y>MNWbuAk|c@Q_r}5nh(@E# z-fTAf08P`FU9DDWxm*%gQ{`&wZd$EYs8lLQr_%_B!+c%&kN%;20U1aS_w_)nU;qFB M07*qoM6N<$f)Z&9p8x;= literal 0 HcmV?d00001 diff --git a/public/img/op_min3.png b/public/img/op_min3.png new file mode 100644 index 0000000000000000000000000000000000000000..7c7eb9c3c63be21879b4a36daeef7cb74a4194b5 GIT binary patch literal 652 zcmV;70(1R|P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z4jLH(000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005!Nkln0?loS6)w#G}U^8Jp_DuHLGaLZW8*Uvak5;RN-EPM|T9yUFFpx+j*xYKh zLZwncE|-HSilBPE{_qSmO~d(ohOX<7Bnbd8olfEPdO<|r!!9*V)8O~}kxHd77z_Xa zrBaD4EEbEH&1Uw1M~cVe3^1S10RWtnt}F-wU9VRnBD!2IC z!>zmwxRp2&Q6La-4wy_PEKaA>KfUlw$Kw&Yu0xh(Rs)?*=i3X<7T=dF$8pGHGU#@@ zKke+~#Ce`4x7+=3FY=bPP literal 0 HcmV?d00001 diff --git a/public/img/op_min4.png b/public/img/op_min4.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f116553df2ac33673e53320546a0245142bcd1 GIT binary patch literal 606 zcmV-k0-^nhP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z5d$5G^DpxN000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005GNklhr;0JQHwRja zF@{VggMPn%+s+~{?(ulY>-FBZvy=AwJ;viPlF1~mW-u7IyMkTA;gADVRTT%ArpW=J zP>Aol?gaoONfI}bWi08HMtZ|9lPt?Twg!t3AQp@9c(q!I15{PzajjOP`Fu`7L-o&N s-LA!Afl8%Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z5d$wlBz|)M000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005hNklGI3)yK7e16y%r=%dK!BbEvwh- zwA<}m7Xk3gGllJTONL=ku~?*Lvq`pXi{$_Sr_%|RWr2tg3We~y$v{K%>#Xe!u67x7!WfZWoC}g3qnjYgDUM7>0ppGzwYQb(%~jq-mP#gY@}) zd_0@YD4);M`Fws(!S!fFV2mN1PNUcB0RYP7GKdI7gi@)5`FsvffSWW;&IMuY$Wuv)E9sZ@~5Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z5d;LH*%9;r000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005=NklM4Z0a>Zy*s5UY0EfeYa=9G2-EQJI?lGWJ zsnBY*`r|{nT&DGUP0Qty{^|I9K94{kfK)1lTCIldb_)Oqg+l1}`*}9|KfXg&+t>CX=XEtJrKd002P{plKS0!y){BKTDX;=O`A7 zNF);Q`~9#38AaDk8ZaMNs?F(G#ZV2Gd(oQuLL3j$8m_o zVraEmcY93z1)I%AJkQ@RJ}Y6j+o9L%Ash~~)3n>|*Gl-VPN%~FilUeY7>28u zzw8A7WU*LSBi$Oy@{W;y;LT*W+gW@YET#a#V35U&#lk#5Q4|)JN+r^DolFhY(P#vx y(|P9+Qvd*%&1NVR3W&$!@cDe^rt*LKLiqw_O}-hE2e$VB0000Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z5d;$<3Y0Sd000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005fNkl{v?u=l^OOnjFl;tc6JuN0VPY{!6%TVg_JD|)+`nIlVl-2z${3H zh4Zf5@pySnR?nMLQ+M}0^Xs~%bFLYQB=LbP_)uTK7w`rA10YF~xV&EPb3mn1!FIcS zI3Nj-8F{sv?<8qE@Rx*Y&#_I-L%(+3fQkNb+zvFrUv809`H@uR5R4 z3>PDHI9_g+iFkW~f%H*lace-tYI3N~K`8+r{2;xkRZ{LOdRa-|vT2Q4|b^ zL&&oHP~Fe+$Kx>)i3EVHmL6?Jxz9B;g{)lxLyZ~DL^0)5bM=y zWgehunpl_1Wz6Sum>R01(TGl`^DZN%03^9sELbcS8H>f}^ZCq8<^S{#_4YvRQ literal 0 HcmV?d00001 diff --git a/public/img/op_min8.png b/public/img/op_min8.png new file mode 100644 index 0000000000000000000000000000000000000000..4ff15e3905b769016bf3980386382eae3210b26a GIT binary patch literal 660 zcmV;F0&D$=P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z5d_CX>@2HM@dakSAh-}0005+NklQbv%0l*<{7Klj z&dT#1@5?zRtMles-A&!U=f0or>wZQeNxUE#FX|0=1Kxli0JT~Tx~@alb<}FL$AnU; z#MNrWzon`w4a1;e7*ti2k0dXbOT^=Gq|<3sDixTf2>|f8kyH&vv`bU@$0VpkA;4@xph>*8~8RB#F^zl#NE? z*3MRL+-9@EZnyty=kK)J?bzvb7z%|%HO*%8xfZ^v)oKYqGMTgvFilec+-|q{{v54t(3QJ3MI2_X9aNKyr u5`ZLUvl;XGJY%sKJsyv>t^A+Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z5d=3K32@>7000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0005vNklOF^ZT%F?Rkb1shEnu@hU{yg>@>#8=QKP<#Lh*xG0*h)oJX5d#)t zuPF?I7W!?oo~ygYy}{}(eh`?1oF6kM-%Jhz0D40Ly{Ql21NZ>`0Wb`MWLc(6CPS8G zxf5Eg7Pi|h{*)}sFijJtX`)i8z)iwtvmr%Ms8}pgx7#J#wuy-7a=8!@5o3&|)2Vv~ z0B}4Wq3b$`2%%620MKr?AxRQ6O@kxji-C=?3lbUN7Wb`LL%$77^YDSmHR z7HYK`6h%Qa8U;0*&8K?+0Q>zOrBVs?dK~~To6Qgm20=t$muqUj%h5Cqj^h9TG)?0R z%jGiW^SL|VnG%Tv2P_r~008qcl>>nQozG_?B08N;B#I(=0x-tzzur%4rt9_kw3KfH zZWbpZ3Wvkq0h7su$JuQ5u@_#+aU2YXLnM<)?txyf_hquY`LI1_KUIRnPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyu8 z4iX12G7;1O000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}00051Nkl zzm>BISub&6VVC?6IOJr$oEv8t0MHNgK|fRg2mk>f0K5%29*@-Nbf{D+Q7V-pf)B0N zYeb__5YgA;2heJ@mmW9b=f=nhOXJ9ZGNdH5Cb7UD~ zC=?194u`I9e03od3Q;&5cE5ag)4V%pd0ALt~1aQv1HHgRKv|KJ_YKz5!5{U%m^LgpN$+9eO4Z7Vfn$4ypHyRD} zdcC_{@uXmyCOGFfpU*g*PT-uwG|i`gkTZo^tw#NRp8!z3UU!9kSI$4oW;5uz4qewV zpU*v=P!vT*VB5C0jY0^hstQ$AT^)I}(%xRG)oK;{{T{!&)&byfIH28bqgX5=nN0fI d%K!9s{si>2I