1<?php 2/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ 3 4namespace Icinga\Module\Monitoring\Command\Transport; 5 6use Icinga\Application\Logger; 7use Icinga\Data\ResourceFactory; 8use Icinga\Exception\ConfigurationError; 9use Icinga\Module\Monitoring\Command\IcingaCommand; 10use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer; 11use Icinga\Module\Monitoring\Exception\CommandTransportException; 12 13/** 14 * A remote Icinga command file 15 * 16 * Key-based SSH login must be possible for the user to log in as on the remote host 17 */ 18class RemoteCommandFile implements CommandTransportInterface 19{ 20 /** 21 * Transport identifier 22 */ 23 const TRANSPORT = 'remote'; 24 25 /** 26 * The name of the Icinga instance this transport will transfer commands to 27 * 28 * @var string 29 */ 30 protected $instanceName; 31 32 /** 33 * Remote host 34 * 35 * @var string 36 */ 37 protected $host; 38 39 /** 40 * Port to connect to on the remote host 41 * 42 * @var int 43 */ 44 protected $port = 22; 45 46 /** 47 * User to log in as on the remote host 48 * 49 * Defaults to current PHP process' user 50 * 51 * @var string 52 */ 53 protected $user; 54 55 /** 56 * Path to the private key file for the key-based authentication 57 * 58 * @var string 59 */ 60 protected $privateKey; 61 62 /** 63 * Path to the Icinga command file on the remote host 64 * 65 * @var string 66 */ 67 protected $path; 68 69 /** 70 * Command renderer 71 * 72 * @var IcingaCommandFileCommandRenderer 73 */ 74 protected $renderer; 75 76 /** 77 * SSH subprocess pipes 78 * 79 * @var array 80 */ 81 protected $sshPipes; 82 83 /** 84 * SSH subprocess 85 * 86 * @var resource 87 */ 88 protected $sshProcess; 89 90 /** 91 * Create a new remote command file command transport 92 */ 93 public function __construct() 94 { 95 $this->renderer = new IcingaCommandFileCommandRenderer(); 96 } 97 98 /** 99 * Set the name of the Icinga instance this transport will transfer commands to 100 * 101 * @param string $name 102 * 103 * @return $this 104 */ 105 public function setInstance($name) 106 { 107 $this->instanceName = $name; 108 return $this; 109 } 110 111 /** 112 * Return the name of the Icinga instance this transport will transfer commands to 113 * 114 * @return string 115 */ 116 public function getInstance() 117 { 118 return $this->instanceName; 119 } 120 121 /** 122 * Set the remote host 123 * 124 * @param string $host 125 * 126 * @return $this 127 */ 128 public function setHost($host) 129 { 130 $this->host = (string) $host; 131 return $this; 132 } 133 134 /** 135 * Get the remote host 136 * 137 * @return string 138 */ 139 public function getHost() 140 { 141 return $this->host; 142 } 143 144 /** 145 * Set the port to connect to on the remote host 146 * 147 * @param int $port 148 * 149 * @return $this 150 */ 151 public function setPort($port) 152 { 153 $this->port = (int) $port; 154 return $this; 155 } 156 157 /** 158 * Get the port to connect on the remote host 159 * 160 * @return int 161 */ 162 public function getPort() 163 { 164 return $this->port; 165 } 166 167 /** 168 * Set the user to log in as on the remote host 169 * 170 * @param string $user 171 * 172 * @return $this 173 */ 174 public function setUser($user) 175 { 176 $this->user = (string) $user; 177 return $this; 178 } 179 180 /** 181 * Get the user to log in as on the remote host 182 * 183 * Defaults to current PHP process' user 184 * 185 * @return string|null 186 */ 187 public function getUser() 188 { 189 return $this->user; 190 } 191 192 /** 193 * Set the path to the private key file 194 * 195 * @param string $privateKey 196 * 197 * @return $this 198 */ 199 public function setPrivateKey($privateKey) 200 { 201 $this->privateKey = (string) $privateKey; 202 return $this; 203 } 204 205 /** 206 * Get the path to the private key 207 * 208 * @return string 209 */ 210 public function getPrivateKey() 211 { 212 return $this->privateKey; 213 } 214 215 /** 216 * Use a given resource to set the user and the key 217 * 218 * @param string 219 * 220 * @throws ConfigurationError 221 */ 222 public function setResource($resource = null) 223 { 224 $config = ResourceFactory::getResourceConfig($resource); 225 226 if (! isset($config->user)) { 227 throw new ConfigurationError( 228 t("Can't send external Icinga Command. Remote user is missing") 229 ); 230 } 231 if (! isset($config->private_key)) { 232 throw new ConfigurationError( 233 t("Can't send external Icinga Command. The private key for the remote user is missing") 234 ); 235 } 236 237 $this->setUser($config->user); 238 $this->setPrivateKey($config->private_key); 239 } 240 241 /** 242 * Set the path to the Icinga command file on the remote host 243 * 244 * @param string $path 245 * 246 * @return $this 247 */ 248 public function setPath($path) 249 { 250 $this->path = (string) $path; 251 return $this; 252 } 253 254 /** 255 * Get the path to the Icinga command file on the remote host 256 * 257 * @return string 258 */ 259 public function getPath() 260 { 261 return $this->path; 262 } 263 264 /** 265 * Write the command to the Icinga command file on the remote host 266 * 267 * @param IcingaCommand $command 268 * @param int|null $now 269 * 270 * @throws ConfigurationError 271 * @throws CommandTransportException 272 */ 273 public function send(IcingaCommand $command, $now = null) 274 { 275 if (! isset($this->path)) { 276 throw new ConfigurationError( 277 'Can\'t send external Icinga Command. Path to the remote command file is missing' 278 ); 279 } 280 if (! isset($this->host)) { 281 throw new ConfigurationError('Can\'t send external Icinga Command. Remote host is missing'); 282 } 283 $commandString = $this->renderer->render($command, $now); 284 Logger::debug( 285 'Sending external Icinga command "%s" to the remote command file "%s:%u%s"', 286 $commandString, 287 $this->host, 288 $this->port, 289 $this->path 290 ); 291 return $this->sendCommandString($commandString); 292 } 293 294 /** 295 * Get the SSH command 296 * 297 * @return string 298 */ 299 protected function sshCommand() 300 { 301 $cmd = sprintf( 302 'exec ssh -o BatchMode=yes -p %u', 303 $this->port 304 ); 305 // -o BatchMode=yes for disabling interactive authentication methods 306 307 if (isset($this->user)) { 308 $cmd .= ' -l ' . escapeshellarg($this->user); 309 } 310 311 if (isset($this->privateKey)) { 312 // TODO: StrictHostKeyChecking=no for compat only, must be removed 313 $cmd .= ' -o StrictHostKeyChecking=no' 314 . ' -i ' . escapeshellarg($this->privateKey); 315 } 316 317 $cmd .= sprintf( 318 ' %s "cat > %s"', 319 escapeshellarg($this->host), 320 escapeshellarg($this->path) 321 ); 322 323 return $cmd; 324 } 325 326 /** 327 * Send the command over SSH 328 * 329 * @param string $commandString 330 * 331 * @throws CommandTransportException 332 */ 333 protected function sendCommandString($commandString) 334 { 335 if ($this->isSshAlive()) { 336 $ret = fwrite($this->sshPipes[0], $commandString . "\n"); 337 if ($ret === false) { 338 $this->throwSshFailure('Cannot write to the remote command pipe'); 339 } elseif ($ret !== strlen($commandString) + 1) { 340 $this->throwSshFailure( 341 'Failed to write the whole command to the remote command pipe' 342 ); 343 } 344 } else { 345 $this->throwSshFailure(); 346 } 347 } 348 349 /** 350 * Get the pipes of the SSH subprocess 351 * 352 * @return array 353 */ 354 protected function getSshPipes() 355 { 356 if ($this->sshPipes === null) { 357 $this->forkSsh(); 358 } 359 360 return $this->sshPipes; 361 } 362 363 /** 364 * Get the SSH subprocess 365 * 366 * @return resource 367 */ 368 protected function getSshProcess() 369 { 370 if ($this->sshProcess === null) { 371 $this->forkSsh(); 372 } 373 374 return $this->sshProcess; 375 } 376 377 /** 378 * Get the status of the SSH subprocess 379 * 380 * @param string $what 381 * 382 * @return mixed 383 */ 384 protected function getSshProcessStatus($what = null) 385 { 386 $status = proc_get_status($this->getSshProcess()); 387 if ($what === null) { 388 return $status; 389 } else { 390 return $status[$what]; 391 } 392 } 393 394 /** 395 * Get whether the SSH subprocess is alive 396 * 397 * @return bool 398 */ 399 protected function isSshAlive() 400 { 401 return $this->getSshProcessStatus('running'); 402 } 403 404 /** 405 * Fork SSH subprocess 406 * 407 * @throws CommandTransportException If fork fails 408 */ 409 protected function forkSsh() 410 { 411 $descriptors = array( 412 0 => array('pipe', 'r'), 413 1 => array('pipe', 'w'), 414 2 => array('pipe', 'w') 415 ); 416 417 $this->sshProcess = proc_open($this->sshCommand(), $descriptors, $this->sshPipes); 418 419 if (! is_resource($this->sshProcess)) { 420 throw new CommandTransportException( 421 'Can\'t send external Icinga command: Failed to fork SSH' 422 ); 423 } 424 } 425 426 /** 427 * Read from STDERR 428 * 429 * @return string 430 */ 431 protected function readStderr() 432 { 433 return stream_get_contents($this->sshPipes[2]); 434 } 435 436 /** 437 * Throw SSH failure 438 * 439 * @param string $msg 440 * 441 * @throws CommandTransportException 442 */ 443 protected function throwSshFailure($msg = 'Can\'t send external Icinga command') 444 { 445 throw new CommandTransportException( 446 '%s: %s', 447 $msg, 448 $this->readStderr() . var_export($this->getSshProcessStatus(), true) 449 ); 450 } 451 452 /** 453 * Close SSH pipes and SSH subprocess 454 */ 455 public function __destruct() 456 { 457 if (is_resource($this->sshProcess)) { 458 fclose($this->sshPipes[0]); 459 fclose($this->sshPipes[1]); 460 fclose($this->sshPipes[2]); 461 462 proc_close($this->sshProcess); 463 } 464 } 465} 466