mirror of
https://github.com/Icinga/icingaweb2-module-director.git
synced 2025-07-31 01:34:12 +02:00
The Services which were added into the Service Set after the snapshot was created must be deleted when the Service Set is being restored from the snapshot.
591 lines
17 KiB
PHP
591 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Icinga\Module\Director\Objects;
|
|
|
|
use Exception;
|
|
use Icinga\Data\Filter\Filter;
|
|
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();
|
|
}
|
|
return $this->getServiceObjectsForSet(array_shift($imports));
|
|
} else {
|
|
return $this->getServiceObjectsForSet($this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param IcingaServiceSet $set
|
|
* @return array
|
|
* @throws \Icinga\Exception\NotFoundError
|
|
*/
|
|
protected function getServiceObjectsForSet(IcingaServiceSet $set)
|
|
{
|
|
if ($set->get('id') === null) {
|
|
return array();
|
|
}
|
|
$connection = $this->getConnection();
|
|
$db = $this->getDb();
|
|
$uuids = $db->fetchCol(
|
|
$db->select()->from('icinga_service', 'uuid')
|
|
->where('service_set_id = ?', $set->get('id'))
|
|
);
|
|
|
|
$services = array();
|
|
foreach ($uuids as $uuid) {
|
|
$service = IcingaService::loadWithUniqueId(Uuid::fromBytes(DbUtil::binaryResult($uuid)), $connection);
|
|
$service->set('service_set', null);
|
|
|
|
$services[$service->getObjectName()] = $service;
|
|
}
|
|
|
|
return $services;
|
|
}
|
|
|
|
public function getUniqueIdentifier()
|
|
{
|
|
return $this->getObjectName();
|
|
}
|
|
|
|
/**
|
|
* @return object
|
|
* @throws \Icinga\Exception\NotFoundError
|
|
*/
|
|
public function export()
|
|
{
|
|
if ($this->get('host_id')) {
|
|
return $this->exportSetOnHost();
|
|
} else {
|
|
return $this->exportTemplate();
|
|
}
|
|
}
|
|
|
|
protected function exportSetOnHost()
|
|
{
|
|
// TODO.
|
|
throw new RuntimeException('Not yet');
|
|
}
|
|
|
|
/**
|
|
* @return object
|
|
* @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) {
|
|
$deleteIds[] = (int) $service->get('id');
|
|
}
|
|
|
|
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) {
|
|
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()
|
|
{
|
|
$connection = $this->getConnection();
|
|
$db = $connection->getDbAdapter();
|
|
|
|
/** @var IcingaService[] $services */
|
|
$services = IcingaService::loadAll(
|
|
$connection,
|
|
$db->select()->from('icinga_service')
|
|
->where('service_set_id = ?', $this->get('id'))
|
|
);
|
|
|
|
return $services;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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 object **/\n"
|
|
. '/* ' . $e->getMessage() . ' */'
|
|
);
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
}
|