From f66ab8dae2f3d0418759f81a3a64dc9517a62449 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 15:17:31 +0200 Subject: Adding upstream version 1.10.2. Signed-off-by: Daniel Baumann --- library/Director/IcingaConfig/AgentWizard.php | 337 +++++++++ library/Director/IcingaConfig/AssignRenderer.php | 268 +++++++ library/Director/IcingaConfig/ExtensibleSet.php | 574 +++++++++++++++ library/Director/IcingaConfig/IcingaConfig.php | 781 +++++++++++++++++++++ library/Director/IcingaConfig/IcingaConfigFile.php | 168 +++++ .../Director/IcingaConfig/IcingaConfigHelper.php | 430 ++++++++++++ .../Director/IcingaConfig/IcingaConfigRendered.php | 34 + .../Director/IcingaConfig/IcingaConfigRenderer.php | 10 + .../IcingaConfig/IcingaLegacyConfigHelper.php | 110 +++ library/Director/IcingaConfig/StateFilterSet.php | 31 + library/Director/IcingaConfig/TypeFilterSet.php | 39 + 11 files changed, 2782 insertions(+) create mode 100644 library/Director/IcingaConfig/AgentWizard.php create mode 100644 library/Director/IcingaConfig/AssignRenderer.php create mode 100644 library/Director/IcingaConfig/ExtensibleSet.php create mode 100644 library/Director/IcingaConfig/IcingaConfig.php create mode 100644 library/Director/IcingaConfig/IcingaConfigFile.php create mode 100644 library/Director/IcingaConfig/IcingaConfigHelper.php create mode 100644 library/Director/IcingaConfig/IcingaConfigRendered.php create mode 100644 library/Director/IcingaConfig/IcingaConfigRenderer.php create mode 100644 library/Director/IcingaConfig/IcingaLegacyConfigHelper.php create mode 100644 library/Director/IcingaConfig/StateFilterSet.php create mode 100644 library/Director/IcingaConfig/TypeFilterSet.php (limited to 'library/Director/IcingaConfig') 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 @@ +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 @@ +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 @@ +__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 @@ +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 @@ +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 @@ + 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 @@ +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 @@ + 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 @@ +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 @@ +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') + ) + ); + } +} -- cgit v1.2.3