redisUnavailable && $self->redis === null) { try { $self->getConnection(); } catch (Exception $_) { // getConnection already logs the error } } return $self->redisUnavailable; } /** * Get the connection to the Icinga Redis * * @return Redis * * @throws Exception */ public function getConnection(): Redis { if ($this->redisUnavailable) { throw new Exception('Redis is still not available'); } elseif ($this->redis === null) { try { $primaryRedis = $this->getPrimaryRedis(); } catch (Exception $e) { try { $secondaryRedis = $this->getSecondaryRedis(); } catch (Exception $ee) { $this->redisUnavailable = true; Logger::error($ee); throw $e; } if ($secondaryRedis === null) { $this->redisUnavailable = true; throw $e; } $this->redis = $secondaryRedis; return $this->redis; } $primaryTimestamp = $this->getLastIcingaHeartbeat($primaryRedis); if ($primaryTimestamp <= time() - 60) { $secondaryRedis = $this->getSecondaryRedis(); if ($secondaryRedis === null) { $this->redis = $primaryRedis; return $this->redis; } $secondaryTimestamp = $this->getLastIcingaHeartbeat($secondaryRedis); if ($secondaryTimestamp > $primaryTimestamp) { $this->redis = $secondaryRedis; } else { $this->redis = $primaryRedis; } } else { $this->redis = $primaryRedis; } } return $this->redis; } /** * Fetch host states * * @param array $ids The host ids to fetch results for * @param array $columns The columns to include in the results * * @return Generator */ public static function fetchHostState(array $ids, array $columns): Generator { return self::fetchState('icinga:host:state', $ids, $columns); } /** * Fetch service states * * @param array $ids The service ids to fetch results for * @param array $columns The columns to include in the results * * @return Generator */ public static function fetchServiceState(array $ids, array $columns): Generator { return self::fetchState('icinga:service:state', $ids, $columns); } /** * Fetch object states * * @param string $key The object key to access * @param array $ids The object ids to fetch results for * @param array $columns The columns to include in the results * * @return Generator */ protected static function fetchState(string $key, array $ids, array $columns): Generator { try { $results = self::instance()->getConnection()->hmget($key, $ids); } catch (Exception $_) { // The error has already been logged elsewhere return; } foreach ($results as $i => $json) { if ($json !== null) { $data = json_decode($json, true); $keyMap = array_fill_keys($columns, null); unset($keyMap['is_overdue']); // Is calculated by Icinga DB, not Icinga 2, hence it's never in redis // TODO: Remove once https://github.com/Icinga/icinga2/issues/9427 is fixed $data['state_type'] = $data['state_type'] === 0 ? 'soft' : 'hard'; if (isset($data['in_downtime']) && is_bool($data['in_downtime'])) { $data['in_downtime'] = $data['in_downtime'] ? 'y' : 'n'; } if (isset($data['is_acknowledged']) && is_int($data['is_acknowledged'])) { $data['is_acknowledged'] = $data['is_acknowledged'] ? 'y' : 'n'; } yield $ids[$i] => array_intersect_key(array_merge($keyMap, $data), $keyMap); } } } /** * Get the last icinga heartbeat from redis * * @param Redis|null $redis * * @return float|int|null */ public static function getLastIcingaHeartbeat(Redis $redis = null) { if ($redis === null) { $redis = self::instance()->getConnection(); } // Predis doesn't support streams (yet). // https://github.com/predis/predis/issues/607#event-3640855190 $rs = $redis->executeRaw(['XREAD', 'COUNT', '1', 'STREAMS', 'icinga:stats', '0']); if (! is_array($rs)) { return null; } $key = null; foreach ($rs[0][1][0][1] as $kv) { if ($key === null) { $key = $kv; } else { if ($key === 'timestamp') { return $kv / 1000; } $key = null; } } return null; } /** * Get the primary redis instance * * @param Config|null $moduleConfig * @param Config|null $redisConfig * * @return Redis */ public static function getPrimaryRedis(Config $moduleConfig = null, Config $redisConfig = null): Redis { if ($moduleConfig === null) { $moduleConfig = Config::module('icingadb'); } if ($redisConfig === null) { $redisConfig = Config::module('icingadb', 'redis'); } $section = $redisConfig->getSection('redis1'); $redis = new Redis([ 'host' => $section->get('host', 'localhost'), 'port' => $section->get('port', 6380), 'password' => $section->get('password', ''), 'timeout' => 0.5 ] + self::getTlsParams($moduleConfig)); $redis->ping(); return $redis; } /** * Get the secondary redis instance if exists * * @param Config|null $moduleConfig * @param Config|null $redisConfig * * @return ?Redis */ public static function getSecondaryRedis(Config $moduleConfig = null, Config $redisConfig = null) { if ($moduleConfig === null) { $moduleConfig = Config::module('icingadb'); } if ($redisConfig === null) { $redisConfig = Config::module('icingadb', 'redis'); } $section = $redisConfig->getSection('redis2'); $host = $section->host; if (empty($host)) { return null; } $redis = new Redis([ 'host' => $host, 'port' => $section->get('port', 6380), 'password' => $section->get('password', ''), 'timeout' => 0.5 ] + self::getTlsParams($moduleConfig)); $redis->ping(); return $redis; } private static function getTlsParams(Config $config): array { $config = $config->getSection('redis'); if (! $config->get('tls', false)) { return []; } $ssl = []; if ($config->get('insecure')) { $ssl['verify_peer'] = false; $ssl['verify_peer_name'] = false; } else { $ca = $config->get('ca'); if ($ca !== null) { $ssl['cafile'] = $ca; } } $cert = $config->get('cert'); $key = $config->get('key'); if ($cert !== null && $key !== null) { $ssl['local_cert'] = $cert; $ssl['local_pk'] = $key; } return ['scheme' => 'tls', 'ssl' => $ssl]; } }