diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:44:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:44:46 +0000 |
commit | b18bc644404e02b57635bfcc8258e85abb141146 (patch) | |
tree | 686512eacb2dba0055277ef7ec2f28695b3418ea /library/Icingadb/Command/Transport | |
parent | Initial commit. (diff) | |
download | icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.tar.xz icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.zip |
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Icingadb/Command/Transport')
6 files changed, 565 insertions, 0 deletions
diff --git a/library/Icingadb/Command/Transport/ApiCommandException.php b/library/Icingadb/Command/Transport/ApiCommandException.php new file mode 100644 index 0000000..5449a7d --- /dev/null +++ b/library/Icingadb/Command/Transport/ApiCommandException.php @@ -0,0 +1,14 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Command\Transport; + +use Icinga\Exception\IcingaException; + +/** + * Exception thrown if a command was not successful + */ +class ApiCommandException extends IcingaException +{ +} diff --git a/library/Icingadb/Command/Transport/ApiCommandTransport.php b/library/Icingadb/Command/Transport/ApiCommandTransport.php new file mode 100644 index 0000000..370d705 --- /dev/null +++ b/library/Icingadb/Command/Transport/ApiCommandTransport.php @@ -0,0 +1,353 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Command\Transport; + +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Icinga\Application\Hook\AuditHook; +use Icinga\Application\Logger; +use Icinga\Exception\Json\JsonDecodeException; +use Icinga\Module\Icingadb\Command\IcingaApiCommand; +use Icinga\Module\Icingadb\Command\IcingaCommand; +use Icinga\Module\Icingadb\Command\Renderer\IcingaApiCommandRenderer; +use Icinga\Util\Json; + +/** + * Command transport over Icinga 2's REST API + */ +class ApiCommandTransport implements CommandTransportInterface +{ + /** + * Transport identifier + */ + const TRANSPORT = 'api'; + + /** + * API host + * + * @var string + */ + protected $host; + + /** + * API password + * + * @var string + */ + protected $password; + + /** + * API port + * + * @var int + */ + protected $port = 5665; + + /** + * Command renderer + * + * @var IcingaApiCommandRenderer + */ + protected $renderer; + + /** + * API username + * + * @var string + */ + protected $username; + + /** + * Create a new API command transport + */ + public function __construct() + { + $this->renderer = new IcingaApiCommandRenderer(); + } + + /** + * Set the name of the Icinga application object + * + * @param string $app + * + * @return $this + */ + public function setApp(string $app): self + { + $this->renderer->setApp($app); + + return $this; + } + + /** + * Get the API host + * + * @return string + */ + public function getHost(): string + { + if ($this->host === null) { + throw new \LogicException( + 'You are accessing an unset property. Please make sure to set it beforehand.' + ); + } + + return $this->host; + } + + /** + * Set the API host + * + * @param string $host + * + * @return $this + */ + public function setHost(string $host): self + { + $this->host = $host; + + return $this; + } + + /** + * Get the API password + * + * @return string + */ + public function getPassword(): string + { + if ($this->password === null) { + throw new \LogicException( + 'You are accessing an unset property. Please make sure to set it beforehand.' + ); + } + + return $this->password; + } + + /** + * Set the API password + * + * @param string $password + * + * @return $this + */ + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * Get the API port + * + * @return int + */ + public function getPort(): int + { + return $this->port; + } + + /** + * Set the API port + * + * @param int $port + * + * @return $this + */ + public function setPort(int $port): self + { + $this->port = $port; + + return $this; + } + + /** + * Get the API username + * + * @return string + */ + public function getUsername(): string + { + if ($this->username === null) { + throw new \LogicException( + 'You are accessing an unset property. Please make sure to set it beforehand.' + ); + } + + return $this->username; + } + + /** + * Set the API username + * + * @param string $username + * + * @return $this + */ + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + /** + * Get URI for endpoint + * + * @param string $endpoint + * + * @return string + */ + protected function getUriFor(string $endpoint): string + { + return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint); + } + + /** + * Send the given command to the icinga2's REST API + * + * @param IcingaApiCommand $command + * + * @return mixed + */ + protected function sendCommand(IcingaApiCommand $command) + { + Logger::debug( + 'Sending Icinga command "%s" to the API "%s:%u"', + $command->getEndpoint(), + $this->getHost(), + $this->getPort() + ); + + $data = $command->getData(); + $payload = Json::encode($data); + AuditHook::logActivity( + 'monitoring/command', + "Issued command {$command->getEndpoint()} with the following payload: $payload", + $data + ); + + $headers = ['Accept' => 'application/json']; + if ($command->getMethod() !== 'POST') { + $headers['X-HTTP-Method-Override'] = $command->getMethod(); + } + + try { + $response = (new Client()) + ->post($this->getUriFor($command->getEndpoint()), [ + 'auth' => [$this->getUsername(), $this->getPassword()], + 'headers' => $headers, + 'json' => $command->getData(), + 'http_errors' => false, + 'verify' => false + ]); + } catch (GuzzleException $e) { + throw new CommandTransportException( + 'Can\'t connect to the Icinga 2 API: %u %s', + $e->getCode(), + $e->getMessage() + ); + } + + try { + $responseData = Json::decode((string) $response->getBody(), true); + } catch (JsonDecodeException $e) { + throw new CommandTransportException( + 'Got invalid JSON response from the Icinga 2 API: %s', + $e->getMessage() + ); + } + + if (! isset($responseData['results']) || empty($responseData['results'])) { + if (isset($responseData['error'])) { + throw new ApiCommandException( + 'Can\'t send external Icinga command: %u %s', + $responseData['error'], + $responseData['status'] + ); + } + + return; + } + + $errorResult = $responseData['results'][0]; + if (isset($errorResult['code']) && ($errorResult['code'] < 200 || $errorResult['code'] >= 300)) { + throw new ApiCommandException( + 'Can\'t send external Icinga command: %u %s', + $errorResult['code'], + $errorResult['status'] + ); + } + + return $responseData['results']; + } + + /** + * Send the Icinga command over the Icinga 2 API + * + * @param IcingaCommand $command + * @param int|null $now + * + * @throws CommandTransportException + * + * @return mixed + */ + public function send(IcingaCommand $command, int $now = null) + { + return $this->sendCommand($this->renderer->render($command)); + } + + /** + * Try to connect to the API + * + * @return void + * + * @throws CommandTransportException In case the connection was not successful + */ + public function probe() + { + try { + $response = (new Client(['timeout' => 15])) + ->get($this->getUriFor(''), [ + 'auth' => [$this->getUsername(), $this->getPassword()], + 'headers' => ['Accept' => 'application/json'], + 'http_errors' => false, + 'verify' => false + ]); + } catch (GuzzleException $e) { + throw new CommandTransportException( + 'Can\'t connect to the Icinga 2 API: %u %s', + $e->getCode(), + $e->getMessage() + ); + } + + try { + $responseData = Json::decode((string) $response->getBody(), true); + } catch (JsonDecodeException $e) { + throw new CommandTransportException( + 'Got invalid JSON response from the Icinga 2 API: %s', + $e->getMessage() + ); + } + + if (! isset($responseData['results']) || empty($responseData['results'])) { + throw new CommandTransportException( + 'Got invalid response from the Icinga 2 API: %s', + JSON::encode($responseData) + ); + } + + $result = array_pop($responseData['results']); + if (! isset($result['user']) || $result['user'] !== $this->getUsername()) { + throw new CommandTransportException( + 'Got invalid response from the Icinga 2 API: %s', + JSON::encode($responseData) + ); + } + } +} diff --git a/library/Icingadb/Command/Transport/CommandTransport.php b/library/Icingadb/Command/Transport/CommandTransport.php new file mode 100644 index 0000000..ea125bc --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransport.php @@ -0,0 +1,130 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Command\Transport; + +use Exception; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Data\ConfigObject; +use Icinga\Exception\ConfigurationError; +use Icinga\Module\Icingadb\Command\IcingaCommand; + +/** + * Command transport + */ +class CommandTransport implements CommandTransportInterface +{ + /** + * Transport configuration + * + * @var Config + */ + protected static $config; + + /** + * Get transport configuration + * + * @return Config + * + * @throws ConfigurationError + */ + public static function getConfig(): Config + { + if (static::$config === null) { + $config = Config::module('icingadb', 'commandtransports'); + if ($config->isEmpty()) { + throw new ConfigurationError( + t('No command transports have been configured in "%s".'), + $config->getConfigFile() + ); + } + + static::$config = $config; + } + + return static::$config; + } + + /** + * Create a transport from config + * + * @param ConfigObject $config + * + * @return ApiCommandTransport + * + * @throws ConfigurationError + */ + public static function createTransport(ConfigObject $config): ApiCommandTransport + { + $config = clone $config; + switch (strtolower($config->transport)) { + case ApiCommandTransport::TRANSPORT: + $transport = new ApiCommandTransport(); + break; + default: + throw new ConfigurationError( + t('Cannot create command transport "%s". Invalid transport defined in "%s". Use one of: %s.'), + $config->transport, + static::getConfig()->getConfigFile(), + join(', ', [ApiCommandTransport::TRANSPORT]) + ); + } + + unset($config->transport); + foreach ($config as $key => $value) { + $method = 'set' . ucfirst($key); + if (! method_exists($transport, $method)) { + // Ignore settings from config that don't have a setter on the transport instead of throwing an + // exception here because the transport should throw an exception if it's not fully set up + // when being about to send a command + continue; + } + + $transport->$method($value); + } + + return $transport; + } + + /** + * Send the given command over an appropriate Icinga command transport + * + * This will try one configured transport after another until the command has been successfully sent. + * + * @param IcingaCommand $command The command to send + * @param int|null $now Timestamp of the command or null for now + * + * @throws CommandTransportException If sending the Icinga command failed + * + * @return mixed + */ + public function send(IcingaCommand $command, int $now = null) + { + $errors = []; + + foreach (static::getConfig() as $name => $transportConfig) { + $transport = static::createTransport($transportConfig); + + try { + $result = $transport->send($command, $now); + } catch (CommandTransportException $e) { + Logger::error($e); + $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.')); + continue; // Try the next transport + } + + return $result; // The command was successfully sent + } + + if (! empty($errors)) { + throw new CommandTransportException(implode("\n", $errors)); + } + + throw new CommandTransportException(t( + 'Failed to send external Icinga command. No transport has been configured' + . ' for this instance. Please contact your Icinga Web administrator.' + )); + } +} diff --git a/library/Icingadb/Command/Transport/CommandTransportConfig.php b/library/Icingadb/Command/Transport/CommandTransportConfig.php new file mode 100644 index 0000000..e17fa04 --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransportConfig.php @@ -0,0 +1,31 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Command\Transport; + +use Icinga\Repository\IniRepository; + +class CommandTransportConfig extends IniRepository +{ + protected $configs = [ + 'transport' => [ + 'name' => 'commandtransports', + 'module' => 'icingadb', + 'keyColumn' => 'name' + ] + ]; + + protected $queryColumns = [ + 'transport' => [ + 'name', + 'transport', + + // API options + 'host', + 'port', + 'username', + 'password' + ] + ]; +} diff --git a/library/Icingadb/Command/Transport/CommandTransportException.php b/library/Icingadb/Command/Transport/CommandTransportException.php new file mode 100644 index 0000000..2ca89d9 --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransportException.php @@ -0,0 +1,14 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Command\Transport; + +use Icinga\Exception\IcingaException; + +/** + * Exception thrown if a command was not sent + */ +class CommandTransportException extends IcingaException +{ +} diff --git a/library/Icingadb/Command/Transport/CommandTransportInterface.php b/library/Icingadb/Command/Transport/CommandTransportInterface.php new file mode 100644 index 0000000..ad07cb9 --- /dev/null +++ b/library/Icingadb/Command/Transport/CommandTransportInterface.php @@ -0,0 +1,23 @@ +<?php + +/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Command\Transport; + +use Icinga\Module\Icingadb\Command\IcingaCommand; + +/** + * Interface for Icinga command transports + */ +interface CommandTransportInterface +{ + /** + * Send an Icinga command over the Icinga command transport + * + * @param IcingaCommand $command The command to send + * @param int|null $now Timestamp of the command or null for now + * + * @throws CommandTransportException If sending the Icinga command failed + */ + public function send(IcingaCommand $command, int $now = null); +} |