summaryrefslogtreecommitdiffstats
path: root/library/Director/Objects/IcingaServiceSet.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Director/Objects/IcingaServiceSet.php591
1 files changed, 591 insertions, 0 deletions
diff --git a/library/Director/Objects/IcingaServiceSet.php b/library/Director/Objects/IcingaServiceSet.php
new file mode 100644
index 0000000..8217a59
--- /dev/null
+++ b/library/Director/Objects/IcingaServiceSet.php
@@ -0,0 +1,591 @@
+<?php
+
+namespace Icinga\Module\Director\Objects;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Director\Data\Db\ServiceSetQueryBuilder;
+use Icinga\Module\Director\Db;
+use Icinga\Module\Director\Db\Cache\PrefetchCache;
+use Icinga\Module\Director\Db\DbUtil;
+use Icinga\Module\Director\DirectorObject\Automation\ExportInterface;
+use Icinga\Module\Director\Exception\DuplicateKeyException;
+use Icinga\Module\Director\IcingaConfig\IcingaConfig;
+use Icinga\Module\Director\Resolver\HostServiceBlacklist;
+use InvalidArgumentException;
+use Ramsey\Uuid\Uuid;
+use RuntimeException;
+
+class IcingaServiceSet extends IcingaObject implements ExportInterface
+{
+ protected $table = 'icinga_service_set';
+
+ protected $defaultProperties = array(
+ 'id' => null,
+ 'uuid' => null,
+ 'host_id' => null,
+ 'object_name' => null,
+ 'object_type' => null,
+ 'description' => null,
+ 'assign_filter' => null,
+ );
+
+ protected $uuidColumn = 'uuid';
+
+ protected $keyName = array('host_id', 'object_name');
+
+ protected $supportsImports = true;
+
+ protected $supportsCustomVars = true;
+
+ protected $supportsApplyRules = true;
+
+ protected $supportedInLegacy = true;
+
+ protected $relations = array(
+ 'host' => 'IcingaHost',
+ );
+
+ public function isDisabled()
+ {
+ return false;
+ }
+
+ public function supportsAssignments()
+ {
+ return true;
+ }
+
+ protected function setKey($key)
+ {
+ if (is_int($key)) {
+ $this->set('id', $key);
+ } elseif (is_string($key)) {
+ $keyComponents = preg_split('~!~', $key);
+ if (count($keyComponents) === 1) {
+ $this->set('object_name', $keyComponents[0]);
+ $this->set('object_type', 'template');
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'Can not parse key: %s',
+ $key
+ ));
+ }
+ } else {
+ return parent::setKey($key);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return IcingaService[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function getServiceObjects()
+ {
+ // don't try to resolve services for unstored objects - as in getServiceObjectsForSet()
+ // also for diff in activity log
+ if ($this->get('id') === null) {
+ return [];
+ }
+
+ if ($this->get('host_id')) {
+ $imports = $this->imports()->getObjects();
+ if (empty($imports)) {
+ return array();
+ }
+ $parent = array_shift($imports);
+ assert($parent instanceof IcingaServiceSet);
+ return $this->getServiceObjectsForSet($parent);
+ } else {
+ return $this->getServiceObjectsForSet($this);
+ }
+ }
+
+ /**
+ * @param IcingaServiceSet $set
+ * @return IcingaService[]
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function getServiceObjectsForSet(IcingaServiceSet $set)
+ {
+ $connection = $this->getConnection();
+ if (self::$dbObjectStore !== null) {
+ $branchUuid = self::$dbObjectStore->getBranch()->getUuid();
+ } else {
+ $branchUuid = null;
+ }
+
+ $builder = new ServiceSetQueryBuilder($connection, $branchUuid);
+ return $builder->fetchServicesWithQuery($builder->selectServicesForSet($set));
+ }
+
+ public function getUniqueIdentifier()
+ {
+ return $this->getObjectName();
+ }
+
+ /**
+ * @return object
+ * @deprecated please use \Icinga\Module\Director\Data\Exporter
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function export()
+ {
+ if ($this->get('host_id')) {
+ $result = $this->exportSetOnHost();
+ } else {
+ $result = $this->exportTemplate();
+ }
+
+ unset($result->uuid);
+ return $result;
+ }
+
+ protected function exportSetOnHost()
+ {
+ // TODO.
+ throw new RuntimeException('Not yet');
+ }
+
+ /**
+ * @return object
+ * @deprecated
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function exportTemplate()
+ {
+ $props = $this->getProperties();
+ unset($props['id'], $props['host_id']);
+ $props['services'] = [];
+ foreach ($this->getServiceObjects() as $serviceObject) {
+ $props['services'][$serviceObject->getObjectName()] = $serviceObject->export();
+ }
+ ksort($props);
+
+ return (object) $props;
+ }
+
+ /**
+ * @param $plain
+ * @param Db $db
+ * @param bool $replace
+ * @return IcingaServiceSet
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public static function import($plain, Db $db, $replace = false)
+ {
+ $properties = (array) $plain;
+ $name = $properties['object_name'];
+ if (isset($properties['services'])) {
+ $services = $properties['services'];
+ unset($properties['services']);
+ } else {
+ $services = [];
+ }
+
+ if ($properties['object_type'] !== 'template') {
+ throw new InvalidArgumentException(sprintf(
+ 'Can import only Templates, got "%s" for "%s"',
+ $properties['object_type'],
+ $name
+ ));
+ }
+ if ($replace && static::exists($name, $db)) {
+ $object = static::load($name, $db);
+ } elseif (static::exists($name, $db)) {
+ throw new DuplicateKeyException(
+ 'Service Set "%s" already exists',
+ $name
+ );
+ } else {
+ $object = static::create([], $db);
+ }
+
+ $object->setProperties($properties);
+
+ // This is not how other imports work, but here we need an ID
+ if (! $object->hasBeenLoadedFromDb()) {
+ $object->store();
+ }
+
+ $setId = $object->get('id');
+ $sQuery = $db->getDbAdapter()->select()->from(
+ ['s' => 'icinga_service'],
+ 's.*'
+ )->where('service_set_id = ?', $setId);
+ $existingServices = IcingaService::loadAll($db, $sQuery, 'object_name');
+ $serviceNames = [];
+ foreach ($services as $service) {
+ if (isset($service->fields)) {
+ unset($service->fields);
+ }
+ $name = $service->object_name;
+ $serviceNames[] = $name;
+ if (isset($existingServices[$name])) {
+ $existing = $existingServices[$name];
+ $existing->setProperties((array) $service);
+ $existing->set('service_set_id', $setId);
+ if ($existing->hasBeenModified()) {
+ $existing->store();
+ }
+ } else {
+ $new = IcingaService::create((array) $service, $db);
+ $new->set('service_set_id', $setId);
+ $new->store();
+ }
+ }
+
+ foreach ($existingServices as $existing) {
+ if (!in_array($existing->getObjectName(), $serviceNames)) {
+ $existing->delete();
+ }
+ }
+
+ return $object;
+ }
+
+ public function beforeDelete()
+ {
+ // check if this is a template, or directly assigned to a host
+ if ($this->get('host_id') === null) {
+ // find all host sets and delete them
+ foreach ($this->fetchHostSets() as $set) {
+ $set->delete();
+ }
+ }
+
+ parent::beforeDelete();
+ }
+
+ /**
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function onDelete()
+ {
+ $hostId = $this->get('host_id');
+ if ($hostId) {
+ $deleteIds = [];
+ foreach ($this->getServiceObjects() as $service) {
+ if ($idToDelete = $service->get('id')) {
+ $deleteIds[] = (int) $idToDelete;
+ }
+ }
+
+ if (! empty($deleteIds)) {
+ $db = $this->getDb();
+ $db->delete(
+ 'icinga_host_service_blacklist',
+ $db->quoteInto(
+ sprintf('host_id = %s AND service_id IN (?)', $hostId),
+ $deleteIds
+ )
+ );
+ }
+ }
+
+ parent::onDelete();
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function renderToConfig(IcingaConfig $config)
+ {
+ // always print the header, so you have minimal info present
+ $file = $this->getConfigFileWithHeader($config);
+
+ if ($this->get('assign_filter') === null && $this->isTemplate()) {
+ return;
+ }
+
+ if ($config->isLegacy()) {
+ $this->renderToLegacyConfig($config);
+ return;
+ }
+
+ $services = $this->getServiceObjects();
+ if (empty($services)) {
+ return;
+ }
+
+ // Loop over all services belonging to this set
+ // add our assign rules and then add the service to the config
+ // eventually clone them beforehand to not get into trouble with caches
+ // figure out whether we might need a zone property
+ foreach ($services as $service) {
+ if ($filter = $this->get('assign_filter')) {
+ $service->set('object_type', 'apply');
+ $service->set('assign_filter', $filter);
+ } elseif ($hostId = $this->get('host_id')) {
+ $host = $this->getRelatedObject('host', $hostId)->getObjectName();
+ if (in_array($host, $this->getBlacklistedHostnames($service))) {
+ continue;
+ }
+ $service->set('object_type', 'object');
+ $service->set('use_var_overrides', 'y');
+ $service->set('host_id', $hostId);
+ } else {
+ // Service set template without assign filter or host
+ continue;
+ }
+
+ $this->copyVarsToService($service);
+ $file->addObject($service);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getBlacklistedHostnames($service)
+ {
+ // 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($service);
+ }
+
+ protected function getConfigFileWithHeader(IcingaConfig $config)
+ {
+ $file = $config->configFile(
+ 'zones.d/' . $this->getRenderingZone($config) . '/servicesets'
+ );
+
+ $file->addContent($this->getConfigHeaderComment($config));
+ return $file;
+ }
+
+ protected function getConfigHeaderComment(IcingaConfig $config)
+ {
+ $name = $this->getObjectName();
+ $assign = $this->get('assign_filter');
+
+ if ($config->isLegacy()) {
+ if ($assign !== null) {
+ return "## applied Service Set '${name}'\n\n";
+ } else {
+ return "## Service Set '${name}' on this host\n\n";
+ }
+ } else {
+ $comment = [
+ "Service Set: ${name}",
+ ];
+
+ if (($host = $this->get('host')) !== null) {
+ $comment[] = 'on host ' . $host;
+ }
+
+ if (($description = $this->get('description')) !== null) {
+ $comment[] = '';
+ foreach (preg_split('~\\n~', $description) as $line) {
+ $comment[] = $line;
+ }
+ }
+
+ if ($assign !== null) {
+ $comment[] = '';
+ $comment[] = trim($this->renderAssign_Filter());
+ }
+
+ return "/**\n * " . join("\n * ", $comment) . "\n */\n\n";
+ }
+ }
+
+ public function copyVarsToService(IcingaService $service)
+ {
+ $serviceVars = $service->vars();
+
+ foreach ($this->vars() as $k => $var) {
+ $serviceVars->$k = $var;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param IcingaConfig $config
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ public function renderToLegacyConfig(IcingaConfig $config)
+ {
+ if ($this->get('assign_filter') === null && $this->isTemplate()) {
+ return;
+ }
+
+ // evaluate my assign rules once, get related hosts
+ // Loop over all services belonging to this set
+ // generate every service with host_name host1,host2... -> not yet. And Zones?
+
+ $conn = $this->getConnection();
+
+ // Delegating this to the service would look, but this way it's faster
+ if ($filter = $this->get('assign_filter')) {
+ $filter = Filter::fromQueryString($filter);
+
+ $hostnames = HostApplyMatches::forFilter($filter, $conn);
+ } else {
+ $hostnames = array($this->getRelated('host')->getObjectName());
+ }
+
+ $blacklists = [];
+
+ foreach ($this->mapHostsToZones($hostnames) as $zone => $names) {
+ $file = $config->configFile('director/' . $zone . '/servicesets', '.cfg');
+ $file->addContent($this->getConfigHeaderComment($config));
+
+ foreach ($this->getServiceObjects() as $service) {
+ $object_name = $service->getObjectName();
+
+ if (! array_key_exists($object_name, $blacklists)) {
+ $blacklists[$object_name] = $service->getBlacklistedHostnames();
+ }
+
+ // check if all hosts in the zone ignore this service
+ $zoneNames = array_diff($names, $blacklists[$object_name]);
+
+ $disabled = [];
+ foreach ($zoneNames as $name) {
+ if (IcingaHost::load($name, $this->getConnection())->isDisabled()) {
+ $disabled[] = $name;
+ }
+ }
+ $zoneNames = array_diff($zoneNames, $disabled);
+
+ if (empty($zoneNames)) {
+ continue;
+ }
+
+ $service->set('object_type', 'object');
+ $service->set('host_id', $names);
+
+ $this->copyVarsToService($service);
+
+ $file->addLegacyObject($service);
+ }
+ }
+ }
+
+ public function getRenderingZone(IcingaConfig $config = null)
+ {
+ if ($this->get('host_id') === null) {
+ if ($hostname = $this->get('host')) {
+ $host = IcingaHost::load($hostname, $this->getConnection());
+ } else {
+ return $this->connection->getDefaultGlobalZoneName();
+ }
+ } else {
+ $host = $this->getRelatedObject('host', $this->get('host_id'));
+ }
+
+ return $host->getRenderingZone($config);
+ }
+
+ public function createWhere()
+ {
+ $where = parent::createWhere();
+ if (! $this->hasBeenLoadedFromDb()) {
+ if (null === $this->get('host_id') && null === $this->get('id')) {
+ $where .= " AND object_type = 'template'";
+ }
+ }
+
+ return $where;
+ }
+
+ /**
+ * @return IcingaService[]
+ */
+ public function fetchServices()
+ {
+ if ($store = self::$dbObjectStore) {
+ $uuid = $store->getBranch()->getUuid();
+ } else {
+ $uuid = null;
+ }
+ $builder = new ServiceSetQueryBuilder($this->getConnection(), $uuid);
+ return $builder->fetchServicesWithQuery($builder->selectServicesForSet($this));
+ }
+
+ /**
+ * Fetch IcingaServiceSet that are based on this set and added to hosts directly
+ *
+ * @return IcingaServiceSet[]
+ */
+ public function fetchHostSets()
+ {
+ $id = $this->get('id');
+ if ($id === null) {
+ return [];
+ }
+
+ $query = $this->db->select()
+ ->from(
+ ['o' => $this->table]
+ )->join(
+ ['ssi' => $this->table . '_inheritance'],
+ 'ssi.service_set_id = o.id',
+ []
+ )->where(
+ 'ssi.parent_service_set_id = ?',
+ $id
+ );
+
+ return static::loadAll($this->connection, $query);
+ }
+
+ /**
+ * @throws DuplicateKeyException
+ * @throws \Icinga\Exception\NotFoundError
+ */
+ protected function beforeStore()
+ {
+ parent::beforeStore();
+
+ $name = $this->getObjectName();
+
+ if ($this->isObject() && $this->get('host_id') === null && $this->get('host') === null) {
+ throw new InvalidArgumentException(
+ 'A Service Set cannot be an object with no related host'
+ );
+ }
+ // checking if template object_name is unique
+ // TODO: Move to IcingaObject
+ if (! $this->hasBeenLoadedFromDb() && $this->isTemplate() && static::exists($name, $this->connection)) {
+ throw new DuplicateKeyException(
+ '%s template "%s" already existing in database!',
+ $this->getType(),
+ $name
+ );
+ }
+ }
+
+ public function toSingleIcingaConfig()
+ {
+ $config = parent::toSingleIcingaConfig();
+
+ try {
+ foreach ($this->fetchHostSets() as $set) {
+ $set->renderToConfig($config);
+ }
+ } catch (Exception $e) {
+ $config->configFile(
+ 'failed-to-render'
+ )->prepend(
+ "/** Failed to render this Service Set **/\n"
+ . '/* ' . $e->getMessage() . ' */'
+ );
+ }
+
+ return $config;
+ }
+}