summaryrefslogtreecommitdiffstats
path: root/library/Director/IcingaConfig
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:17:31 +0000
commitf66ab8dae2f3d0418759f81a3a64dc9517a62449 (patch)
treefbff2135e7013f196b891bbde54618eb050e4aaf /library/Director/IcingaConfig
parentInitial commit. (diff)
downloadicingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.tar.xz
icingaweb2-module-director-f66ab8dae2f3d0418759f81a3a64dc9517a62449.zip
Adding upstream version 1.10.2.upstream/1.10.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--library/Director/IcingaConfig/AgentWizard.php337
-rw-r--r--library/Director/IcingaConfig/AssignRenderer.php268
-rw-r--r--library/Director/IcingaConfig/ExtensibleSet.php574
-rw-r--r--library/Director/IcingaConfig/IcingaConfig.php781
-rw-r--r--library/Director/IcingaConfig/IcingaConfigFile.php168
-rw-r--r--library/Director/IcingaConfig/IcingaConfigHelper.php430
-rw-r--r--library/Director/IcingaConfig/IcingaConfigRendered.php34
-rw-r--r--library/Director/IcingaConfig/IcingaConfigRenderer.php10
-rw-r--r--library/Director/IcingaConfig/IcingaLegacyConfigHelper.php110
-rw-r--r--library/Director/IcingaConfig/StateFilterSet.php31
-rw-r--r--library/Director/IcingaConfig/TypeFilterSet.php39
11 files changed, 2782 insertions, 0 deletions
diff --git a/library/Director/IcingaConfig/AgentWizard.php b/library/Director/IcingaConfig/AgentWizard.php
new file mode 100644
index 0000000..aceddb1
--- /dev/null
+++ b/library/Director/IcingaConfig/AgentWizard.php
@@ -0,0 +1,337 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\Objects\IcingaEndpoint;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaZone;
+use Icinga\Module\Director\Util;
+use LogicException;
+
+class AgentWizard
+{
+ protected $db;
+
+ protected $host;
+
+ protected $parentZone;
+
+ protected $parentEndpoints;
+
+ /** @var string PKI ticket */
+ protected $ticket;
+
+ public function __construct(IcingaHost $host)
+ {
+ $this->host = $host;
+ }
+
+ protected function assertAgent()
+ {
+ if ($this->host->getResolvedProperty('has_agent') !== 'y') {
+ throw new ProgrammingError(
+ 'The given host "%s" is not an Agent',
+ $this->host->getObjectName()
+ );
+ }
+ }
+
+ protected function getCaServer()
+ {
+ return $this->db()->getDeploymentEndpointName();
+
+ // TODO: This is a problem with Icinga 2. Should look like this:
+ // return current($this->getParentEndpoints())->object_name;
+ }
+
+ protected function shouldConnectToMaster()
+ {
+ return $this->host->getResolvedProperty('master_should_connect') !== 'y';
+ }
+
+ protected function getParentZone()
+ {
+ if ($this->parentZone === null) {
+ $this->parentZone = $this->loadParentZone();
+ }
+
+ return $this->parentZone;
+ }
+
+ protected function loadParentZone()
+ {
+ $db = $this->db();
+
+ if ($zoneId = $this->host->getResolvedProperty('zone_id')) {
+ return IcingaZone::loadWithAutoIncId($zoneId, $db);
+ } else {
+ return IcingaZone::load($db->getMasterZoneName(), $db);
+ }
+ }
+
+ protected function getParentEndpoints()
+ {
+ if ($this->parentEndpoints === null) {
+ $this->parentEndpoints = $this->loadParentEndpoints();
+ }
+
+ return $this->parentEndpoints;
+ }
+
+ protected function loadParentEndpoints()
+ {
+ $db = $this->db()->getDbAdapter();
+
+ $query = $db->select()
+ ->from('icinga_endpoint')
+ ->where(
+ 'zone_id = ?',
+ $this->getParentZone()->get('id')
+ );
+
+ return IcingaEndpoint::loadAll(
+ $this->db(),
+ $query,
+ 'object_name'
+ );
+ }
+
+ /**
+ * Get the PKI ticket
+ *
+ * @return string
+ *
+ * @throws LogicException If ticket has not been set
+ */
+ protected function getTicket()
+ {
+ if ($this->ticket === null) {
+ throw new LogicException('Ticket is null');
+ }
+
+ return $this->ticket;
+ }
+
+ /**
+ * Set the PKI ticket
+ *
+ * @param string $ticket
+ *
+ * @return $this
+ */
+ public function setTicket($ticket)
+ {
+ $this->ticket = $ticket;
+ }
+
+ protected function loadPowershellModule()
+ {
+ return $this->getContribFile('windows-agent-installer/Icinga2Agent.psm1');
+ }
+
+ public function renderWindowsInstaller()
+ {
+ return $this->loadPowershellModule()
+ . "\n\n"
+ . 'exit Icinga2AgentModule `' . "\n "
+ . $this->renderPowershellParameters([
+ 'AgentName' => $this->host->getEndpointName(),
+ 'Ticket' => $this->getTicket(),
+ 'ParentZone' => $this->getParentZone()->getObjectName(),
+ 'ParentEndpoints' => array_keys($this->getParentEndpoints()),
+ 'CAServer' => $this->getCaServer(),
+ 'RunInstaller'
+ ]);
+ }
+
+ public function renderIcinga4WindowsWizardCommand($token)
+ {
+ $script = "Use-Icinga;\n"
+ . 'Start-IcingaAgentInstallWizard `' . "\n "
+ . $this->renderPowershellParameters([
+ 'DirectorUrl' => $this->getDirectorUrl(),
+ 'SelfServiceAPIKey' => $token,
+ 'UseDirectorSelfService' => 1,
+ 'OverrideDirectorVars' => 0,
+ 'Reconfigure',
+ 'RunInstaller'
+ ]);
+
+ return $script;
+ }
+
+ public function renderPowershellModuleInstaller($token, $withModule = false)
+ {
+ if ($withModule) {
+ $script = $this->loadPowershellModule() . "\n\n";
+ } else {
+ $script = '';
+ }
+
+ $script .= 'exit Icinga2AgentModule `' . "\n "
+ . $this->renderPowershellParameters([
+ 'DirectorUrl' => $this->getDirectorUrl(),
+ 'DirectorAuthToken' => $token,
+ 'RunInstaller'
+ ]);
+
+ return $script;
+ }
+
+ protected function getDirectorUrl()
+ {
+ $r = Icinga::app()->getRequest();
+ $scheme = $r->getServer('HTTP_X_FORWARDED_PROTO', $r->getScheme());
+
+ return sprintf(
+ '%s://%s%s/director/',
+ $scheme,
+ $r->getHttpHost(),
+ $r->getBaseUrl()
+ );
+ }
+
+ protected function renderPowershellParameters($parameters)
+ {
+ $maxKeyLength = max(array_map('strlen', array_keys($parameters)));
+ foreach ($parameters as $key => $value) {
+ if (is_int($key)) {
+ $maxKeyLength = max($maxKeyLength, strlen($value));
+ }
+ }
+ $parts = array();
+
+ foreach ($parameters as $key => $value) {
+ if (is_int($key)) {
+ $parts[] = $this->renderPowershellParameter($value, null, $maxKeyLength);
+ } else {
+ $parts[] = $this->renderPowershellParameter($key, $value, $maxKeyLength);
+ }
+ }
+
+ return implode(' `' . "\n ", $parts);
+ }
+
+ protected function renderPowershellParameter($key, $value, $maxKeyLength = null)
+ {
+ $ret = '-' . $key;
+ if ($value === null) {
+ return $ret;
+ }
+
+ $ret .= ' ';
+
+ if ($maxKeyLength !== null) {
+ $ret .= str_repeat(' ', $maxKeyLength - strlen($key));
+ }
+
+ if (is_array($value)) {
+ $vals = array();
+ foreach ($value as $val) {
+ $vals[] = $this->renderPowershellString($val);
+ }
+ $ret .= implode(', ', $vals);
+ } elseif (is_int($value)) {
+ $ret .= $value;
+ } else {
+ $ret .= $this->renderPowershellString($value);
+ }
+
+ return $ret;
+ }
+
+ protected function renderPowershellString($string)
+ {
+ // TODO: Escaping
+ return "'" . $string . "'";
+ }
+
+ protected function db()
+ {
+ if ($this->db === null) {
+ $this->db = $this->host->getConnection();
+ }
+
+ return $this->db;
+ }
+
+ public function renderLinuxInstaller()
+ {
+ $script = $this->loadBashModule();
+
+ $endpoints = [];
+ foreach ($this->getParentEndpoints() as $endpoint) {
+ $endpoints[$endpoint->getObjectName()] = $endpoint->get('host');
+ }
+
+ return $this->replaceBashTemplate($script, [
+ 'ICINGA2_NODENAME' => $this->host->getEndpointName(),
+ 'ICINGA2_CA_TICKET' => $this->getTicket(),
+ 'ICINGA2_PARENT_ZONE' => $this->getParentZone()->getObjectName(),
+ 'ICINGA2_PARENT_ENDPOINTS' => $endpoints,
+ 'ICINGA2_CA_NODE' => $this->getCaServer(),
+ 'ICINGA2_GLOBAL_ZONES' => [$this->db()->getDefaultGlobalZoneName()],
+ ]);
+ }
+
+ protected function loadBashModule()
+ {
+ return $this->getContribFile('linux-agent-installer/Icinga2Agent.bash');
+ }
+
+ protected function replaceBashTemplate($script, $parameters)
+ {
+ foreach ($parameters as $key => $value) {
+ $quotedKey = preg_quote($key, '~');
+ if (is_array($value)) {
+ $list = [];
+ foreach ($value as $k => $v) {
+ if (!is_numeric($k)) {
+ $v = "$k,$v";
+ }
+ $list[] = escapeshellarg($v);
+ }
+ $value = '(' . join(' ', $list) . ')';
+ } else {
+ $value = escapeshellarg($value);
+ }
+ $script = preg_replace("~^#?$quotedKey='@$quotedKey@'$~m", "${key}=${value}", $script);
+ }
+
+ return $script;
+ }
+
+ protected function renderBashParameter($key, $value)
+ {
+ $ret = $key . '=';
+
+ // Cheating, this doesn't really help. We should ship the rendered config
+ if (is_array($value) && count($value) === 1) {
+ $value = array_shift($value);
+ }
+
+ if (is_array($value)) {
+ $vals = array();
+ foreach ($value as $val) {
+ $vals[] = $this->renderPowershellString($val);
+ }
+ $ret .= '(' . implode(' ', $vals) . ')';
+ } else {
+ $ret .= $this->renderPowershellString($value);
+ }
+
+ return $ret;
+ }
+
+ protected function getContribDir()
+ {
+ return dirname(dirname(dirname(__DIR__))) . '/contrib';
+ }
+
+ protected function getContribFile($path)
+ {
+ return file_get_contents($this->getContribDir() . '/' . $path);
+ }
+}
diff --git a/library/Director/IcingaConfig/AssignRenderer.php b/library/Director/IcingaConfig/AssignRenderer.php
new file mode 100644
index 0000000..6acbfee
--- /dev/null
+++ b/library/Director/IcingaConfig/AssignRenderer.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use gipfl\Json\JsonString;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterEqualOrGreaterThan;
+use Icinga\Data\Filter\FilterEqualOrLessThan;
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Data\Filter\FilterGreaterThan;
+use Icinga\Data\Filter\FilterLessThan;
+use Icinga\Data\Filter\FilterMatch;
+use Icinga\Data\Filter\FilterMatchNot;
+use Icinga\Data\Filter\FilterNotEqual;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Director\Data\Json;
+use InvalidArgumentException;
+
+class AssignRenderer
+{
+ protected $filter;
+
+ public function __construct(Filter $filter)
+ {
+ $this->filter = $filter;
+ }
+
+ public static function forFilter(Filter $filter)
+ {
+ return new static($filter);
+ }
+
+ public function renderAssign()
+ {
+ return $this->render('assign');
+ }
+
+ public function renderIgnore()
+ {
+ return $this->render('ignore');
+ }
+
+ public function render($type)
+ {
+ return $type . ' where ' . $this->renderFilter($this->filter);
+ }
+
+ protected function renderFilter(Filter $filter)
+ {
+ if ($filter instanceof FilterNot) {
+ $parts = [];
+ foreach ($filter->filters() as $sub) {
+ $parts[] = $this->renderFilter($sub);
+ }
+
+ return '!(' . implode(' && ', $parts) . ')';
+ }
+ if ($filter->isChain()) {
+ /** @var FilterChain $filter */
+ return $this->renderFilterChain($filter);
+ } else {
+ /** @var FilterExpression $filter */
+ return $this->renderFilterExpression($filter);
+ }
+ }
+
+ protected function renderEquals($column, $expression)
+ {
+ if (substr($column, -7) === '.groups') {
+ return sprintf(
+ '%s in %s',
+ $expression,
+ $column
+ );
+ } else {
+ return sprintf(
+ '%s == %s',
+ $column,
+ $expression
+ );
+ }
+ }
+
+ protected function renderNotEquals($column, $expression)
+ {
+ if (substr($column, -7) === '.groups') {
+ return sprintf(
+ '!(%s in %s)',
+ $expression,
+ $column
+ );
+ } else {
+ return sprintf(
+ '%s != %s',
+ $column,
+ $expression
+ );
+ }
+ }
+
+ protected function renderInArray($column, $expression)
+ {
+ return sprintf(
+ '%s in %s',
+ $column,
+ $expression
+ );
+ }
+
+ protected function renderContains(FilterExpression $filter)
+ {
+ return sprintf(
+ '%s in %s',
+ $this->renderExpressionValue(json_decode($filter->getColumn())),
+ $filter->getExpression()
+ );
+ }
+
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ if ($this->columnIsJson($filter)) {
+ return $this->renderContains($filter);
+ }
+
+ $column = $filter->getColumn();
+ $rawExpression = Json::decode($filter->getExpression());
+ $expression = $this->renderExpressionValue($rawExpression);
+
+ if (is_array($rawExpression) && $filter instanceof FilterMatch) {
+ return $this->renderInArray($column, $expression);
+ }
+
+ if (is_string($rawExpression) && ctype_digit($rawExpression)) {
+ // TODO: doing this for compat reasons, should work for all filters
+ if ($filter instanceof FilterEqualOrGreaterThan
+ || $filter instanceof FilterGreaterThan
+ || $filter instanceof FilterEqualOrLessThan
+ || $filter instanceof FilterLessThan
+ ) {
+ $expression = $rawExpression;
+ }
+ }
+
+ if ($filter instanceof FilterEqual) {
+ if (is_array($rawExpression)) {
+ return sprintf(
+ '%s in %s',
+ $column,
+ $expression
+ );
+ } else {
+ return sprintf(
+ '%s == %s',
+ $column,
+ $expression
+ );
+ }
+ } elseif ($filter instanceof FilterMatch) {
+ if ($rawExpression === true) {
+ return $column;
+ }
+ if ($rawExpression === false) {
+ return sprintf(
+ '! %s',
+ $column
+ );
+ }
+ if (strpos($expression, '*') === false) {
+ return $this->renderEquals($column, $expression);
+ } else {
+ return sprintf(
+ 'match(%s, %s)',
+ $expression,
+ $column
+ );
+ }
+ } elseif ($filter instanceof FilterMatchNot) {
+ if (strpos($expression, '*') === false) {
+ return $this->renderNotEquals($column, $expression);
+ } else {
+ return sprintf(
+ '! match(%s, %s)',
+ $expression,
+ $column
+ );
+ }
+ } elseif ($filter instanceof FilterNotEqual) {
+ return sprintf(
+ '%s != %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterEqualOrGreaterThan) {
+ return sprintf(
+ '%s >= %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterEqualOrLessThan) {
+ return sprintf(
+ '%s <= %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterGreaterThan) {
+ return sprintf(
+ '%s > %s',
+ $column,
+ $expression
+ );
+ } elseif ($filter instanceof FilterLessThan) {
+ return sprintf(
+ '%s < %s',
+ $column,
+ $expression
+ );
+ } else {
+ throw new QueryException(
+ 'Filter expression of type "%s" is not supported',
+ get_class($filter)
+ );
+ }
+ }
+
+ protected function renderExpressionValue($value)
+ {
+ return IcingaConfigHelper::renderPhpValue($value);
+ }
+
+ protected function columnIsJson(FilterExpression $filter)
+ {
+ $col = $filter->getColumn();
+ return strlen($col) && $col[0] === '"';
+ }
+
+ protected function renderFilterChain(FilterChain $filter)
+ {
+ // TODO: brackets if deeper level?
+ if ($filter instanceof FilterAnd) {
+ $op = ' && ';
+ } elseif ($filter instanceof FilterOr) {
+ $op = ' || ';
+ } elseif ($filter instanceof FilterNot) {
+ throw new InvalidArgumentException('renderFilterChain should never get a FilterNot instance');
+ } else {
+ throw new InvalidArgumentException('Cannot render filter: %s', $filter);
+ }
+
+ $parts = array();
+ if (! $filter->isEmpty()) {
+ /** @var Filter $f */
+ foreach ($filter->filters() as $f) {
+ if ($f instanceof FilterChain && $f->count() > 1) {
+ $parts[] = '(' . $this->renderFilter($f) . ')';
+ } else {
+ $parts[] = $this->renderFilter($f);
+ }
+ }
+ }
+
+ return implode($op, $parts);
+ }
+}
diff --git a/library/Director/IcingaConfig/ExtensibleSet.php b/library/Director/IcingaConfig/ExtensibleSet.php
new file mode 100644
index 0000000..9120816
--- /dev/null
+++ b/library/Director/IcingaConfig/ExtensibleSet.php
@@ -0,0 +1,574 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\Objects\IcingaObject;
+
+class ExtensibleSet
+{
+ protected $ownValues;
+
+ protected $plusValues = [];
+
+ protected $minusValues = [];
+
+ protected $resolvedValues;
+
+ protected $allowedValues;
+
+ protected $inheritedValues = [];
+
+ protected $fromDb;
+
+ /**
+ * @var IcingaObject
+ */
+ protected $object;
+
+ /**
+ * Object property name pointing to this set
+ *
+ * This also implies set table called <object_table>_<propertyName>_set
+ *
+ * @var string
+ */
+ protected $propertyName;
+
+ public function __construct($values = null)
+ {
+ if (null !== $values) {
+ $this->override($values);
+ }
+ }
+
+ public static function forIcingaObject(IcingaObject $object, $propertyName)
+ {
+ $set = new static;
+ $set->object = $object;
+ $set->propertyName = $propertyName;
+
+ if ($object->hasBeenLoadedFromDb()) {
+ $set->loadFromDb();
+ }
+
+ return $set;
+ }
+
+ public function set($set)
+ {
+ if (null === $set) {
+ $this->reset();
+
+ return $this;
+ } elseif (is_array($set) || is_string($set)) {
+ $this->reset();
+ $this->override($set);
+ } elseif (is_object($set)) {
+ $this->reset();
+
+ foreach (['override', 'extend', 'blacklist'] as $method) {
+ if (property_exists($set, $method)) {
+ $this->$method($set->$method);
+ }
+ }
+ } else {
+ throw new ProgrammingError(
+ 'ExtensibleSet::set accepts only plain arrays or objects'
+ );
+ }
+
+ return $this;
+ }
+
+ public function isEmpty()
+ {
+ return $this->ownValues === null
+ && empty($this->plusValues)
+ && empty($this->minusValues);
+ }
+
+ public function toPlainObject()
+ {
+ if ($this->ownValues !== null) {
+ if (empty($this->minusValues) && empty($this->plusValues)) {
+ return $this->ownValues;
+ }
+ }
+
+ $plain = (object) [];
+
+ if ($this->ownValues !== null) {
+ $plain->override = $this->ownValues;
+ }
+ if (! empty($this->plusValues)) {
+ $plain->extend = $this->plusValues;
+ }
+ if (! empty($this->minusValues)) {
+ $plain->blacklist = $this->minusValues;
+ }
+
+ return $plain;
+ }
+
+ public function getPlainUnmodifiedObject()
+ {
+ if ($this->fromDb === null) {
+ return null;
+ }
+
+ $old = $this->fromDb;
+
+ if ($old['override'] !== null) {
+ if (empty($old['blacklist']) && empty($old['extend'])) {
+ return $old['override'];
+ }
+ }
+
+ $plain = (object) [];
+
+ if ($old['override'] !== null) {
+ $plain->override = $old['override'];
+ }
+ if (! empty($old['extend'])) {
+ $plain->extend = $old['extend'];
+ }
+ if (! empty($old['blacklist'])) {
+ $plain->blacklist = $old['blacklist'];
+ }
+
+ return $plain;
+ }
+
+ public function hasBeenLoadedFromDb()
+ {
+ return $this->fromDb !== null;
+ }
+
+ public function hasBeenModified()
+ {
+ if ($this->hasBeenLoadedFromDb()) {
+ if ($this->ownValues !== $this->fromDb['override']) {
+ return true;
+ }
+
+ if ($this->plusValues !== $this->fromDb['extend']) {
+ return true;
+ }
+
+ if ($this->minusValues !== $this->fromDb['blacklist']) {
+ return true;
+ }
+
+ return false;
+ } else {
+ if ($this->ownValues === null
+ && empty($this->plusValues)
+ && empty($this->minusValues)
+ ) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ protected function loadFromDb()
+ {
+ $db = $this->object->getDb();
+
+ $query = $db->select()->from($this->tableName(), [
+ 'property',
+ 'merge_behaviour'
+ ])->where($this->foreignKey() . ' = ?', $this->object->get('id'));
+
+ $byBehaviour = [
+ 'override' => [],
+ 'extend' => [],
+ 'blacklist' => [],
+ ];
+
+ foreach ($db->fetchAll($query) as $row) {
+ if (! array_key_exists($row->merge_behaviour, $byBehaviour)) {
+ throw new ProgrammingError(
+ 'Got unknown merge_behaviour "%s". Schema change?',
+ $row->merge_behaviour
+ );
+ }
+
+ $byBehaviour[$row->merge_behaviour][] = $row->property;
+ }
+
+ foreach ($byBehaviour as $method => &$values) {
+ if (empty($values)) {
+ continue;
+ }
+
+ sort($values);
+ $this->$method($values);
+ }
+
+ if (empty($byBehaviour['override'])) {
+ $byBehaviour['override'] = null;
+ }
+
+ $this->fromDb = $byBehaviour;
+
+ return $this;
+ }
+
+ protected function foreignKey()
+ {
+ return $this->object->getShortTableName() . '_id';
+ }
+
+ protected function tableName()
+ {
+ return implode('_', [
+ $this->object->getTableName(),
+ $this->propertyName,
+ 'set'
+ ]);
+ }
+
+ public function getObject()
+ {
+ return $this->object;
+ }
+
+ public function store()
+ {
+ if (null === $this->object) {
+ throw new ProgrammingError(
+ 'Cannot store ExtensibleSet with no assigned object'
+ );
+ }
+
+ if (! $this->hasBeenModified()) {
+ return false;
+ }
+
+ $this->storeToDb();
+ return true;
+ }
+
+ protected function storeToDb()
+ {
+ $db = $this->object->getDb();
+
+ if ($db === null) {
+ throw new ProgrammingError(
+ 'Cannot store a set for an unstored related object'
+ );
+ }
+
+ $table = $this->tableName();
+ $props = [
+ $this->foreignKey() => $this->object->get('id')
+ ];
+
+ $db->delete(
+ $this->tableName(),
+ $db->quoteInto(
+ $this->foreignKey() . ' = ?',
+ $this->object->get('id')
+ )
+ );
+
+ if ($this->ownValues !== null) {
+ $props['merge_behaviour'] = 'override';
+ foreach ($this->ownValues as $value) {
+ $db->insert(
+ $table,
+ array_merge($props, ['property' => $value])
+ );
+ }
+ }
+
+ if (! empty($this->plusValues)) {
+ $props['merge_behaviour'] = 'extend';
+ foreach ($this->plusValues as $value) {
+ $db->insert(
+ $table,
+ array_merge($props, ['property' => $value])
+ );
+ }
+ }
+
+ if (! empty($this->minusValues)) {
+ $props['merge_behaviour'] = 'blacklist';
+ foreach ($this->minusValues as $value) {
+ $db->insert(
+ $table,
+ array_merge($props, ['property' => $value])
+ );
+ }
+ }
+
+ $this->setBeingLoadedFromDb();
+ }
+
+ public function setBeingLoadedFromDb()
+ {
+ $this->fromDb = [
+ 'override' => $this->ownValues ?: [],
+ 'extend' => $this->plusValues ?: [],
+ 'blacklist' => $this->minusValues ?: [],
+ ];
+ }
+
+ public function override($values)
+ {
+ $this->ownValues = [];
+ $this->inheritedValues = [];
+
+ $this->addValuesTo($this->ownValues, $values);
+
+ return $this->addResolvedValues($values);
+ }
+
+ public function extend($values)
+ {
+ $this->addValuesTo($this->plusValues, $values);
+ return $this->addResolvedValues($values);
+ }
+
+ public function blacklist($values)
+ {
+ $this->addValuesTo($this->minusValues, $values);
+
+ if ($this->hasBeenResolved()) {
+ $this->removeValuesFrom($this->resolvedValues, $values);
+ }
+
+ return $this;
+ }
+
+ public function getResolvedValues()
+ {
+ if (! $this->hasBeenResolved()) {
+ $this->recalculate();
+ }
+
+ sort($this->resolvedValues);
+
+ return $this->resolvedValues;
+ }
+
+ public function inheritFrom(ExtensibleSet $parent)
+ {
+ if ($this->ownValues !== null) {
+ return $this;
+ }
+
+ if ($this->hasBeenResolved()) {
+ $this->resolvedValues = null;
+ }
+
+ $this->inheritedValues = [];
+
+ $this->addValuesTo(
+ $this->inheritedValues,
+ $this->stripBlacklistedValues($parent->getResolvedValues())
+ );
+
+ return $this->recalculate();
+ }
+
+ public function forgetInheritedValues()
+ {
+ $this->inheritedValues = [];
+ return $this;
+ }
+
+ protected function renderArray($array)
+ {
+ $safe = [];
+ foreach ($array as $value) {
+ $safe[] = c::alreadyRendered($value);
+ }
+
+ return c::renderArray($safe);
+ }
+
+ public function renderAs($key, $prefix = ' ')
+ {
+ $parts = [];
+
+ // TODO: It would be nice if we could use empty arrays to override
+ // inherited ones
+ // if ($this->ownValues !== null) {
+ if (!empty($this->ownValues)) {
+ $parts[] = c::renderKeyValue(
+ $key,
+ $this->renderArray($this->ownValues),
+ $prefix
+ );
+ }
+
+ if (!empty($this->plusValues)) {
+ $parts[] = c::renderKeyOperatorValue(
+ $key,
+ '+=',
+ $this->renderArray($this->plusValues),
+ $prefix
+ );
+ }
+
+ if (!empty($this->minusValues)) {
+ $parts[] = c::renderKeyOperatorValue(
+ $key,
+ '-=',
+ $this->renderArray($this->minusValues),
+ $prefix
+ );
+ }
+
+ return implode('', $parts);
+ }
+
+ public function isRestricted()
+ {
+ return $this->allowedValues === null;
+ }
+
+ public function enumAllowedValues()
+ {
+ if ($this->isRestricted()) {
+ throw new ProgrammingError(
+ 'No allowed value set available, this set is not restricted'
+ );
+ }
+
+ if (empty($this->allowedValues)) {
+ return [];
+ }
+
+ return array_combine($this->allowedValues, $this->allowedValues);
+ }
+
+ protected function hasBeenResolved()
+ {
+ return $this->resolvedValues !== null;
+ }
+
+ protected function stripBlacklistedValues($array)
+ {
+ $this->removeValuesFrom($array, $this->minusValues);
+
+ return $array;
+ }
+
+ protected function assertValidValue($value)
+ {
+ if (null === $this->allowedValues) {
+ return $this;
+ }
+
+ if (in_array($value, $this->allowedValues)) {
+ return $this;
+ }
+
+ throw new InvalidPropertyException(
+ 'Got invalid property "%s", allowed are: (%s)',
+ $value,
+ implode(', ', $this->allowedValues)
+ );
+ }
+
+ protected function addValuesTo(&$array, $values)
+ {
+ foreach ($this->wantArray($values) as $value) {
+ // silently ignore null or empty strings
+ if (strlen($value) === 0) {
+ continue;
+ }
+
+ $this->addTo($array, $value);
+ }
+
+ return $this;
+ }
+
+ protected function addResolvedValues($values)
+ {
+ if (! $this->hasBeenResolved()) {
+ $this->resolvedValues = [];
+ }
+
+ return $this->addValuesTo(
+ $this->resolvedValues,
+ $this->stripBlacklistedValues($this->wantArray($values))
+ );
+ }
+
+ protected function removeValuesFrom(&$array, $values)
+ {
+ foreach ($this->wantArray($values) as $value) {
+ $this->removeFrom($array, $value);
+ }
+
+ return $this;
+ }
+
+ protected function addTo(&$array, $value)
+ {
+ if (! in_array($value, $array)) {
+ $this->assertValidValue($value);
+ $array[] = $value;
+ }
+
+ return $this;
+ }
+
+ protected function removeFrom(&$array, $value)
+ {
+ if (false !== ($pos = array_search($value, $array))) {
+ unset($array[$pos]);
+ }
+
+ return $this;
+ }
+
+ protected function recalculate()
+ {
+ $this->resolvedValues = [];
+
+ if ($this->ownValues === null) {
+ $this->addValuesTo($this->resolvedValues, $this->inheritedValues);
+ } else {
+ $this->addValuesTo($this->resolvedValues, $this->ownValues);
+ }
+ $this->addValuesTo($this->resolvedValues, $this->plusValues);
+ $this->removeFrom($this->resolvedValues, $this->minusValues);
+
+ return $this;
+ }
+
+ protected function reset()
+ {
+ $this->ownValues = null;
+ $this->plusValues = [];
+ $this->minusValues = [];
+ $this->resolvedValues = null;
+ $this->inheritedValues = [];
+
+ return $this;
+ }
+
+ protected function translate($string)
+ {
+ return mt('director', $string);
+ }
+
+ protected function wantArray($values)
+ {
+ if (is_array($values)) {
+ return $values;
+ }
+
+ return [$values];
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfig.php b/library/Director/IcingaConfig/IcingaConfig.php
new file mode 100644
index 0000000..72edd7e
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfig.php
@@ -0,0 +1,781 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Application\Benchmark;
+use Icinga\Application\Hook;
+use Icinga\Application\Icinga;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Director\Application\MemoryLimit;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Hook\ShipConfigFilesHook;
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Objects\IcingaHost;
+use Icinga\Module\Director\Objects\IcingaZone;
+use InvalidArgumentException;
+use LogicException;
+use RuntimeException;
+
+class IcingaConfig
+{
+ protected $files = array();
+
+ protected $checksum;
+
+ protected $zoneMap = array();
+
+ protected $lastActivityChecksum;
+
+ /** @var \Zend_Db_Adapter_Abstract */
+ protected $db;
+
+ protected $connection;
+
+ protected $generationTime;
+
+ protected $configFormat;
+
+ protected $deploymentModeV1;
+
+ public static $table = 'director_generated_config';
+
+ public function __construct(Db $connection)
+ {
+ // Make sure module hooks are loaded:
+ Icinga::app()->getModuleManager()->loadEnabledModules();
+
+ $this->connection = $connection;
+ $this->db = $connection->getDbAdapter();
+ $this->configFormat = $this->connection->settings()->config_format;
+ $this->deploymentModeV1 = $this->connection->settings()->deployment_mode_v1;
+ }
+
+ public function getSize()
+ {
+ $size = 0;
+ foreach ($this->getFiles() as $file) {
+ $size += $file->getSize();
+ }
+ return $size;
+ }
+
+ public function getDuration()
+ {
+ return $this->generationTime;
+ }
+
+ public function getFileCount()
+ {
+ return count($this->files);
+ }
+
+ public function getConfigFormat()
+ {
+ return $this->configFormat;
+ }
+
+ public function getDeploymentMode()
+ {
+ if ($this->isLegacy()) {
+ return $this->deploymentModeV1;
+ } else {
+ throw new LogicException('There is no deployment mode for Icinga 2 config format!');
+ }
+ }
+
+ public function setConfigFormat($format)
+ {
+ if (! in_array($format, array('v1', 'v2'))) {
+ throw new InvalidArgumentException(sprintf(
+ 'Only Icinga v1 and v2 config format is supported, got "%s"',
+ $format
+ ));
+ }
+
+ $this->configFormat = $format;
+
+ return $this;
+ }
+
+ public function isLegacy()
+ {
+ return $this->configFormat === 'v1';
+ }
+
+ public function getObjectCount()
+ {
+ $cnt = 0;
+ foreach ($this->getFiles() as $file) {
+ $cnt += $file->getObjectCount();
+ }
+ return $cnt;
+ }
+
+ public function getTemplateCount()
+ {
+ $cnt = 0;
+ foreach ($this->getFiles() as $file) {
+ $cnt += $file->getTemplateCount();
+ }
+ return $cnt;
+ }
+
+ public function getApplyCount()
+ {
+ $cnt = 0;
+ foreach ($this->getFiles() as $file) {
+ $cnt += $file->getApplyCount();
+ }
+ return $cnt;
+ }
+
+ public function getChecksum()
+ {
+ return $this->checksum;
+ }
+
+ public function getHexChecksum()
+ {
+ return bin2hex($this->checksum);
+ }
+
+ /**
+ * @return IcingaConfigFile[]
+ */
+ public function getFiles()
+ {
+ return $this->files;
+ }
+
+ public function getFileContents()
+ {
+ $result = array();
+ foreach ($this->files as $name => $file) {
+ $result[$name] = $file->getContent();
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return array
+ */
+ public function getFileNames()
+ {
+ return array_keys($this->files);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return IcingaConfigFile
+ */
+ public function getFile($name)
+ {
+ return $this->files[$name];
+ }
+
+ /**
+ * @param string $checksum
+ * @param Db $connection
+ *
+ * @return static
+ */
+ public static function load($checksum, Db $connection)
+ {
+ $config = new static($connection);
+ $config->loadFromDb($checksum);
+ return $config;
+ }
+
+ /**
+ * @param string $checksum
+ * @param Db $connection
+ *
+ * @return bool
+ */
+ public static function exists($checksum, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('c' => self::$table),
+ array('checksum' => $connection->dbHexFunc('c.checksum'))
+ )->where(
+ 'checksum = ?',
+ $connection->quoteBinary(hex2bin($checksum))
+ );
+
+ return $db->fetchOne($query) === $checksum;
+ }
+
+ public static function loadByActivityChecksum($checksum, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('c' => self::$table),
+ array('checksum' => 'c.checksum')
+ )->join(
+ array('l' => 'director_activity_log'),
+ 'l.checksum = c.last_activity_checksum',
+ array()
+ )->where(
+ 'last_activity_checksum = ?',
+ $connection->quoteBinary(hex2bin($checksum))
+ )->order('l.id DESC')->limit(1);
+
+ return self::load($db->fetchOne($query), $connection);
+ }
+
+ public static function existsForActivityChecksum($checksum, Db $connection)
+ {
+ $db = $connection->getDbAdapter();
+ $query = $db->select()->from(
+ array('c' => self::$table),
+ array('checksum' => $connection->dbHexFunc('c.checksum'))
+ )->join(
+ array('l' => 'director_activity_log'),
+ 'l.checksum = c.last_activity_checksum',
+ array()
+ )->where(
+ 'last_activity_checksum = ?',
+ $connection->quoteBinary(hex2bin($checksum))
+ )->order('l.id DESC')->limit(1);
+
+ return $db->fetchOne($query) === $checksum;
+ }
+
+ /**
+ * @param Db $connection
+ *
+ * @return mixed
+ */
+ public static function generate(Db $connection)
+ {
+ $config = new static($connection);
+ return $config->storeIfModified();
+ }
+
+ public static function wouldChange(Db $connection)
+ {
+ $config = new static($connection);
+ return $config->hasBeenModified();
+ }
+
+ public function hasBeenModified()
+ {
+ $this->generateFromDb();
+ $this->collectExtraFiles();
+ $checksum = $this->calculateChecksum();
+ $activity = $this->getLastActivityChecksum();
+
+ $lastActivity = $this->connection->binaryDbResult(
+ $this->db->fetchOne(
+ $this->db->select()->from(
+ self::$table,
+ 'last_activity_checksum'
+ )->where(
+ 'checksum = ?',
+ $this->dbBin($checksum)
+ )
+ )
+ );
+
+ if ($lastActivity === false || $lastActivity === null) {
+ return true;
+ }
+
+ if ($lastActivity !== $activity) {
+ $this->db->update(
+ self::$table,
+ array(
+ 'last_activity_checksum' => $this->dbBin($activity)
+ ),
+ $this->db->quoteInto('checksum = ?', $this->dbBin($checksum))
+ );
+ }
+
+ return false;
+ }
+
+ protected function storeIfModified()
+ {
+ if ($this->hasBeenModified()) {
+ $this->store();
+ }
+
+ return $this;
+ }
+
+ protected function dbBin($binary)
+ {
+ return $this->connection->quoteBinary($binary);
+ }
+
+ protected function calculateChecksum()
+ {
+ $files = array();
+ $sortedFiles = $this->files;
+ ksort($sortedFiles);
+ /** @var IcingaConfigFile $file */
+ foreach ($sortedFiles as $name => $file) {
+ $files[] = $name . '=' . $file->getHexChecksum();
+ }
+
+ $this->checksum = sha1(implode(';', $files), true);
+ return $this->checksum;
+ }
+
+ public function getFilesChecksums()
+ {
+ $checksums = array();
+
+ /** @var IcingaConfigFile $file */
+ foreach ($this->files as $name => $file) {
+ $checksums[] = $file->getChecksum();
+ }
+
+ return $checksums;
+ }
+
+ // TODO: prepare lookup cache if empty?
+ public function getZoneName($id)
+ {
+ if (! array_key_exists($id, $this->zoneMap)) {
+ $zone = IcingaZone::loadWithAutoIncId($id, $this->connection);
+ $this->zoneMap[$id] = $zone->get('object_name');
+ }
+
+ return $this->zoneMap[$id];
+ }
+
+ /**
+ * @return self
+ */
+ public function store()
+ {
+ $fileTable = IcingaConfigFile::$table;
+ $fileKey = IcingaConfigFile::$keyName;
+
+ $existingQuery = $this->db->select()
+ ->from($fileTable, 'checksum')
+ ->where('checksum IN (?)', array_map(array($this, 'dbBin'), $this->getFilesChecksums()));
+
+ $existing = $this->db->fetchCol($existingQuery);
+
+ foreach ($existing as $key => $val) {
+ if (is_resource($val)) {
+ $existing[$key] = stream_get_contents($val);
+ }
+ }
+
+ $missing = array_diff($this->getFilesChecksums(), $existing);
+ $stored = array();
+
+ /** @var IcingaConfigFile $file */
+ foreach ($this->files as $name => $file) {
+ $checksum = $file->getChecksum();
+ if (! in_array($checksum, $missing)) {
+ continue;
+ }
+
+ if (array_key_exists($checksum, $stored)) {
+ continue;
+ }
+
+ $stored[$checksum] = true;
+
+ $this->db->insert(
+ $fileTable,
+ array(
+ $fileKey => $this->dbBin($checksum),
+ 'content' => $file->getContent(),
+ 'cnt_object' => $file->getObjectCount(),
+ 'cnt_template' => $file->getTemplateCount()
+ )
+ );
+ }
+
+ $activity = $this->dbBin($this->getLastActivityChecksum());
+ $this->db->beginTransaction();
+ try {
+ $this->db->insert(self::$table, [
+ 'duration' => $this->generationTime,
+ 'first_activity_checksum' => $activity,
+ 'last_activity_checksum' => $activity,
+ 'checksum' => $this->dbBin($this->getChecksum()),
+ ]);
+ /** @var IcingaConfigFile $file */
+ foreach ($this->files as $name => $file) {
+ $this->db->insert('director_generated_config_file', [
+ 'config_checksum' => $this->dbBin($this->getChecksum()),
+ 'file_checksum' => $this->dbBin($file->getChecksum()),
+ 'file_path' => $name,
+ ]);
+ }
+ $this->db->commit();
+ } catch (\Exception $e) {
+ try {
+ $this->db->rollBack();
+ } catch (\Exception $ignored) {
+ // Well...
+ }
+
+ throw $e;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function generateFromDb()
+ {
+ PrefetchCache::initialize($this->connection);
+ $start = microtime(true);
+
+ MemoryLimit::raiseTo('1024M');
+ ini_set('max_execution_time', 0);
+ // Workaround for https://bugs.php.net/bug.php?id=68606 or similar
+ ini_set('zend.enable_gc', 0);
+
+ if (! $this->connection->isPgsql() && $this->db->quote("1\0") !== '\'1\\0\'') {
+ throw new RuntimeException(
+ 'Refusing to render the configuration, your DB layer corrupts binary data.'
+ . ' You might be affected by Zend Framework bug #655'
+ );
+ }
+
+ $this
+ ->prepareGlobalBasics()
+ ->createFileFromDb('zone')
+ ->createFileFromDb('endpoint')
+ ->createFileFromDb('command')
+ ->createFileFromDb('timePeriod')
+ ->createFileFromDb('hostGroup')
+ ->createFileFromDb('host')
+ ->createFileFromDb('serviceGroup')
+ ->createFileFromDb('service')
+ ->createFileFromDb('serviceSet')
+ ->createFileFromDb('userGroup')
+ ->createFileFromDb('user')
+ ->createFileFromDb('notification')
+ ->createFileFromDb('dependency')
+ ->createFileFromDb('scheduledDowntime')
+ ;
+
+ PrefetchCache::forget();
+ IcingaHost::clearAllPrefetchCaches();
+
+ $this->generationTime = (int) ((microtime(true) - $start) * 1000);
+
+ return $this;
+ }
+
+ /**
+ * @return self
+ */
+ protected function prepareGlobalBasics()
+ {
+ if ($this->isLegacy()) {
+ $this->configFile(
+ sprintf(
+ 'director/%s/001-director-basics',
+ $this->connection->getDefaultGlobalZoneName()
+ ),
+ '.cfg'
+ )->prepend(
+ $this->renderLegacyDefaultNotification()
+ );
+
+ return $this;
+ }
+
+ $this->configFile(
+ sprintf(
+ 'zones.d/%s/001-director-basics',
+ $this->connection->getDefaultGlobalZoneName()
+ )
+ )->prepend(
+ "\nconst DirectorStageDir = dirname(dirname(current_filename))\n"
+ . $this->renderFlappingLogHelper()
+ . $this->renderHostOverridableVars()
+ );
+
+ return $this;
+ }
+
+ protected function renderFlappingLogHelper()
+ {
+ return '
+globals.directorWarnedOnceForThresholds = false;
+globals.directorWarnOnceForThresholds = function() {
+ if (globals.directorWarnedOnceForThresholds == false) {
+ globals.directorWarnedOnceForThresholds = true
+ log(LogWarning, "config", "Director: flapping_threshold_high/low is not supported in this Icinga 2 version!")
+ }
+}
+';
+ }
+
+ protected function renderHostOverridableVars()
+ {
+ $settings = $this->connection->settings();
+
+ return sprintf(
+ '
+const DirectorOverrideTemplate = "%s"
+if (! globals.contains(DirectorOverrideTemplate)) {
+ const DirectorOverrideVars = "%s"
+
+ globals.directorWarnedOnceForServiceWithoutHost = false;
+ globals.directorWarnOnceForServiceWithoutHost = function() {
+ if (globals.directorWarnedOnceForServiceWithoutHost == false) {
+ globals.directorWarnedOnceForServiceWithoutHost = true
+ log(
+ LogWarning,
+ "config",
+ "Director: Custom Variable Overrides will not work in this Icinga 2 version. See Director issue #1579"
+ )
+ }
+ }
+
+ template Service DirectorOverrideTemplate {
+ /**
+ * Seems that host is missing when used in a service object, works fine for
+ * apply rules
+ */
+ if (! host) {
+ var host = get_host(host_name)
+ }
+ if (! host) {
+ globals.directorWarnOnceForServiceWithoutHost()
+ }
+
+ if (vars) {
+ vars += host.vars[DirectorOverrideVars][name]
+ } else {
+ vars = host.vars[DirectorOverrideVars][name]
+ }
+ }
+}
+',
+ $settings->override_services_templatename,
+ $settings->override_services_varname
+ );
+ }
+
+ /**
+ * @param string $checksum
+ *
+ * @throws NotFoundError
+ *
+ * @return self
+ */
+ protected function loadFromDb($checksum)
+ {
+ $query = $this->db->select()->from(
+ self::$table,
+ array('checksum', 'last_activity_checksum', 'duration')
+ )->where('checksum = ?', $this->dbBin($checksum));
+ $result = $this->db->fetchRow($query);
+
+ if (empty($result)) {
+ throw new NotFoundError('Got no config for %s', bin2hex($checksum));
+ }
+
+ $this->checksum = $this->connection->binaryDbResult($result->checksum);
+ $this->generationTime = $result->duration;
+ $this->lastActivityChecksum = $this->connection->binaryDbResult($result->last_activity_checksum);
+
+ $query = $this->db->select()->from(
+ array('cf' => 'director_generated_config_file'),
+ array(
+ 'file_path' => 'cf.file_path',
+ 'checksum' => 'f.checksum',
+ 'content' => 'f.content',
+ 'cnt_object' => 'f.cnt_object',
+ 'cnt_template' => 'f.cnt_template',
+ 'cnt_apply' => 'f.cnt_apply',
+ )
+ )->join(
+ array('f' => 'director_generated_file'),
+ 'cf.file_checksum = f.checksum',
+ array()
+ )->where('cf.config_checksum = ?', $this->dbBin($checksum));
+
+ foreach ($this->db->fetchAll($query) as $row) {
+ $file = new IcingaConfigFile();
+ $this->files[$row->file_path] = $file
+ ->setContent($row->content)
+ ->setObjectCount($row->cnt_object)
+ ->setTemplateCount($row->cnt_template)
+ ->setApplyCount($row->cnt_apply);
+ }
+
+ return $this;
+ }
+
+ protected function createFileFromDb($type)
+ {
+ /** @var IcingaObject $class */
+ $class = 'Icinga\\Module\\Director\\Objects\\Icinga' . ucfirst($type);
+ Benchmark::measure(sprintf('Prefetching %s', $type));
+ $objects = $class::prefetchAll($this->connection);
+ return $this->createFileForObjects($type, $objects);
+ }
+
+ /**
+ * @param string $type Short object type, like 'service' or 'zone'
+ * @param IcingaObject[] $objects
+ *
+ * @return self
+ */
+ protected function createFileForObjects($type, $objects)
+ {
+ if (empty($objects)) {
+ return $this;
+ }
+
+ Benchmark::measure(sprintf('Generating %ss: %s', $type, count($objects)));
+ foreach ($objects as $object) {
+ if ($object->isExternal()) {
+ if ($type === 'zone') {
+ $this->zoneMap[$object->get('id')] = $object->getObjectName();
+ }
+ }
+ $object->renderToConfig($this);
+ }
+
+ Benchmark::measure(sprintf('%ss done', $type));
+ return $this;
+ }
+
+ protected function typeWantsGlobalZone($type)
+ {
+ $types = array(
+ 'command',
+ );
+
+ return in_array($type, $types);
+ }
+
+ protected function typeWantsMasterZone($type)
+ {
+ $types = array(
+ 'host',
+ 'hostGroup',
+ 'service',
+ 'serviceGroup',
+ 'endpoint',
+ 'user',
+ 'userGroup',
+ 'timePeriod',
+ 'notification',
+ 'dependency'
+ );
+
+ return in_array($type, $types);
+ }
+
+ /**
+ * @param string $name Relative config file name
+ * @param string $suffix Config file suffix, defaults to '.conf'
+ *
+ * @return IcingaConfigFile
+ */
+ public function configFile($name, $suffix = '.conf')
+ {
+ $filename = $name . $suffix;
+ if (! array_key_exists($filename, $this->files)) {
+ $this->files[$filename] = new IcingaConfigFile();
+ }
+
+ return $this->files[$filename];
+ }
+
+ protected function collectExtraFiles()
+ {
+ /** @var ShipConfigFilesHook $hook */
+ foreach (Hook::all('Director\\ShipConfigFiles') as $hook) {
+ foreach ($hook->fetchFiles() as $filename => $file) {
+ if (array_key_exists($filename, $this->files)) {
+ throw new LogicException(sprintf(
+ 'Cannot ship one file twice: %s',
+ $filename
+ ));
+ }
+ if ($file instanceof IcingaConfigFile) {
+ $this->files[$filename] = $file;
+ } else {
+ $this->configFile($filename, '')->setContent((string) $file);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ public function getLastActivityHexChecksum()
+ {
+ return bin2hex($this->getLastActivityChecksum());
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getLastActivityChecksum()
+ {
+ if ($this->lastActivityChecksum === null) {
+ $query = $this->db->select()
+ ->from('director_activity_log', 'checksum')
+ ->order('id DESC')
+ ->limit(1);
+
+ $this->lastActivityChecksum = $this->db->fetchOne($query);
+
+ // PgSQL workaround:
+ if (is_resource($this->lastActivityChecksum)) {
+ $this->lastActivityChecksum = stream_get_contents($this->lastActivityChecksum);
+ }
+ }
+
+ return $this->lastActivityChecksum;
+ }
+
+ protected function renderLegacyDefaultNotification()
+ {
+ return preg_replace('~^ {12}~m', '', '
+ #
+ # Default objects to avoid warnings
+ #
+
+ define contact {
+ contact_name icingaadmin
+ alias Icinga Admin
+ host_notifications_enabled 0
+ host_notification_commands notify-never-default
+ host_notification_period notification_none
+ service_notifications_enabled 0
+ service_notification_commands notify-never-default
+ service_notification_period notification_none
+ }
+
+ define contactgroup {
+ contactgroup_name icingaadmins
+ members icingaadmin
+ }
+
+ define timeperiod {
+ timeperiod_name notification_none
+ alias No Notifications
+ }
+
+ define command {
+ command_name notify-never-default
+ command_line /bin/echo "NOOP"
+ }
+ ');
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigFile.php b/library/Director/IcingaConfig/IcingaConfigFile.php
new file mode 100644
index 0000000..109eb8a
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigFile.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use Icinga\Module\Director\Objects\IcingaObject;
+use Icinga\Module\Director\Util;
+
+class IcingaConfigFile
+{
+ public static $table = 'director_generated_file';
+
+ public static $keyName = 'checksum';
+
+ protected $content;
+
+ protected $checksum;
+
+ protected $cntObject = 0;
+
+ protected $cntTemplate = 0;
+
+ protected $cntApply = 0;
+
+ /**
+ * @param $content
+ *
+ * @return self
+ */
+ public function prepend($content)
+ {
+ $this->content = $content . $this->content;
+ $this->checksum = null;
+ return $this;
+ }
+
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ public function setContent($content)
+ {
+ $this->content = $content;
+ $this->checksum = null;
+ return $this;
+ }
+
+ public function addContent($content)
+ {
+ if ($this->content === null) {
+ $this->content = $content;
+ } else {
+ $this->content .= $content;
+ }
+ $this->checksum = null;
+ return $this;
+ }
+
+ public function getObjectCount()
+ {
+ return $this->cntObject;
+ }
+
+ public function getTemplateCount()
+ {
+ return $this->cntTemplate;
+ }
+
+ public function getApplyCount()
+ {
+ return $this->cntApply;
+ }
+
+ public function getSize()
+ {
+ return strlen($this->content);
+ }
+
+ public function setObjectCount($cnt)
+ {
+ $this->cntObject = $cnt;
+ return $this;
+ }
+
+ public function setTemplateCount($cnt)
+ {
+ $this->cntTemplate = $cnt;
+ return $this;
+ }
+
+ public function setApplyCount($cnt)
+ {
+ $this->cntApply = $cnt;
+ return $this;
+ }
+
+ public function getHexChecksum()
+ {
+ return bin2hex($this->getChecksum());
+ }
+
+ public function getChecksum()
+ {
+ if ($this->checksum === null) {
+ $this->checksum = sha1($this->content, true);
+ }
+
+ return $this->checksum;
+ }
+
+ public function addLegacyObjects($objects)
+ {
+ foreach ($objects as $object) {
+ $this->addLegacyObject($object);
+ }
+
+ return $this;
+ }
+
+ public function addObjects($objects)
+ {
+ foreach ($objects as $object) {
+ $this->addObject($object);
+ }
+
+ return $this;
+ }
+
+ public function addObject(IcingaObject $object)
+ {
+ $this->content .= $object->toConfigString();
+ $this->checksum = null;
+ return $this->addObjectStats($object);
+ }
+
+ public function addLegacyObject(IcingaObject $object)
+ {
+ $this->content .= $object->toLegacyConfigString();
+ $this->checksum = null;
+ return $this->addObjectStats($object);
+ }
+
+ protected function addObjectStats(IcingaObject $object)
+ {
+ if ($object->hasProperty('object_type')) {
+ $type = $object->object_type;
+
+ switch ($type) {
+ case 'object':
+ $this->cntObject++;
+ break;
+ case 'template':
+ $this->cntTemplate++;
+ break;
+ case 'apply':
+ $this->cntApply++;
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ public function __toString()
+ {
+ return $this->getContent();
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigHelper.php b/library/Director/IcingaConfig/IcingaConfigHelper.php
new file mode 100644
index 0000000..03c017e
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigHelper.php
@@ -0,0 +1,430 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use InvalidArgumentException;
+use function ctype_digit;
+use function explode;
+use function floor;
+use function implode;
+use function preg_match;
+use function preg_split;
+use function sprintf;
+use function strlen;
+use function strpos;
+use function substr;
+
+class IcingaConfigHelper
+{
+ /**
+ * Reserved words according to
+ * https://icinga.com/docs/icinga2/latest/doc/17-language-reference/#reserved-keywords
+ */
+ protected static $reservedWords = [
+ 'object',
+ 'template',
+ 'include',
+ 'include_recursive',
+ 'include_zones',
+ 'library',
+ 'null',
+ 'true',
+ 'false',
+ 'const',
+ 'var',
+ 'this',
+ 'globals',
+ 'locals',
+ 'use',
+ 'default',
+ 'ignore_on_error',
+ 'current_filename',
+ 'current_line',
+ 'apply',
+ 'to',
+ 'where',
+ 'import',
+ 'assign',
+ 'ignore',
+ 'function',
+ 'return',
+ 'break',
+ 'continue',
+ 'for',
+ 'if',
+ 'else',
+ 'while',
+ 'throw',
+ 'try',
+ 'except',
+ 'in',
+ 'using',
+ 'namespace',
+ ];
+
+ public static function renderKeyValue($key, $value, $prefix = ' ')
+ {
+ return self::renderKeyOperatorValue($key, '=', $value, $prefix);
+ }
+
+ public static function renderKeyOperatorValue($key, $operator, $value, $prefix = ' ')
+ {
+ $string = sprintf(
+ "%s %s %s",
+ $key,
+ $operator,
+ $value
+ );
+
+ if ($prefix && strpos($string, "\n") !== false) {
+ return $prefix . implode("\n" . $prefix, explode("\n", $string)) . "\n";
+ }
+
+ return $prefix . $string . "\n";
+ }
+
+ public static function renderBoolean($value)
+ {
+ if ($value === 'y' || $value === true) {
+ return 'true';
+ }
+ if ($value === 'n' || $value === false) {
+ return 'false';
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ '%s is not a valid boolean',
+ $value
+ ));
+ }
+
+ protected static function renderInteger($value)
+ {
+ return (string) $value;
+ }
+
+ public static function renderFloat($value)
+ {
+ // Render .0000 floats as integers, mainly because of some JSON
+ // implementations:
+ if ((string) (int) $value === (string) $value) {
+ return static::renderInteger((int) $value);
+ }
+
+ return sprintf('%F', $value);
+ }
+
+ protected static function renderNull()
+ {
+ return 'null';
+ }
+
+ // TODO: Find out how to allow multiline {{{...}}} strings.
+ // Parameter? Dedicated method? Always if \n is found?
+ public static function renderString($string)
+ {
+ $special = [
+ '/\\\/',
+ '/"/',
+ '/\$/',
+ '/\t/',
+ '/\r/',
+ '/\n/',
+ // '/\b/', -> doesn't work
+ '/\f/',
+ ];
+
+ $replace = [
+ '\\\\\\',
+ '\\"',
+ '\\$',
+ '\\t',
+ '\\r',
+ '\\n',
+ // '\\b',
+ '\\f',
+ ];
+
+ $string = preg_replace($special, $replace, $string);
+
+ return '"' . $string . '"';
+ }
+
+ public static function renderPhpValue($value)
+ {
+ if (is_null($value)) {
+ return static::renderNull();
+ }
+ if (is_bool($value)) {
+ return static::renderBoolean($value);
+ }
+ if (is_int($value)) {
+ return static::renderInteger($value);
+ }
+ if (is_float($value)) {
+ return static::renderFloat($value);
+ }
+ // TODO:
+ // if (is_object($value) || static::isAssocArray($value)) {
+ // return static::renderHash($value, $prefix)
+ // TODO: also check array
+ if (is_array($value)) {
+ return static::renderArray($value);
+ }
+ if (is_string($value)) {
+ return static::renderString($value);
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ 'Unexpected type %s',
+ var_export($value, 1)
+ ));
+ }
+
+ public static function renderDictionaryKey($key)
+ {
+ if (preg_match('/^[a-z_]+[a-z0-9_]*$/i', $key)) {
+ return static::escapeIfReserved($key);
+ }
+
+ return static::renderString($key);
+ }
+
+ // Requires an array
+ public static function renderArray($array)
+ {
+ $data = [];
+ foreach ($array as $entry) {
+ if ($entry instanceof IcingaConfigRenderer) {
+ $data[] = $entry;
+ } else {
+ $data[] = self::renderString($entry);
+ }
+ }
+
+ return static::renderEscapedArray($data);
+ }
+
+ public static function renderEscapedArray($array)
+ {
+ $str = '[ ' . implode(', ', $array) . ' ]';
+
+ if (strlen($str) < 60) {
+ return $str;
+ }
+
+ // Prefix for toConfigString?
+ return "[\n " . implode(",\n ", $array) . "\n]";
+ }
+
+ public static function renderDictionary($dictionary)
+ {
+ $values = [];
+ foreach ($dictionary as $key => $value) {
+ $values[$key] = rtrim(
+ self::renderKeyValue(
+ self::renderDictionaryKey($key),
+ $value
+ )
+ );
+ }
+
+ if (empty($values)) {
+ return '{}';
+ }
+ ksort($values, SORT_STRING);
+
+ // Prefix for toConfigString?
+ return "{\n" . implode("\n", $values) . "\n}";
+ }
+
+ public static function renderExpression($string)
+ {
+ return "{{\n " . $string . "\n}}";
+ }
+
+ public static function alreadyRendered($string)
+ {
+ return new IcingaConfigRendered($string);
+ }
+
+ public static function isReserved($string)
+ {
+ return in_array($string, self::$reservedWords, true);
+ }
+
+ public static function escapeIfReserved($string)
+ {
+ if (self::isReserved($string)) {
+ return '@' . $string;
+ }
+
+ return $string;
+ }
+
+ public static function isValidInterval($interval)
+ {
+ if (ctype_digit($interval)) {
+ return true;
+ }
+
+ $parts = preg_split('/\s+/', $interval, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($parts as $part) {
+ if (! preg_match('/^(\d+)([dhms]?)$/', $part)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static function parseInterval($interval)
+ {
+ if ($interval === null || $interval === '') {
+ return null;
+ }
+
+ if (is_int($interval) || ctype_digit($interval)) {
+ return (int) $interval;
+ }
+
+ $parts = preg_split('/\s+/', $interval, -1, PREG_SPLIT_NO_EMPTY);
+ $value = 0;
+ foreach ($parts as $part) {
+ if (! preg_match('/^(\d+)([dhms]?)$/', $part, $m)) {
+ throw new InvalidArgumentException(sprintf(
+ '"%s" is not a valid time (duration) definition',
+ $interval
+ ));
+ }
+
+ switch ($m[2]) {
+ case 'd':
+ $value += $m[1] * 86400;
+ break;
+ case 'h':
+ $value += $m[1] * 3600;
+ break;
+ case 'm':
+ $value += $m[1] * 60;
+ break;
+ default:
+ $value += (int) $m[1];
+ }
+ }
+
+ return $value;
+ }
+
+ public static function renderInterval($interval)
+ {
+ // TODO: compat only, do this at munge time. All db fields should be int
+ $seconds = self::parseInterval($interval);
+ if ($seconds === 0) {
+ return '0s';
+ }
+
+ $steps = [
+ 'd' => 86400,
+ 'h' => 3600,
+ 'm' => 60,
+ ];
+
+ foreach ($steps as $unit => $duration) {
+ if ($seconds % $duration === 0) {
+ return (int) floor($seconds / $duration) . $unit;
+ }
+ }
+
+ return $seconds . 's';
+ }
+
+ public static function stringHasMacro($string, $macroName = null)
+ {
+ $len = strlen($string);
+ $start = false;
+ // TODO: robust UTF8 support. It works, but it is not 100% correct
+ for ($i = 0; $i < $len; $i++) {
+ if ($string[$i] === '$') {
+ if ($start === false) {
+ $start = $i;
+ } else {
+ // Escaping, $$
+ if ($start + 1 === $i) {
+ $start = false;
+ } else {
+ if ($macroName === null) {
+ return true;
+ }
+ if ($macroName === substr($string, $start + 1, $i - $start - 1)) {
+ return true;
+ }
+
+ $start = false;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Hint: this isn't complete, but let's restrict ourselves right now
+ *
+ * @param $name
+ * @return bool
+ */
+ public static function isValidMacroName($name)
+ {
+ return preg_match('/^[A-z_][A-z_.\d]+$/', $name)
+ && ! preg_match('/\.$/', $name);
+ }
+
+ public static function renderStringWithVariables($string, array $whiteList = null)
+ {
+ $len = strlen($string);
+ $start = false;
+ $parts = [];
+ // TODO: UTF8...
+ $offset = 0;
+ for ($i = 0; $i < $len; $i++) {
+ if ($string[$i] === '$') {
+ if ($start === false) {
+ $start = $i;
+ } else {
+ // Ignore $$
+ if ($start + 1 === $i) {
+ $start = false;
+ } else {
+ // We got a macro
+ $macroName = substr($string, $start + 1, $i - $start - 1);
+ if (static::isValidMacroName($macroName)) {
+ if ($whiteList === null || in_array($macroName, $whiteList)) {
+ if ($start > $offset) {
+ $parts[] = static::renderString(
+ substr($string, $offset, $start - $offset)
+ );
+ }
+ $parts[] = $macroName;
+ $offset = $i + 1;
+ }
+ }
+
+ $start = false;
+ }
+ }
+ }
+ }
+
+ if ($offset < $i) {
+ $parts[] = static::renderString(substr($string, $offset, $i - $offset));
+ }
+
+ if (! empty($parts)) {
+ return implode(' + ', $parts);
+ }
+
+ return '""';
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigRendered.php b/library/Director/IcingaConfig/IcingaConfigRendered.php
new file mode 100644
index 0000000..90b710e
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigRendered.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use InvalidArgumentException;
+
+class IcingaConfigRendered implements IcingaConfigRenderer
+{
+ protected $rendered;
+
+ public function __construct($string)
+ {
+ if (! is_string($string)) {
+ throw new InvalidArgumentException('IcingaConfigRendered accepts only strings');
+ }
+
+ $this->rendered = $string;
+ }
+
+ public function toConfigString()
+ {
+ return $this->rendered;
+ }
+
+ public function __toString()
+ {
+ return $this->toConfigString();
+ }
+
+ public function toLegacyConfigString()
+ {
+ return $this->rendered;
+ }
+}
diff --git a/library/Director/IcingaConfig/IcingaConfigRenderer.php b/library/Director/IcingaConfig/IcingaConfigRenderer.php
new file mode 100644
index 0000000..108956d
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaConfigRenderer.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+interface IcingaConfigRenderer
+{
+ public function toConfigString();
+ public function toLegacyConfigString();
+ public function __toString();
+}
diff --git a/library/Director/IcingaConfig/IcingaLegacyConfigHelper.php b/library/Director/IcingaConfig/IcingaLegacyConfigHelper.php
new file mode 100644
index 0000000..38d93ee
--- /dev/null
+++ b/library/Director/IcingaConfig/IcingaLegacyConfigHelper.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+use InvalidArgumentException;
+
+class IcingaLegacyConfigHelper
+{
+ public static function renderKeyValue($key, $value, $prefix = ' ')
+ {
+ return self::renderKeyOperatorValue($key, "\t", $value, $prefix);
+ }
+
+ public static function renderKeyOperatorValue($key, $operator, $value, $prefix = ' ')
+ {
+ $string = sprintf(
+ "%s%s%s",
+ $key,
+ $operator,
+ $value
+ );
+
+ if ($prefix && strpos($string, "\n") !== false) {
+ return $prefix . implode("\n" . $prefix, explode("\n", $string)) . "\n";
+ }
+
+ return $prefix . $string . "\n";
+ }
+
+ public static function renderBoolean($value)
+ {
+ if ($value === 'y') {
+ return '1';
+ } elseif ($value === 'n') {
+ return '0';
+ } else {
+ throw new InvalidArgumentException('%s is not a valid boolean', $value);
+ }
+ }
+
+ // TODO: Double-check legacy "encoding"
+ public static function renderString($string)
+ {
+ $special = [
+ '/\\\/',
+ '/\$/',
+ '/\t/',
+ '/\r/',
+ '/\n/',
+ // '/\b/', -> doesn't work
+ '/\f/',
+ ];
+
+ $replace = [
+ '\\\\\\',
+ '\\$',
+ '\\t',
+ '\\r',
+ '\\n',
+ // '\\b',
+ '\\f',
+ ];
+
+ $string = preg_replace($special, $replace, $string);
+
+ return $string;
+ }
+
+ /**
+ * @param array $array
+ * @return string
+ */
+ public static function renderArray($array)
+ {
+ $data = [];
+ foreach ($array as $entry) {
+ if ($entry instanceof IcingaConfigRenderer) {
+ // $data[] = $entry;
+ $data[] = 'INVALID_ARRAY_MEMBER';
+ } else {
+ $data[] = self::renderString($entry);
+ }
+ }
+
+ return implode(', ', $data);
+ }
+
+ public static function renderDictionary($dictionary)
+ {
+ return 'INVALID_DICTIONARY';
+ }
+
+ public static function renderExpression($string)
+ {
+ return 'INVALID_EXPRESSION';
+ }
+
+ public static function alreadyRendered($string)
+ {
+ return new IcingaConfigRendered($string);
+ }
+
+ public static function renderInterval($interval)
+ {
+ if ($interval < 60) {
+ $interval = 60;
+ }
+ return $interval / 60;
+ }
+}
diff --git a/library/Director/IcingaConfig/StateFilterSet.php b/library/Director/IcingaConfig/StateFilterSet.php
new file mode 100644
index 0000000..7a2daec
--- /dev/null
+++ b/library/Director/IcingaConfig/StateFilterSet.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+class StateFilterSet extends ExtensibleSet
+{
+ protected $allowedValues = array(
+ 'Up',
+ 'Down',
+ 'OK',
+ 'Warning',
+ 'Critical',
+ 'Unknown',
+ );
+
+ public function enumAllowedValues()
+ {
+ return array(
+ $this->translate('Hosts') => array(
+ 'Up' => $this->translate('Up'),
+ 'Down' => $this->translate('Down')
+ ),
+ $this->translate('Services') => array(
+ 'OK' => $this->translate('OK'),
+ 'Warning' => $this->translate('Warning'),
+ 'Critical' => $this->translate('Critical'),
+ 'Unknown' => $this->translate('Unknown'),
+ ),
+ );
+ }
+}
diff --git a/library/Director/IcingaConfig/TypeFilterSet.php b/library/Director/IcingaConfig/TypeFilterSet.php
new file mode 100644
index 0000000..dffd4cf
--- /dev/null
+++ b/library/Director/IcingaConfig/TypeFilterSet.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Director\IcingaConfig;
+
+class TypeFilterSet extends ExtensibleSet
+{
+ protected $allowedValues = array(
+ 'Problem',
+ 'Recovery',
+ 'Custom',
+ 'Acknowledgement',
+ 'DowntimeStart',
+ 'DowntimeEnd',
+ 'DowntimeRemoved',
+ 'FlappingStart',
+ 'FlappingEnd',
+ );
+
+ public function enumAllowedValues()
+ {
+ return array(
+ $this->translate('State changes') => array(
+ 'Problem' => $this->translate('Problem'),
+ 'Recovery' => $this->translate('Recovery'),
+ 'Custom' => $this->translate('Custom notification'),
+ ),
+ $this->translate('Problem handling') => array(
+ 'Acknowledgement' => $this->translate('Acknowledgement'),
+ 'DowntimeStart' => $this->translate('Downtime starts'),
+ 'DowntimeEnd' => $this->translate('Downtime ends'),
+ 'DowntimeRemoved' => $this->translate('Downtime removed'),
+ ),
+ $this->translate('Flapping') => array(
+ 'FlappingStart' => $this->translate('Flapping starts'),
+ 'FlappingEnd' => $this->translate('Flapping ends')
+ )
+ );
+ }
+}