598 lines
17 KiB
PHP
598 lines
17 KiB
PHP
<?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\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\Resolver\HostServiceBlacklist;
|
|
use InvalidArgumentException;
|
|
use Ramsey\Uuid\Uuid;
|
|
use stdClass;
|
|
|
|
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',
|
|
);
|
|
|
|
/** @var IcingaService[] Cached services */
|
|
protected $cachedServices = [];
|
|
|
|
/** @var IcingaService[]|null */
|
|
private $services;
|
|
|
|
/**
|
|
* Set the services to be cached
|
|
*
|
|
* @param $services IcingaService[]
|
|
* @return void
|
|
*/
|
|
public function setCachedServices($services)
|
|
{
|
|
$this->cachedServices = $services;
|
|
}
|
|
|
|
/**
|
|
* Get the cached services
|
|
*
|
|
* @return IcingaService[]
|
|
*/
|
|
public function getCachedServices()
|
|
{
|
|
return $this->cachedServices;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @param stdClass[] $services
|
|
* @return void
|
|
*/
|
|
public function setServices(array $services)
|
|
{
|
|
$existing = $this->getServices();
|
|
$uuidMap = [];
|
|
foreach ($existing as $service) {
|
|
$uuidMap[$service->getUniqueId()->getBytes()] = $service;
|
|
}
|
|
$this->services = [];
|
|
foreach ($services as $service) {
|
|
if (isset($service->uuid)) {
|
|
$uuid = Uuid::fromString($service->uuid)->getBytes();
|
|
$current = $uuidMap[$uuid] ?? IcingaService::create([], $this->connection);
|
|
} else {
|
|
if (! is_object($service)) {
|
|
var_dump($service);
|
|
exit;
|
|
}
|
|
$current = $existing[$service->object_name] ?? IcingaService::create([], $this->connection);
|
|
}
|
|
$current->setProperties((array) $service);
|
|
$this->services[] = $current;
|
|
}
|
|
}
|
|
|
|
protected function storeRelatedServices()
|
|
{
|
|
if ($this->services === null) {
|
|
$cachedServices = $this->getCachedServices();
|
|
if ($cachedServices) {
|
|
$this->services = $cachedServices;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$seen = [];
|
|
/** @var IcingaService $service */
|
|
foreach ($this->services as $service) {
|
|
$seen[$service->getUniqueId()->getBytes()] = true;
|
|
$service->set('service_set_id', $this->get('id'));
|
|
$service->store();
|
|
}
|
|
|
|
foreach ($this->fetchServices() as $service) {
|
|
if (!isset($seen[$service->getUniqueId()->getBytes()])) {
|
|
$service->delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
* @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();
|
|
}
|
|
|
|
public function beforeDelete()
|
|
{
|
|
$this->setCachedServices($this->getServices());
|
|
// 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)
|
|
{
|
|
$files = [];
|
|
$zone = $this->getRenderingZone($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);
|
|
foreach ($service->getRenderingZones($config) as $serviceZone) {
|
|
$file = $this->getConfigFileWithHeader($config, $serviceZone, $files);
|
|
$file->addObject($service);
|
|
}
|
|
}
|
|
|
|
if (empty($files)) {
|
|
// always print the header, so you have minimal info present
|
|
$this->getConfigFileWithHeader($config, $zone, $files);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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, $zone, &$files = [])
|
|
{
|
|
if (!isset($files[$zone])) {
|
|
$file = $config->configFile(
|
|
'zones.d/' . $zone . '/servicesets'
|
|
);
|
|
|
|
$file->addContent($this->getConfigHeaderComment($config));
|
|
$files[$zone] = $file;
|
|
}
|
|
|
|
return $files[$zone];
|
|
}
|
|
|
|
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 getServices(): array
|
|
{
|
|
if ($this->services !== null) {
|
|
return $this->services;
|
|
}
|
|
|
|
if ($this->hasBeenLoadedFromDb()) {
|
|
return $this->fetchServices();
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return IcingaService[]
|
|
*/
|
|
public function fetchServices(): array
|
|
{
|
|
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 onStore()
|
|
{
|
|
$this->storeRelatedServices();
|
|
}
|
|
|
|
public function hasBeenModified()
|
|
{
|
|
if ($this->services !== null) {
|
|
foreach ($this->services as $service) {
|
|
if ($service->hasBeenModified()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return parent::hasBeenModified();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|