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]; } public function listNonGlobalZones(): array { if ($this->nonGlobalZones === null) { $this->nonGlobalZones = array_values($this->connection->enumNonglobalZones()); } return $this->nonGlobalZones; } /** * @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() . $this->renderIfwFallbackTemplate() ); 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 ); } protected function renderIfwFallbackTemplate(): string { return ' // Make sure config validates for Icinga < 2.14 with IfW 1.11 configuration. This might look weird, // but is intentional. get_object() does\'t work as expected at parse time. if (! globals.System || ! System.get_template || ! get_template(CheckCommand, "ifw-api-check-command")) { object CheckCommand "ifw-api" { import "plugin-check-command" } } '; } /** * @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" } '); } }