summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Common/IcingaRedis.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icingadb/Common/IcingaRedis.php323
1 files changed, 323 insertions, 0 deletions
diff --git a/library/Icingadb/Common/IcingaRedis.php b/library/Icingadb/Common/IcingaRedis.php
new file mode 100644
index 0000000..a22a0f0
--- /dev/null
+++ b/library/Icingadb/Common/IcingaRedis.php
@@ -0,0 +1,323 @@
+<?php
+
+/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Common;
+
+use Exception;
+use Generator;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Predis\Client as Redis;
+
+class IcingaRedis
+{
+ /** @var static The singleton */
+ protected static $instance;
+
+ /** @var Redis Connection to the Icinga Redis */
+ private $redis;
+
+ /** @var bool true if no connection attempt was successful */
+ private $redisUnavailable = false;
+
+ /**
+ * Get the singleton
+ *
+ * @return static
+ */
+ public static function instance(): self
+ {
+ if (self::$instance === null) {
+ self::$instance = new static();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Get whether Redis is unavailable
+ *
+ * @return bool
+ */
+ public static function isUnavailable(): bool
+ {
+ $self = self::instance();
+
+ if (! $self->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];
+ }
+}