summaryrefslogtreecommitdiffstats
path: root/library/Director/Objects/IcingaService.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Director/Objects/IcingaService.php')
-rw-r--r--library/Director/Objects/IcingaService.php828
1 files changed, 828 insertions, 0 deletions
diff --git a/library/Director/Objects/IcingaService.php b/library/Director/Objects/IcingaService.php
new file mode 100644
index 0000000..9479ef7
--- /dev/null
+++ b/library/Director/Objects/IcingaService.php
@@ -0,0 +1,828 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Data\PropertiesFilter;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
+use Icinga\Module\Director\IcingaConfig\IcingaLegacyConfigHelper as c1;
+use Icinga\Module\Director\Objects\Extension\FlappingSupport;
+use Icinga\Module\Director\Resolver\HostServiceBlacklist;
+use InvalidArgumentException;
+use RuntimeException;
+
+class IcingaService extends IcingaObject implements ExportInterface
+{
+ use FlappingSupport;
+
+ protected $table = 'icinga_service';
+
+ protected $uuidColumn = 'uuid';
+
+ protected $defaultProperties = [
+ 'id' => null,
+ 'uuid' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'disabled' => 'n',
+ 'display_name' => null,
+ 'host_id' => null,
+ 'service_set_id' => null,
+ 'check_command_id' => null,
+ 'max_check_attempts' => null,
+ 'check_period_id' => null,
+ 'check_interval' => null,
+ 'retry_interval' => null,
+ 'check_timeout' => null,
+ 'enable_notifications' => null,
+ 'enable_active_checks' => null,
+ 'enable_passive_checks' => null,
+ 'enable_event_handler' => null,
+ 'enable_flapping' => null,
+ 'enable_perfdata' => null,
+ 'event_command_id' => null,
+ 'flapping_threshold_high' => null,
+ 'flapping_threshold_low' => null,
+ 'volatile' => null,
+ 'zone_id' => null,
+ 'command_endpoint_id' => null,
+ 'notes' => null,
+ 'notes_url' => null,
+ 'action_url' => null,
+ 'icon_image' => null,
+ 'icon_image_alt' => null,
+ 'use_agent' => null,
+ 'apply_for' => null,
+ 'use_var_overrides' => null,
+ 'assign_filter' => null,
+ 'template_choice_id' => null,
+ ];
+
+ protected $relations = [
+ 'host' => 'IcingaHost',
+ 'service_set' => 'IcingaServiceSet',
+ 'check_command' => 'IcingaCommand',
+ 'event_command' => 'IcingaCommand',
+ 'check_period' => 'IcingaTimePeriod',
+ 'command_endpoint' => 'IcingaEndpoint',
+ 'zone' => 'IcingaZone',
+ 'template_choice' => 'IcingaTemplateChoiceService',
+ ];
+
+ protected $booleans = [
+ 'enable_notifications' => 'enable_notifications',
+ 'enable_active_checks' => 'enable_active_checks',
+ 'enable_passive_checks' => 'enable_passive_checks',
+ 'enable_event_handler' => 'enable_event_handler',
+ 'enable_flapping' => 'enable_flapping',
+ 'enable_perfdata' => 'enable_perfdata',
+ 'volatile' => 'volatile',
+ 'use_agent' => 'use_agent',
+ 'use_var_overrides' => 'use_var_overrides',
+ ];
+
+ protected $intervalProperties = [
+ 'check_interval' => 'check_interval',
+ 'check_timeout' => 'check_timeout',
+ 'retry_interval' => 'retry_interval',
+ ];
+
+ protected $supportsGroups = true;
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsFields = true;
+
+ protected $supportsImports = true;
+
+ protected $supportsApplyRules = true;
+
+ protected $supportsSets = true;
+
+ protected $supportsChoices = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $keyName = ['host_id', 'service_set_id', 'object_name'];
+
+ protected $prioritizedProperties = ['host_id'];
+
+ protected $propertiesNotForRendering = [
+ 'id',
+ 'object_name',
+ 'object_type',
+ 'apply_for'
+ ];
+
+ /** @var ServiceGroupMembershipResolver */
+ protected $servicegroupMembershipResolver;
+
+ /**
+ * @return IcingaCommand
+ * @throws IcingaException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getCheckCommand()
+ {
+ $id = $this->getSingleResolvedProperty('check_command_id');
+ return IcingaCommand::loadWithAutoIncId(
+ $id,
+ $this->getConnection()
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function isApplyRule()
+ {
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ return true;
+ }
+
+ return $this->hasProperty('object_type')
+ && $this->get('object_type') === 'apply';
+ }
+
+ /**
+ * @return bool
+ */
+ public function usesVarOverrides()
+ {
+ return $this->get('use_var_overrides') === 'y';
+ }
+
+ public function getUniqueIdentifier()
+ {
+ if ($this->isTemplate()) {
+ return $this->getObjectName();
+ } else {
+ throw new RuntimeException(
+ 'getUniqueIdentifier() is supported by Service Templates only'
+ );
+ }
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ // TODO: ksort in toPlainObject?
+ $props = (array) $this->toPlainObject();
+ $props['fields'] = $this->loadFieldReferences();
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaService
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ if ($properties['object_type'] !== 'template') {
+ throw new InvalidArgumentException(sprintf(
+ 'Can import only Templates, got "%s" for "%s"',
+ $properties['object_type'],
+ $name
+ ));
+ }
+ $key = [
+ 'object_type' => 'template',
+ 'object_name' => $name
+ ];
+
+ if ($replace && static::exists($key, $db)) {
+ $object = static::load($key, $db);
+ } elseif (static::exists($key, $db)) {
+ throw new DuplicateKeyException(
+ 'Service Template "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ // $object->newFields = $properties['fields'];
+ unset($properties['fields']);
+ $object->setProperties($properties);
+
+ return $object;
+ }
+
+ /**
+ * @deprecated please use \Icinga\Module\Director\Data\FieldReferenceLoader
+ * @return array
+ */
+ protected function loadFieldReferences()
+ {
+ $db = $this->getDb();
+
+ $res = $db->fetchAll(
+ $db->select()->from([
+ 'sf' => 'icinga_service_field'
+ ], [
+ 'sf.datafield_id',
+ 'sf.is_required',
+ 'sf.var_filter',
+ ])->join(['df' => 'director_datafield'], 'df.id = sf.datafield_id', [])
+ ->where('service_id = ?', $this->get('id'))
+ ->order('varname ASC')
+ );
+
+ if (empty($res)) {
+ return [];
+ } else {
+ foreach ($res as $field) {
+ $field->datafield_id = (int) $field->datafield_id;
+ }
+
+ return $res;
+ }
+ }
+
+ /**
+ * @param string $key
+ * @return $this
+ */
+ protected function setKey($key)
+ {
+ if (is_int($key)) {
+ $this->set('id', $key);
+ } elseif (is_array($key)) {
+ foreach (['id', 'host_id', 'service_set_id', 'object_name'] as $k) {
+ if (array_key_exists($k, $key)) {
+ $this->set($k, $key[$k]);
+ }
+ }
+ } else {
+ parent::setKey($key);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ * @return $this
+ * @codingStandardsIgnoreStart
+ */
+ protected function setObject_Name($name)
+ {
+ // @codingStandardsIgnoreEnd
+
+ if ($name === null && $this->isApplyRule()) {
+ $name = '';
+ }
+
+ return $this->reallySet('object_name', $name);
+ }
+
+ /**
+ * Render host_id as host_name
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderHost_id()
+ {
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ return '';
+ }
+
+ return $this->renderRelationProperty('host', $this->get('host_id'), 'host_name');
+ }
+
+ /**
+ * @codingStandardsIgnoreStart
+ */
+ protected function renderLegacyHost_id($value)
+ {
+ // @codingStandardsIgnoreEnd
+ if (is_array($value)) {
+ $blacklisted = $this->getBlacklistedHostnames();
+ $c = c1::renderKeyValue('host_name', c1::renderArray(array_diff($value, $blacklisted)));
+
+ // blacklisted in this (zoned) scope?
+ $bl = array_intersect($blacklisted, $value);
+ if (! empty($bl)) {
+ $c .= c1::renderKeyValue('# ignored on', c1::renderArray($bl));
+ }
+
+ return $c;
+ } else {
+ return parent::renderLegacyHost_id($value);
+ }
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws IcingaException
+ */
+ public function renderToLegacyConfig(IcingaConfig $config)
+ {
+ if ($this->get('service_set_id') !== null) {
+ return;
+ } elseif ($this->isApplyRule()) {
+ $this->renderLegacyApplyToConfig($config);
+ } else {
+ parent::renderToLegacyConfig($config);
+ }
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws IcingaException
+ */
+ protected function renderLegacyApplyToConfig(IcingaConfig $config)
+ {
+ $conn = $this->getConnection();
+
+ $assign_filter = $this->get('assign_filter');
+ $filter = Filter::fromQueryString($assign_filter);
+ $hostnames = HostApplyMatches::forFilter($filter, $conn);
+
+ $this->set('object_type', 'object');
+
+ foreach ($this->mapHostsToZones($hostnames) as $zone => $names) {
+ $blacklisted = $this->getBlacklistedHostnames();
+ $zoneNames = array_diff($names, $blacklisted);
+
+ $disabled = [];
+ foreach ($zoneNames as $name) {
+ if (IcingaHost::load($name, $this->getConnection())->isDisabled()) {
+ $disabled[] = $name;
+ }
+ }
+ $zoneNames = array_diff($zoneNames, $disabled);
+
+ if (empty($zoneNames)) {
+ continue;
+ }
+
+ $this->set('host_id', $zoneNames);
+
+ $config->configFile('director/' . $zone . '/service_apply', '.cfg')
+ ->addLegacyObject($this);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function toLegacyConfigString()
+ {
+ if ($this->get('service_set_id') !== null) {
+ return '';
+ }
+
+ $str = parent::toLegacyConfigString();
+
+ if (! $this->isDisabled()
+ && $this->get('host_id')
+ && $this->getRelated('host')->isDisabled()
+ ) {
+ return "# --- This services host has been disabled ---\n"
+ . preg_replace('~^~m', '# ', trim($str))
+ . "\n\n";
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function toConfigString()
+ {
+ if ($this->get('service_set_id')) {
+ return '';
+ }
+ $str = parent::toConfigString();
+
+ if (! $this->isDisabled()
+ && $this->get('host_id')
+ && $this->getRelated('host')->isDisabled()
+ ) {
+ return "/* --- This services host has been disabled ---\n"
+ // Do not allow strings to break our comment
+ . str_replace('*/', "* /", $str) . "*/\n";
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderObjectHeader()
+ {
+ if ($this->isApplyRule()
+ && !$this->hasBeenAssignedToHostTemplate()
+ && $this->get('apply_for') !== null
+ ) {
+ $name = $this->getObjectName();
+ $extraName = '';
+
+ if (c::stringHasMacro($name)) {
+ $extraName = c::renderKeyValue('name', c::renderStringWithVariables($name));
+ $name = '';
+ } elseif ($name !== '') {
+ $name = ' ' . c::renderString($name);
+ }
+
+ return sprintf(
+ "%s %s%s for (config in %s) {\n",
+ $this->getObjectTypeName(),
+ $this->getType(),
+ $name,
+ $this->get('apply_for')
+ ) . $extraName;
+ }
+
+ return parent::renderObjectHeader();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getLegacyObjectKeyName()
+ {
+ if ($this->isTemplate()) {
+ return 'name';
+ } else {
+ return 'service_description';
+ }
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenAssignedToHostTemplate()
+ {
+ // Branches would fail
+ if ($this->properties['host_id'] === null) {
+ return null;
+ }
+ $hostId = $this->get('host_id');
+
+ return $hostId && $this->getRelatedObject(
+ 'host',
+ $hostId
+ )->isTemplate();
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ * @throws \Icinga\Module\Director\Exception\NestingError
+ */
+ protected function renderSuffix()
+ {
+ $suffix = '';
+ if ($this->isApplyRule()) {
+ $zoneName = $this->getRenderingZone();
+ if (!IcingaZone::zoneNameIsGlobal($zoneName, $this->connection)) {
+ $suffix .= c::renderKeyValue('zone', c::renderString($zoneName));
+ }
+ }
+
+ if ($this->isApplyRule() || $this->usesVarOverrides()) {
+ $suffix .= $this->renderImportHostVarOverrides();
+ }
+
+ return $suffix . parent::renderSuffix();
+ }
+
+ /**
+ * @return string
+ */
+ protected function renderImportHostVarOverrides()
+ {
+ if (! $this->connection) {
+ throw new RuntimeException(
+ 'Cannot render services without an assigned DB connection'
+ );
+ }
+
+ return "\n import DirectorOverrideTemplate\n";
+ }
+
+ /**
+ * @return string
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function renderCustomExtensions()
+ {
+ $output = '';
+
+ if ($this->hasBeenAssignedToHostTemplate()) {
+ // TODO: use assignment renderer?
+ $filter = sprintf(
+ 'assign where %s in host.templates',
+ c::renderString($this->get('host'))
+ );
+
+ $output .= "\n " . $filter . "\n";
+ }
+
+ $blacklist = $this->getBlacklistedHostnames();
+ $blacklistedTemplates = [];
+ $blacklistedHosts = [];
+ foreach ($blacklist as $hostname) {
+ if (IcingaHost::load($hostname, $this->connection)->isTemplate()) {
+ $blacklistedTemplates[] = $hostname;
+ } else {
+ $blacklistedHosts[] = $hostname;
+ }
+ }
+ foreach ($blacklistedTemplates as $template) {
+ $output .= sprintf(
+ " ignore where %s in host.templates\n",
+ c::renderString($template)
+ );
+ }
+ if (! empty($blacklistedHosts)) {
+ if (count($blacklistedHosts) === 1) {
+ $output .= sprintf(
+ " ignore where host.name == %s\n",
+ c::renderString($blacklistedHosts[0])
+ );
+ } else {
+ $output .= sprintf(
+ " ignore where host.name in %s\n",
+ c::renderArray($blacklistedHosts)
+ );
+ }
+ }
+
+ // A hand-crafted command endpoint overrides use_agent
+ if ($this->get('command_endpoint_id') !== null) {
+ return $output;
+ }
+
+ if ($this->get('use_agent') === 'y') {
+ // When feature flag feature_custom_endpoint is enabled, render additional code
+ if ($this->connection->settings()->get('feature_custom_endpoint') === 'y') {
+ return $output . "
+ // Set command_endpoint dynamically with Director
+ if (!host) {
+ var host = get_host(host_name)
+ }
+ if (host.vars._director_custom_endpoint_name) {
+ command_endpoint = host.vars._director_custom_endpoint_name
+ } else {
+ command_endpoint = host_name
+ }
+";
+ } else {
+ return $output . c::renderKeyValue('command_endpoint', 'host_name');
+ }
+ } elseif ($this->get('use_agent') === 'n') {
+ return $output . c::renderKeyValue('command_endpoint', c::renderPhpValue(null));
+ } else {
+ return $output;
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getBlacklistedHostnames()
+ {
+ // Hint: if ($this->isApplyRule()) would be nice, but apply rules are
+ // not enough, one might want to blacklist single services from Sets
+ // assigned to single Hosts.
+ if (PrefetchCache::shouldBeUsed()) {
+ $lookup = PrefetchCache::instance()->hostServiceBlacklist();
+ } else {
+ $lookup = new HostServiceBlacklist($this->getConnection());
+ }
+
+ return $lookup->getBlacklistedHostnamesForService($this);
+ }
+
+ /**
+ * Do not render internal property
+ *
+ * Avoid complaints for method names with underscore:
+ * @codingStandardsIgnoreStart
+ *
+ * @return string
+ */
+ public function renderUse_agent()
+ {
+ return '';
+ }
+
+ public function renderUse_var_overrides()
+ {
+ return '';
+ }
+
+ protected function renderTemplate_choice_id()
+ {
+ return '';
+ }
+
+ protected function renderLegacyDisplay_Name()
+ {
+ // @codingStandardsIgnoreEnd
+ return c1::renderKeyValue('display_name', $this->get('display_name'));
+ }
+
+ public function hasCheckCommand()
+ {
+ return $this->getSingleResolvedProperty('check_command_id') !== null;
+ }
+
+ public function getOnDeleteUrl()
+ {
+ if ($this->get('host_id')) {
+ return 'director/host/services?name=' . rawurlencode($this->get('host'));
+ } elseif ($this->get('service_set_id')) {
+ return 'director/serviceset/services?name=' . rawurlencode($this->get('service_set'));
+ } else {
+ return parent::getOnDeleteUrl();
+ }
+ }
+
+ protected function getDefaultZone(IcingaConfig $config = null)
+ {
+ if ($this->get('host_id') === null) {
+ return parent::getDefaultZone();
+ } else {
+ return $this->getRelatedObject('host', $this->get('host_id'))
+ ->getRenderingZone($config);
+ }
+ }
+
+ /**
+ * @return string
+ */
+ public function createWhere()
+ {
+ $where = parent::createWhere();
+ if (! $this->hasBeenLoadedFromDb()) {
+ if (null === $this->get('service_set_id')
+ && null === $this->get('host_id')
+ && null === $this->get('id')
+ ) {
+ $where .= " AND object_type = 'template'";
+ }
+ }
+
+ return $where;
+ }
+
+
+ /**
+ * TODO: Duplicate code, clean this up, split it into multiple methods
+ * @param Db|null $connection
+ * @param string $prefix
+ * @param null $filter
+ * @return array
+ */
+ public static function enumProperties(
+ Db $connection = null,
+ $prefix = '',
+ $filter = null
+ ) {
+ $serviceProperties = [];
+ if ($filter === null) {
+ $filter = new PropertiesFilter();
+ }
+ $realProperties = static::create()->listProperties();
+ sort($realProperties);
+
+ if ($filter->match(PropertiesFilter::$SERVICE_PROPERTY, 'name')) {
+ $serviceProperties[$prefix . 'name'] = 'name';
+ }
+ foreach ($realProperties as $prop) {
+ if (!$filter->match(PropertiesFilter::$SERVICE_PROPERTY, $prop)) {
+ continue;
+ }
+
+ if (substr($prop, -3) === '_id') {
+ if ($prop === 'template_choice_id') {
+ continue;
+ }
+ $prop = substr($prop, 0, -3);
+ }
+
+ $serviceProperties[$prefix . $prop] = $prop;
+ }
+
+ $serviceVars = [];
+
+ if ($connection !== null) {
+ foreach ($connection->fetchDistinctServiceVars() as $var) {
+ if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) {
+ if ($var->datatype) {
+ $serviceVars[$prefix . 'vars.' . $var->varname] = sprintf(
+ '%s (%s)',
+ $var->varname,
+ $var->caption
+ );
+ } else {
+ $serviceVars[$prefix . 'vars.' . $var->varname] = $var->varname;
+ }
+ }
+ }
+ }
+
+ //$properties['vars.*'] = 'Other custom variable';
+ ksort($serviceVars);
+
+ $props = mt('director', 'Service properties');
+ $vars = mt('director', 'Custom variables');
+
+ $properties = [];
+ if (!empty($serviceProperties)) {
+ $properties[$props] = $serviceProperties;
+ $properties[$props][$prefix . 'groups'] = 'Groups';
+ }
+
+ if (!empty($serviceVars)) {
+ $properties[$vars] = $serviceVars;
+ }
+
+ $hostProps = mt('director', 'Host properties');
+ $hostVars = mt('director', 'Host Custom variables');
+
+ $hostProperties = IcingaHost::enumProperties($connection, 'host.');
+
+ if (array_key_exists($hostProps, $hostProperties)) {
+ $p = $hostProperties[$hostProps];
+ if (!empty($p)) {
+ $properties[$hostProps] = $p;
+ }
+ }
+
+ if (array_key_exists($vars, $hostProperties)) {
+ $p = $hostProperties[$vars];
+ if (!empty($p)) {
+ $properties[$hostVars] = $p;
+ }
+ }
+
+ return $properties;
+ }
+
+ protected function beforeStore()
+ {
+ parent::beforeStore();
+ if ($this->isObject()
+ && $this->get('service_set_id') === null
+ && $this->get('host_id') === null
+ ) {
+ throw new InvalidArgumentException(
+ 'Cannot store a Service object without a related host or set: ' . $this->getObjectName()
+ );
+ }
+ }
+
+ protected function notifyResolvers()
+ {
+ $resolver = $this->getServiceGroupMembershipResolver();
+ $resolver->addObject($this);
+ $resolver->refreshDb();
+
+ return $this;
+ }
+
+ protected function getServiceGroupMembershipResolver()
+ {
+ if ($this->servicegroupMembershipResolver === null) {
+ $this->servicegroupMembershipResolver = new ServiceGroupMembershipResolver(
+ $this->getConnection()
+ );
+ }
+
+ return $this->servicegroupMembershipResolver;
+ }
+
+ public function setServiceGroupMembershipResolver(ServiceGroupMembershipResolver $resolver)
+ {
+ $this->servicegroupMembershipResolver = $resolver;
+ return $this;
+ }
+}