summaryrefslogtreecommitdiffstats
path: root/modules/monitoring/library/Monitoring/Command/Transport
diff options
context:
space:
mode:
Diffstat (limited to 'modules/monitoring/library/Monitoring/Command/Transport')
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php291
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php170
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php22
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php168
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php465
5 files changed, 1116 insertions, 0 deletions
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
new file mode 100644
index 0000000..06e6afd
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
@@ -0,0 +1,291 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Icinga\Application\Hook\AuditHook;
+use Icinga\Application\Logger;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Module\Monitoring\Command\IcingaApiCommand;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Renderer\IcingaApiCommandRenderer;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+use Icinga\Module\Monitoring\Exception\CurlException;
+use Icinga\Module\Monitoring\Web\Rest\RestRequest;
+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($app)
+ {
+ $this->renderer->setApp($app);
+
+ return $this;
+ }
+
+ /**
+ * Get the API host
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Set the API host
+ *
+ * @param string $host
+ *
+ * @return $this
+ */
+ public function setHost($host)
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ /**
+ * Get the API password
+ *
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ /**
+ * Set the API password
+ *
+ * @param string $password
+ *
+ * @return $this
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+
+ return $this;
+ }
+
+ /**
+ * Get the API port
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Set the API port
+ *
+ * @param int $port
+ *
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ $this->port = (int) $port;
+
+ return $this;
+ }
+
+ /**
+ * Get the API username
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Set the API username
+ *
+ * @param string $username
+ *
+ * @return $this
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+
+ return $this;
+ }
+
+ /**
+ * Get URI for endpoint
+ *
+ * @param string $endpoint
+ *
+ * @return string
+ */
+ protected function getUriFor($endpoint)
+ {
+ return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint);
+ }
+
+ 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
+ );
+
+ try {
+ $response = RestRequest::post($this->getUriFor($command->getEndpoint()))
+ ->authenticateWith($this->getUsername(), $this->getPassword())
+ ->sendJson()
+ ->noStrictSsl()
+ ->setPayload($command->getData())
+ ->send();
+ } catch (JsonDecodeException $e) {
+ throw new CommandTransportException(
+ 'Got invalid JSON response from the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ }
+
+ if (isset($response['error'])) {
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command: %u %s',
+ $response['error'],
+ $response['status']
+ );
+ }
+ $result = array_pop($response['results']);
+ if (! empty($result)
+ && ($result['code'] < 200 || $result['code'] >= 300)
+ ) {
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command: %u %s',
+ $result['code'],
+ $result['status']
+ );
+ }
+ if ($command->hasNext()) {
+ $this->sendCommand($command->getNext());
+ }
+ }
+
+ /**
+ * Send the Icinga command over the Icinga 2 API
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws CommandTransportException
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ $this->sendCommand($this->renderer->render($command));
+ }
+
+ /**
+ * Try to connect to the API
+ *
+ * @throws CommandTransportException In case of failure
+ */
+ public function probe()
+ {
+ $request = RestRequest::get($this->getUriFor(null))
+ ->authenticateWith($this->getUsername(), $this->getPassword())
+ ->noStrictSsl();
+
+ try {
+ $response = $request->send();
+ } catch (CurlException $e) {
+ throw new CommandTransportException(
+ 'Couldn\'t connect to the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ } catch (JsonDecodeException $e) {
+ throw new CommandTransportException(
+ 'Got invalid JSON response from the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ }
+
+ if (isset($response['error'])) {
+ throw new CommandTransportException(
+ 'Can\'t connect to the Icinga 2 API: %u %s',
+ $response['error'],
+ $response['status']
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
new file mode 100644
index 0000000..4086dec
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
@@ -0,0 +1,170 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Object\ObjectCommand;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+
+/**
+ * Command transport
+ *
+ * This class is subject to change as we do not have environments yet (#4471).
+ */
+class CommandTransport implements CommandTransportInterface
+{
+ /**
+ * Transport configuration
+ *
+ * @var Config
+ */
+ protected static $config;
+
+ /**
+ * Get transport configuration
+ *
+ * @return Config
+ *
+ * @throws ConfigurationError
+ */
+ public static function getConfig()
+ {
+ if (static::$config === null) {
+ $config = Config::module('monitoring', 'commandtransports');
+ if ($config->isEmpty()) {
+ throw new ConfigurationError(
+ mt('monitoring', '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 LocalCommandFile|RemoteCommandFile|ApiCommandTransport
+ *
+ * @throws ConfigurationError
+ */
+ public static function createTransport(ConfigObject $config)
+ {
+ $config = clone $config;
+ switch (strtolower($config->transport)) {
+ case RemoteCommandFile::TRANSPORT:
+ $transport = new RemoteCommandFile();
+ break;
+ case ApiCommandTransport::TRANSPORT:
+ $transport = new ApiCommandTransport();
+ break;
+ case LocalCommandFile::TRANSPORT:
+ case '': // Casting null to string is the empty string
+ $transport = new LocalCommandFile();
+ break;
+ default:
+ throw new ConfigurationError(
+ mt(
+ 'monitoring',
+ 'Cannot create command transport "%s". Invalid transport'
+ . ' defined in "%s". Use one of "%s", "%s" or "%s".'
+ ),
+ $config->transport,
+ static::getConfig()->getConfigFile(),
+ LocalCommandFile::TRANSPORT,
+ RemoteCommandFile::TRANSPORT,
+ 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
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ $errors = array();
+
+ foreach (static::getConfig() as $name => $transportConfig) {
+ $transport = static::createTransport($transportConfig);
+ if ($this->transferPossible($command, $transport)) {
+ try {
+ $transport->send($command, $now);
+ } catch (Exception $e) {
+ Logger::error($e);
+ $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.'));
+ continue; // Try the next transport
+ }
+
+ return; // The command was successfully sent
+ }
+ }
+
+ if (! empty($errors)) {
+ throw new CommandTransportException(implode("\n", $errors));
+ }
+
+ throw new CommandTransportException(
+ mt(
+ 'monitoring',
+ 'Failed to send external Icinga command. No transport has been configured'
+ . ' for this instance. Please contact your Icinga Web administrator.'
+ )
+ );
+ }
+
+ /**
+ * Return whether it is possible to send the given command using the given transport
+ *
+ * @param IcingaCommand $command
+ * @param CommandTransportInterface $transport
+ *
+ * @return bool
+ */
+ protected function transferPossible($command, $transport)
+ {
+ if (! method_exists($transport, 'getInstance') || !$command instanceof ObjectCommand) {
+ return true;
+ }
+
+ $transportInstance = $transport->getInstance();
+ if (! $transportInstance || $transportInstance === 'none') {
+ return true;
+ }
+
+ return strtolower($transportInstance) === strtolower($command->getObject()->instance_name);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php
new file mode 100644
index 0000000..e9cb086
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Icinga\Module\Monitoring\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 \Icinga\Module\Monitoring\Exception\CommandTransportException If sending the Icinga command failed
+ */
+ public function send(IcingaCommand $command, $now = null);
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
new file mode 100644
index 0000000..891a46f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
@@ -0,0 +1,168 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Exception;
+use RuntimeException;
+use Icinga\Application\Logger;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+use Icinga\Util\File;
+
+/**
+ * A local Icinga command file
+ */
+class LocalCommandFile implements CommandTransportInterface
+{
+ /**
+ * Transport identifier
+ */
+ const TRANSPORT = 'local';
+
+ /**
+ * The name of the Icinga instance this transport will transfer commands to
+ *
+ * @var string
+ */
+ protected $instanceName;
+
+ /**
+ * Path to the icinga command file
+ *
+ * @var String
+ */
+ protected $path;
+
+ /**
+ * Mode used to open the icinga command file
+ *
+ * @var string
+ */
+ protected $openMode = 'wn';
+
+ /**
+ * Command renderer
+ *
+ * @var IcingaCommandFileCommandRenderer
+ */
+ protected $renderer;
+
+ /**
+ * Create a new local command file command transport
+ */
+ public function __construct()
+ {
+ $this->renderer = new IcingaCommandFileCommandRenderer();
+ }
+
+ /**
+ * Set the name of the Icinga instance this transport will transfer commands to
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setInstance($name)
+ {
+ $this->instanceName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the Icinga instance this transport will transfer commands to
+ *
+ * @return string
+ */
+ public function getInstance()
+ {
+ return $this->instanceName;
+ }
+
+ /**
+ * Set the path to the local Icinga command file
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->path = (string) $path;
+ return $this;
+ }
+
+ /**
+ * Get the path to the local Icinga command file
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Set the mode used to open the icinga command file
+ *
+ * @param string $openMode
+ *
+ * @return $this
+ */
+ public function setOpenMode($openMode)
+ {
+ $this->openMode = (string) $openMode;
+ return $this;
+ }
+
+ /**
+ * Get the mode used to open the icinga command file
+ *
+ * @return string
+ */
+ public function getOpenMode()
+ {
+ return $this->openMode;
+ }
+
+ /**
+ * Write the command to the local Icinga command file
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws ConfigurationError
+ * @throws CommandTransportException
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ if (! isset($this->path)) {
+ throw new ConfigurationError(
+ 'Can\'t send external Icinga Command. Path to the local command file is missing'
+ );
+ }
+ $commandString = $this->renderer->render($command, $now);
+ Logger::debug(
+ 'Sending external Icinga command "%s" to the local command file "%s"',
+ $commandString,
+ $this->path
+ );
+ try {
+ $file = new File($this->path, $this->openMode);
+ $file->fwrite($commandString . "\n");
+ } catch (Exception $e) {
+ $message = $e->getMessage();
+ if ($e instanceof RuntimeException && ($pos = strrpos($message, ':')) !== false) {
+ // Assume RuntimeException thrown by SplFileObject in the format: __METHOD__ . "({$filename}): Message"
+ $message = substr($message, $pos + 1);
+ }
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command to the local command file "%s": %s',
+ $this->path,
+ $message
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
new file mode 100644
index 0000000..8619e87
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
@@ -0,0 +1,465 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Icinga\Application\Logger;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+
+/**
+ * A remote Icinga command file
+ *
+ * Key-based SSH login must be possible for the user to log in as on the remote host
+ */
+class RemoteCommandFile implements CommandTransportInterface
+{
+ /**
+ * Transport identifier
+ */
+ const TRANSPORT = 'remote';
+
+ /**
+ * The name of the Icinga instance this transport will transfer commands to
+ *
+ * @var string
+ */
+ protected $instanceName;
+
+ /**
+ * Remote host
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * Port to connect to on the remote host
+ *
+ * @var int
+ */
+ protected $port = 22;
+
+ /**
+ * User to log in as on the remote host
+ *
+ * Defaults to current PHP process' user
+ *
+ * @var string
+ */
+ protected $user;
+
+ /**
+ * Path to the private key file for the key-based authentication
+ *
+ * @var string
+ */
+ protected $privateKey;
+
+ /**
+ * Path to the Icinga command file on the remote host
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Command renderer
+ *
+ * @var IcingaCommandFileCommandRenderer
+ */
+ protected $renderer;
+
+ /**
+ * SSH subprocess pipes
+ *
+ * @var array
+ */
+ protected $sshPipes;
+
+ /**
+ * SSH subprocess
+ *
+ * @var resource
+ */
+ protected $sshProcess;
+
+ /**
+ * Create a new remote command file command transport
+ */
+ public function __construct()
+ {
+ $this->renderer = new IcingaCommandFileCommandRenderer();
+ }
+
+ /**
+ * Set the name of the Icinga instance this transport will transfer commands to
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setInstance($name)
+ {
+ $this->instanceName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the Icinga instance this transport will transfer commands to
+ *
+ * @return string
+ */
+ public function getInstance()
+ {
+ return $this->instanceName;
+ }
+
+ /**
+ * Set the remote host
+ *
+ * @param string $host
+ *
+ * @return $this
+ */
+ public function setHost($host)
+ {
+ $this->host = (string) $host;
+ return $this;
+ }
+
+ /**
+ * Get the remote host
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Set the port to connect to on the remote host
+ *
+ * @param int $port
+ *
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ $this->port = (int) $port;
+ return $this;
+ }
+
+ /**
+ * Get the port to connect on the remote host
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Set the user to log in as on the remote host
+ *
+ * @param string $user
+ *
+ * @return $this
+ */
+ public function setUser($user)
+ {
+ $this->user = (string) $user;
+ return $this;
+ }
+
+ /**
+ * Get the user to log in as on the remote host
+ *
+ * Defaults to current PHP process' user
+ *
+ * @return string|null
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the path to the private key file
+ *
+ * @param string $privateKey
+ *
+ * @return $this
+ */
+ public function setPrivateKey($privateKey)
+ {
+ $this->privateKey = (string) $privateKey;
+ return $this;
+ }
+
+ /**
+ * Get the path to the private key
+ *
+ * @return string
+ */
+ public function getPrivateKey()
+ {
+ return $this->privateKey;
+ }
+
+ /**
+ * Use a given resource to set the user and the key
+ *
+ * @param ?string $resource
+ *
+ * @throws ConfigurationError
+ */
+ public function setResource($resource = null)
+ {
+ $config = ResourceFactory::getResourceConfig($resource);
+
+ if (! isset($config->user)) {
+ throw new ConfigurationError(
+ t("Can't send external Icinga Command. Remote user is missing")
+ );
+ }
+ if (! isset($config->private_key)) {
+ throw new ConfigurationError(
+ t("Can't send external Icinga Command. The private key for the remote user is missing")
+ );
+ }
+
+ $this->setUser($config->user);
+ $this->setPrivateKey($config->private_key);
+ }
+
+ /**
+ * Set the path to the Icinga command file on the remote host
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->path = (string) $path;
+ return $this;
+ }
+
+ /**
+ * Get the path to the Icinga command file on the remote host
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Write the command to the Icinga command file on the remote host
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws ConfigurationError
+ * @throws CommandTransportException
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ if (! isset($this->path)) {
+ throw new ConfigurationError(
+ 'Can\'t send external Icinga Command. Path to the remote command file is missing'
+ );
+ }
+ if (! isset($this->host)) {
+ throw new ConfigurationError('Can\'t send external Icinga Command. Remote host is missing');
+ }
+ $commandString = $this->renderer->render($command, $now);
+ Logger::debug(
+ 'Sending external Icinga command "%s" to the remote command file "%s:%u%s"',
+ $commandString,
+ $this->host,
+ $this->port,
+ $this->path
+ );
+ return $this->sendCommandString($commandString);
+ }
+
+ /**
+ * Get the SSH command
+ *
+ * @return string
+ */
+ protected function sshCommand()
+ {
+ $cmd = sprintf(
+ 'exec ssh -o BatchMode=yes -p %u',
+ $this->port
+ );
+ // -o BatchMode=yes for disabling interactive authentication methods
+
+ if (isset($this->user)) {
+ $cmd .= ' -l ' . escapeshellarg($this->user);
+ }
+
+ if (isset($this->privateKey)) {
+ // TODO: StrictHostKeyChecking=no for compat only, must be removed
+ $cmd .= ' -o StrictHostKeyChecking=no'
+ . ' -i ' . escapeshellarg($this->privateKey);
+ }
+
+ $cmd .= sprintf(
+ ' %s "cat > %s"',
+ escapeshellarg($this->host),
+ escapeshellarg($this->path)
+ );
+
+ return $cmd;
+ }
+
+ /**
+ * Send the command over SSH
+ *
+ * @param string $commandString
+ *
+ * @throws CommandTransportException
+ */
+ protected function sendCommandString($commandString)
+ {
+ if ($this->isSshAlive()) {
+ $ret = fwrite($this->sshPipes[0], $commandString . "\n");
+ if ($ret === false) {
+ $this->throwSshFailure('Cannot write to the remote command pipe');
+ } elseif ($ret !== strlen($commandString) + 1) {
+ $this->throwSshFailure(
+ 'Failed to write the whole command to the remote command pipe'
+ );
+ }
+ } else {
+ $this->throwSshFailure();
+ }
+ }
+
+ /**
+ * Get the pipes of the SSH subprocess
+ *
+ * @return array
+ */
+ protected function getSshPipes()
+ {
+ if ($this->sshPipes === null) {
+ $this->forkSsh();
+ }
+
+ return $this->sshPipes;
+ }
+
+ /**
+ * Get the SSH subprocess
+ *
+ * @return resource
+ */
+ protected function getSshProcess()
+ {
+ if ($this->sshProcess === null) {
+ $this->forkSsh();
+ }
+
+ return $this->sshProcess;
+ }
+
+ /**
+ * Get the status of the SSH subprocess
+ *
+ * @param string $what
+ *
+ * @return mixed
+ */
+ protected function getSshProcessStatus($what = null)
+ {
+ $status = proc_get_status($this->getSshProcess());
+ if ($what === null) {
+ return $status;
+ } else {
+ return $status[$what];
+ }
+ }
+
+ /**
+ * Get whether the SSH subprocess is alive
+ *
+ * @return bool
+ */
+ protected function isSshAlive()
+ {
+ return $this->getSshProcessStatus('running');
+ }
+
+ /**
+ * Fork SSH subprocess
+ *
+ * @throws CommandTransportException If fork fails
+ */
+ protected function forkSsh()
+ {
+ $descriptors = array(
+ 0 => array('pipe', 'r'),
+ 1 => array('pipe', 'w'),
+ 2 => array('pipe', 'w')
+ );
+
+ $this->sshProcess = proc_open($this->sshCommand(), $descriptors, $this->sshPipes);
+
+ if (! is_resource($this->sshProcess)) {
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command: Failed to fork SSH'
+ );
+ }
+ }
+
+ /**
+ * Read from STDERR
+ *
+ * @return string
+ */
+ protected function readStderr()
+ {
+ return stream_get_contents($this->sshPipes[2]);
+ }
+
+ /**
+ * Throw SSH failure
+ *
+ * @param string $msg
+ *
+ * @throws CommandTransportException
+ */
+ protected function throwSshFailure($msg = 'Can\'t send external Icinga command')
+ {
+ throw new CommandTransportException(
+ '%s: %s',
+ $msg,
+ $this->readStderr() . var_export($this->getSshProcessStatus(), true)
+ );
+ }
+
+ /**
+ * Close SSH pipes and SSH subprocess
+ */
+ public function __destruct()
+ {
+ if (is_resource($this->sshProcess)) {
+ fclose($this->sshPipes[0]);
+ fclose($this->sshPipes[1]);
+ fclose($this->sshPipes[2]);
+
+ proc_close($this->sshProcess);
+ }
+ }
+}