├── .gitignore ├── test ├── include.php └── NagiosLivestatusClientTest.php ├── phpunit.xml ├── composer.json ├── README.md └── lib └── Nagios └── Livestatus └── Client.php /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | vendor/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /test/include.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | test 10 | 11 | 12 | 13 | 14 | lib 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aashley/nagios-livestatus-client", 3 | "description": "An OO client to talk to MK Livestatus", 4 | "homepage": "https://github.com/aashley/nagios-livestatus-client", 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Adam Ashley", 10 | "email": "aashley@adamashley.name", 11 | "role": "Developer" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.1", 16 | "ext-json": "*", 17 | "ext-sockets": "*" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "@stable" 21 | }, 22 | "autoload": { 23 | "psr-0": { "Nagios\\Livestatus": "lib" } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Nagios MK Livestatus Client 2 | =========================== 3 | 4 | This package implements a PHP OO client for talking to the MK Livestatus 5 | Nagios Event Broker. 6 | 7 | This implementation is based on Lars Michelsen's 8 | [LivestatusSlave](http://nagios.larsmichelsen.com/livestatusslave/). 9 | 10 | Requirements 11 | ------------ 12 | 13 | * PHP 5.3.1+ 14 | * Sockets enabled 15 | * JSON enabled 16 | 17 | Usage 18 | ----- 19 | 20 | ``` php 21 | 'tcp', 27 | 'socketAddress' => '10.253.14.22', 28 | 'socketPort' => '6557', 29 | ); 30 | 31 | $client = new Client($options); 32 | 33 | $response = $client 34 | ->get('hosts') 35 | ->column('host_name') 36 | ->column('state') 37 | ->execute(); 38 | 39 | foreach ($response as $host) { 40 | print $host[0] . ": " . $host[1] . "\n"; 41 | } 42 | 43 | $response = $client 44 | ->get('hosts') 45 | ->column('host_name') 46 | ->column('state') 47 | ->executeAssoc(); 48 | 49 | foreach ($response as $host) { 50 | print $host['host_name'] . ": " . $host['state'] . "\n"; 51 | } 52 | 53 | $client->command( 54 | array( 55 | 'ACKNOWLEDGE_SVC_PROBLEM', 56 | 'example.com', 57 | 'some service', 2, 0, 1, 58 | 'username', 'Example comment' 59 | ) 60 | ); 61 | ``` 62 | 63 | Installation 64 | ------------ 65 | 66 | In composer add a dependancy on `aashley/nagios-livestatus-client` 67 | 68 | composer require aashley/nagios-livestatus-client 69 | -------------------------------------------------------------------------------- /test/NagiosLivestatusClientTest.php: -------------------------------------------------------------------------------- 1 | 'tcp', 16 | 'socketAddress' => '10.248.14.22', 17 | 'socketPort' => '6557' 18 | ); 19 | 20 | $client = new Client($options); 21 | 22 | return $client; 23 | } 24 | 25 | /** 26 | * @expectedException InvalidArgumentException 27 | */ 28 | public function testInvalidSocketType() 29 | { 30 | $options = array( 31 | 'socketType' => 'foo', 32 | ); 33 | 34 | $client = new Client($options); 35 | } 36 | 37 | /** 38 | * @expectedException InvalidArgumentException 39 | */ 40 | public function testSocketTypeUnixNoPath() 41 | { 42 | $options = array( 43 | 'socketType' => 'unix', 44 | 'socketPath' => '' 45 | ); 46 | 47 | $client = new Client($options); 48 | } 49 | 50 | /** 51 | * @expectedException InvalidArgumentException 52 | */ 53 | public function testSocketTypeTcpNoAddress() 54 | { 55 | $options = array( 56 | 'socketType' => 'tcp', 57 | 'socketAddress' => '', 58 | ); 59 | 60 | $client = new Client($options); 61 | } 62 | 63 | /** 64 | * @expectedException InvalidArgumentException 65 | */ 66 | public function testSocketTypeTcpNoPort() 67 | { 68 | $options = array( 69 | 'socketType' => 'tcp', 70 | 'socketAddress' => '10.253.14.22', 71 | 'socketPort' => '' 72 | ); 73 | 74 | $client = new Client($options); 75 | } 76 | 77 | public function testGetHosts() 78 | { 79 | $response = $this->createTcpClient() 80 | ->get('hosts') 81 | ->execute(); 82 | 83 | $this->assertGreaterThanOrEqual(2, count($response), "No hosts where returned by the search"); 84 | $this->assertEquals("accept_passive_checks", $response[0][0], "First column of header row not as expected"); 85 | } 86 | 87 | public function testGetHostsAssoc() 88 | { 89 | $response = $this->createTcpClient() 90 | ->get('hosts') 91 | ->executeAssoc(); 92 | 93 | $this->assertGreaterThanOrEqual(1, count($response), "No hosts where returned by the search"); 94 | $this->assertNotEquals("accept_passive_checks", $response[0][0], "Header row still in response"); 95 | $this->assertArrayHasKey("accept_passive_checks", $response[0], "Associative keys not set"); 96 | } 97 | 98 | public function testGetHostColumns() 99 | { 100 | $response = $this->createTcpClient() 101 | ->get('hosts') 102 | ->column('host_name') 103 | ->column('host_alias') 104 | ->execute(); 105 | 106 | $this->assertGreaterThanOrEqual(2, count($response), "No hosts where returned by the search"); 107 | $this->assertCount(2, $response[0], "Incorrect number of columns returned"); 108 | } 109 | 110 | public function testGetHostColumnsAssoc() 111 | { 112 | $response = $this->createTcpClient() 113 | ->get('hosts') 114 | ->column('host_name') 115 | ->column('host_alias') 116 | ->executeAssoc(); 117 | 118 | $this->assertGreaterThanOrEqual(2, count($response), "No hosts where returned by the search"); 119 | $this->assertCount(4, $response[0], "Incorrect number of columns returned"); 120 | $this->assertArrayHasKey("host_name", $response[0], "host_name column not available"); 121 | $this->assertArrayHasKey("host_alias", $response[0], "host_alias column not available"); 122 | } 123 | 124 | public function testGetHostFilter() 125 | { 126 | $allHosts = $this->createTcpClient() 127 | ->get('hosts') 128 | ->execute(); 129 | 130 | $filteredHosts = $this->createTcpClient() 131 | ->get('hosts') 132 | ->filter('state = 2') 133 | ->execute(); 134 | 135 | $this->assertGreaterThanOrEqual(2, count($allHosts), "No hosts where returned by the search"); 136 | $this->assertNotEquals(count($allHosts), count($filteredHosts), "Filter returned same hosts list"); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/Nagios/Livestatus/Client.php: -------------------------------------------------------------------------------- 1 | $value) { 34 | if (property_exists($this, $key)) { 35 | $this->{$key} = $value; 36 | } else { 37 | throw new InvalidArgumentException("The option '$key' is not recognised."); 38 | } 39 | } 40 | 41 | switch ($this->socketType) { 42 | case "unix": 43 | if (strlen($this->socketPath) == 0) { 44 | throw new InvalidArgumentException("The option socketPath must be supplied for socketType 'unix'."); 45 | } 46 | 47 | if (!file_exists($this->socketPath) || !is_readable($this->socketPath) || !is_writable($this->socketPath)) { 48 | throw new InvalidArgumentException("The supplied socketPath '{$this->socketPath}' is not accessible to this script."); 49 | } 50 | 51 | break; 52 | case "tcp": 53 | if (strlen($this->socketAddress) == 0) { 54 | throw new InvalidArgumentException("The option socketAddress must be supplied for socketType 'tcp'."); 55 | } 56 | 57 | if (strlen($this->socketPort) == 0) { 58 | throw new InvalidArgumentException("The option socketPort must be supplied for socketType 'tcp'."); 59 | } 60 | 61 | break; 62 | default: 63 | throw new InvalidArgumentException("Socket Type is invalid. Must be one of 'unix' or 'tcp'."); 64 | } 65 | 66 | $this->reset(); 67 | } 68 | 69 | public function get($table) 70 | { 71 | if (!is_string($table)) { 72 | throw new InvalidArgumentException("A string must be supplied."); 73 | } 74 | 75 | $this->table = $table; 76 | return $this; 77 | } 78 | 79 | public function column($column) 80 | { 81 | if (!is_string($column)) { 82 | throw new InvalidArgumentException("A string must be supplied."); 83 | } 84 | 85 | $this->columns[] = $column; 86 | return $this; 87 | } 88 | 89 | public function headers($boolean) 90 | { 91 | if (!is_bool($boolean)) { 92 | throw new InvalidArgumentException("A boolean must be supplied."); 93 | } 94 | 95 | if ($boolean === true) { 96 | $this->headers = "on"; 97 | } else { 98 | $this->headers = "off"; 99 | } 100 | 101 | return $this; 102 | } 103 | 104 | 105 | public function columns(array $columns) 106 | { 107 | if (!is_array($columns)) { 108 | throw new InvalidArgumentException("An array must be supplied."); 109 | } 110 | 111 | $this->columns = $columns; 112 | return $this; 113 | } 114 | 115 | public function filter($filter) 116 | { 117 | if (!is_string($filter)) { 118 | throw new InvalidArgumentException("A string must be supplied."); 119 | } 120 | 121 | $this->query .= "Filter: " . $filter . "\n"; 122 | return $this; 123 | } 124 | 125 | public function stat($stat) 126 | { 127 | return $this->stats($stat); 128 | } 129 | 130 | public function stats($stats) 131 | { 132 | if (!is_string($stats)) { 133 | throw new InvalidArgumentException("A string must be supplied."); 134 | } 135 | 136 | $this->query .= "Stats: " . $stats . "\n"; 137 | return $this; 138 | } 139 | 140 | public function statsAnd($statsAnd) 141 | { 142 | if (!is_int($statsAnd)) { 143 | throw new InvalidArgumentException("An integer must be supplied."); 144 | } 145 | 146 | $this->query .= "StatsAnd: " . $statsAnd . "\n"; 147 | return $this; 148 | } 149 | 150 | public function statsNegate() 151 | { 152 | $this->query .= "StatsNegate:\n"; 153 | return $this; 154 | } 155 | 156 | public function lor($orLines) 157 | { 158 | return $this->logicalOr($orLines); 159 | } 160 | 161 | public function logicalOr($orLines) 162 | { 163 | if (!is_int($orLines)) { 164 | throw new InvalidArgumentException("An integer must be supplied."); 165 | } 166 | 167 | $this->query .= "Or: " . $orLines . "\n"; 168 | return $this; 169 | } 170 | 171 | public function logicalAnd($andLines) 172 | { 173 | if (!is_int($andLines)) { 174 | throw new InvalidArgumentException("An integer must be supplied."); 175 | } 176 | 177 | $this->query .= "And: " . $andLines . "\n"; 178 | return $this; 179 | } 180 | 181 | public function negate() 182 | { 183 | $this->query .= "Negate:\n"; 184 | return $this; 185 | } 186 | 187 | public function parameter($parameter) 188 | { 189 | if (!is_string($parameter)) { 190 | throw new InvalidArgumentException("A string must be supplied."); 191 | } 192 | 193 | if (trim($parameter) === "") { 194 | return $this; 195 | } 196 | 197 | $this->query .= $this->checkEnding($parameter); 198 | return $this; 199 | } 200 | 201 | public function outputFormat($outputFormat) 202 | { 203 | if (!is_string($outputFormat)) { 204 | throw new InvalidArgumentException("A string must be supplied."); 205 | } 206 | 207 | $this->outputFormat = $outputFormat; 208 | return $this; 209 | } 210 | 211 | public function limit($limit) 212 | { 213 | if (!is_int($limit)) { 214 | throw new InvalidArgumentException("An integer must be supplied."); 215 | } 216 | 217 | $this->limit = "Limit: " . $limit . "\n"; 218 | return $this; 219 | } 220 | 221 | public function authUser($authUser) 222 | { 223 | if (!is_string($parameter)) { 224 | throw new InvalidArgumentException("A string must be supplied."); 225 | } 226 | 227 | $this->authUser = "AuthUser: " . $authUser . "\n"; 228 | return $this; 229 | } 230 | 231 | public function execute($query = null) 232 | { 233 | $this->openSocket(); 234 | 235 | $response = $this->runQuery($query); 236 | 237 | $this->closeSocket(); 238 | 239 | return $response; 240 | } 241 | 242 | public function executeAssoc() 243 | { 244 | $this->openSocket(); 245 | 246 | $response = $this->runQuery(); 247 | 248 | if (count($this->columns) > 0) { 249 | $headers = $this->columns; 250 | } else { 251 | $headers = array_shift($response); 252 | } 253 | 254 | $cols = count($headers); 255 | $rows = count($response); 256 | for ($i = 0; $i < $rows; $i++) { 257 | for ($j = 0; $j < $cols; $j++) { 258 | $response[$i][$headers[$j]] = $response[$i][$j]; 259 | } 260 | } 261 | 262 | $this->closeSocket(); 263 | 264 | return $response; 265 | } 266 | 267 | public function command(array $command) 268 | { 269 | $this->openSocket(); 270 | 271 | $fullcommand = sprintf("COMMAND [%lu] %s\n", time(), implode(';', $command)); 272 | socket_write($this->socket, $fullcommand); 273 | $this->closeSocket(); 274 | } 275 | 276 | public function reset() 277 | { 278 | $this->closeSocket(); 279 | } 280 | 281 | public function buildRequest($request = null) 282 | { 283 | // Check if request was supplied 284 | if (!is_null($request)) { 285 | $request = $this->checkEnding($request); 286 | } else { 287 | $request = "GET " . $this->table . "\n"; 288 | 289 | if ($this->columns) { 290 | $request .= "Columns: " . implode(" ", $this->columns) . "\n"; 291 | if ($this->headers) { 292 | $request .= "ColumnHeaders: " . $this->headers . "\n"; 293 | } 294 | } 295 | 296 | if (!is_null($this->query)) { 297 | $request .= $this->query; 298 | } 299 | 300 | if (!is_null($this->outputFormat)) { 301 | $request .= "OutputFormat: " . $this->outputFormat . "\n"; 302 | } 303 | 304 | if (!is_null($this->authUser)) { 305 | $request .= $this->authUser; 306 | } 307 | 308 | if (!is_null($this->limit)) { 309 | $request .= $this->limit; 310 | } 311 | } 312 | 313 | $request .= "ResponseHeader: fixed16\n"; 314 | $request .= "\n"; 315 | 316 | return $request; 317 | } 318 | 319 | protected function openSocket() 320 | { 321 | if (!is_null($this->socket)) { 322 | // Assume socket still good and continue 323 | return; 324 | } 325 | 326 | if ($this->socketType === "unix") { 327 | $this->socket = socket_create(AF_UNIX, SOCK_STREAM, 0); 328 | } elseif ($this->socketType === "tcp") { 329 | $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 330 | } 331 | 332 | if (!$this->socket) { 333 | $this->socket = null; 334 | throw new RuntimeException("Could not create socket."); 335 | } 336 | 337 | if ($this->socketType === "unix") { 338 | $result = socket_connect($this->socket, $this->socketPath); 339 | } elseif ($this->socketType === "tcp") { 340 | $result = socket_connect($this->socket, $this->socketAddress, $this->socketPort); 341 | } 342 | 343 | if (!$result) { 344 | $this->closeSocket(); 345 | throw new RuntimeException("Unable to connect to socket."); 346 | } 347 | 348 | if ($this->socketType === "tcp") { 349 | socket_set_option($this->socket, SOL_TCP, TCP_NODELAY, 1); 350 | } 351 | 352 | if ($this->socketTimeout) { 353 | socket_set_option($this->socket, SOCK_STREAM, SO_RCVTIMEO, $this->socketTimeout); 354 | socket_set_option($this->socket, SOCK_STREAM, SO_SNDTIMEO, $this->socketTimeout); 355 | } 356 | } 357 | 358 | protected function closeSocket() 359 | { 360 | if (is_resource($this->socket)) { 361 | socket_close($this->socket); 362 | } 363 | 364 | $this->socket = null; 365 | $this->query = null; 366 | $this->table = "hosts"; 367 | $this->headers = "off"; 368 | $this->columns = array(); 369 | $this->outputFormat = "json"; 370 | $this->authUser = null; 371 | $this->limit = null; 372 | } 373 | 374 | protected function readSocket($length) 375 | { 376 | $offset = 0; 377 | $socketData = ""; 378 | 379 | while ($offset < $length) { 380 | if (false === ($data = socket_read($this->socket, $length - $offset))) { 381 | throw new RuntimeException( 382 | "Problem reading from socket: " 383 | . socket_strerror(socket_last_error($this->socket)) 384 | ); 385 | } 386 | 387 | $dataLen = strlen($data); 388 | $offset += $dataLen; 389 | $socketData .= $data; 390 | 391 | if ($dataLen == 0) { 392 | break; 393 | } 394 | } 395 | 396 | return $socketData; 397 | } 398 | 399 | protected function checkEnding($string) 400 | { 401 | if ($string[strlen($string)-1] !== "\n") { 402 | $string .= "\n"; 403 | } 404 | 405 | return $string; 406 | } 407 | 408 | protected function runQuery($query = null) 409 | { 410 | $query = $this->buildRequest($query); 411 | 412 | // Send the query to MK Livestatus 413 | socket_write($this->socket, $query); 414 | 415 | // Read 16 bytes to get the status code and body size 416 | $header = $this->readSocket(16); 417 | 418 | $status = substr($header, 0, 3); 419 | $length = intval(trim(substr($header, 4, 11))); 420 | 421 | $response = $this->readSocket($length); 422 | 423 | // Check for errors. A 200 reponse means request was OK. 424 | // Any other response is a failure. 425 | if ($status != "200") { 426 | throw new RuntimeException("Error response from Nagios MK Livestatus: " . $response); 427 | } 428 | 429 | if ($this->outputFormat === "json") { 430 | $response = json_decode(utf8_encode($response)); 431 | } 432 | 433 | if (is_null($response)) { 434 | throw new RuntimeException("The response was invalid."); 435 | } 436 | 437 | return $response; 438 | } 439 | } 440 | --------------------------------------------------------------------------------