diff options
Diffstat (limited to 'library/Director/IcingaConfig/IcingaConfig.php')
-rw-r--r-- | library/Director/IcingaConfig/IcingaConfig.php | 781 |
1 files changed, 781 insertions, 0 deletions
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 @@ +<?php + +namespace Icinga\Module\Director\IcingaConfig; + +use Icinga\Application\Benchmark; +use Icinga\Application\Hook; +use Icinga\Application\Icinga; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Application\MemoryLimit; +use Icinga\Module\Director\Db\Cache\PrefetchCache; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Hook\ShipConfigFilesHook; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Objects\IcingaHost; +use Icinga\Module\Director\Objects\IcingaZone; +use InvalidArgumentException; +use LogicException; +use RuntimeException; + +class IcingaConfig +{ + protected $files = array(); + + protected $checksum; + + protected $zoneMap = array(); + + protected $lastActivityChecksum; + + /** @var \Zend_Db_Adapter_Abstract */ + protected $db; + + protected $connection; + + protected $generationTime; + + protected $configFormat; + + protected $deploymentModeV1; + + public static $table = 'director_generated_config'; + + public function __construct(Db $connection) + { + // Make sure module hooks are loaded: + Icinga::app()->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" + } + '); + } +} |