├── .gitignore ├── CHANGES ├── Dockerfile ├── LICENSE ├── MANIFEST ├── README.md ├── Vagrantfile ├── build_deb.sh ├── debian ├── changelog ├── compat ├── control ├── default ├── init ├── nagios-api.triggers ├── rules └── upstart ├── nagios-api ├── nagios-api-initd ├── nagios-cli ├── nagios ├── __init__.py └── core.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.vagrant 2 | /debian/nagios-api/ 3 | /debian/*.debhelper 4 | /debian/*.log 5 | *.pyc 6 | /dist/ 7 | /debian/files 8 | /debian/*.substvars 9 | 10 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 1.2.2: June 14th, 2013 2 | * Fix the exit function to actually exit [lollipopman] 3 | 4 | 1.2.1: March 21st, 2013 5 | * Fix SIGTERM handler to clean up pid [xb95] 6 | 7 | 1.2: March 21st, 2013 8 | + Add nagios-api-initd script for RedHat/CentOS [jonlives] 9 | + Add --pid-file option [lollipopman] 10 | + Include comment/downtime data in hosts output [ryanfrantz] 11 | 12 | 1.1: February 14th, 2013 13 | * Fix gzip handling in nagios-cli [rincebrain] 14 | + Add acknowledge-problem support to nagios-cli [rincebrain] 15 | 16 | 1.0: January 15th, 2013 17 | - Initial release [various contributors] 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | MAINTAINER Rogier Slag 3 | 4 | EXPOSE 8080 5 | # The following files can be mapped into the container for usages towards Nagios 6 | # However setting these as a volume causes Docker to create directories for them 7 | # VOLUME ["/opt/status.dat", "/opt/nagios.cmd", "/opt/nagios.log"] 8 | 9 | RUN apt-get update && \ 10 | apt-get install python-virtualenv libffi-dev python-dev python-pip python-setuptools openssl libssl-dev -y 11 | RUN cd /opt && \ 12 | virtualenv env && \ 13 | /opt/env/bin/pip install diesel && \ 14 | /opt/env/bin/pip install requests 15 | 16 | RUN mkdir /opt/nagios-api 17 | COPY . /opt/nagios-api 18 | 19 | CMD ["/opt/env/bin/python", "/opt/nagios-api/nagios-api", "-p", "8080", "-s", "/opt/status.dat", "-c", "/opt/nagios.cmd", "-l", "/opt/nagios.log", "-q"] 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 by Bump Technologies, Inc, and authors and 2 | contributors. 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 25 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 31 | THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | LICENSE 2 | CHANGES 3 | README 4 | nagios-api 5 | nagios-cli 6 | setup.py 7 | nagios/__init__.py 8 | nagios/core.py 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nagios API 2 | nagios-api - presents a REST-like JSON interface to Nagios. 3 | 4 | ## Description 5 | This program provides a simple REST-like interface to Nagios. Run this 6 | on your Nagios host and then sit back and enjoy a much easier, more 7 | straightforward way to accomplish things with Nagios. You can use the 8 | bundled nagios-cli, but you may find it easier to write your own system 9 | for interfacing with the API. 10 | 11 | ## Synopsis 12 | `nagios-api [OPTIONS]` 13 | 14 | ## Dependencies 15 | Dependencies include: 16 | 17 | - diesel 18 | - greenlet 19 | - python-openssl 20 | 21 | These should be available via pip/easy_install. 22 | 23 | ## Usage 24 | Usage is pretty easy: 25 | 26 | ``` 27 | nagios-api -p 8080 -c /var/lib/nagios3/rw/nagios.cmd\ 28 | -s /var/cache/nagios3/status.dat -l /var/log/nagios3/nagios.log 29 | ``` 30 | 31 | You must at least provide the status file options. If you don't provide 32 | the other options, then we will disable that functionality and error to 33 | clients who request it. 34 | 35 | ## Using the API 36 | The server speaks [JSON](http://www.json.org/). You can either GET data from it or POST data to 37 | it and take an action. It's pretty straightforward, here's an idea of 38 | what you can do from the command line: 39 | 40 | ``` 41 | curl http://localhost:8080/state 42 | ``` 43 | 44 | That calls the `state` method and returns the JSON result. 45 | 46 | ``` 47 | curl -d '{"host": "web01", "duration": 600}' -H 'Content-Type: application/json' http://localhost:8080/schedule_downtime 48 | ``` 49 | 50 | This POSTs the given JSON object to the `schedule_downtime` method. You 51 | will note that all objects returned follow a predictable format: 52 | 53 | ``` 54 | {"content": , "result": } 55 | ``` 56 | 57 | The `result` field is always `true` or `false`, allowing you to 58 | determine at a glance if the command succeeded. The `content` field may 59 | be any valid JavaScript object: an int, string, null, bool, hash, list, 60 | etc etc. What is returned depends on the method being called. 61 | 62 | ## Using `nagios-cli` 63 | Once your API server is up and running you can access it through the 64 | included nagios-cli script. The script now has some decent built-in help 65 | so you should be able to get all you need: 66 | 67 | ``` 68 | nagios-cli -h 69 | ``` 70 | 71 | The original raw JSON mode is still supported by passing the --raw 72 | option. 73 | 74 | ## Options 75 | Below are the options taken on the CLI. 76 | 77 | ``` 78 | -p, --port=PORT 79 | ``` 80 | 81 | Listen on port 'PORT' for HTTP requests. 82 | 83 | ``` 84 | -b, --bind=ADDR 85 | ``` 86 | 87 | Bind to ADDR for HTTP requests (defaults to all interfaces). 88 | 89 | ``` 90 | -c, --command-file=FILE 91 | ``` 92 | 93 | Use 'FILE' to write commands to Nagios. This is where external 94 | commands are sent. If your Nagios installation does not allow 95 | external commands, do not set this option. 96 | 97 | ``` 98 | -d, --config-directory=PATH 99 | ``` 100 | 101 | The directory in which Nagios will look for object files and import 102 | hosts into its internal database for monitoring. 103 | 104 | ``` 105 | -s, --status-file=FILE 106 | ``` 107 | 108 | Set 'FILE' to the status file where Nagios stores its status 109 | information. This is where we learn about the state of the world and 110 | is the only required parameter. 111 | 112 | ``` 113 | -l, --log-file=FILE 114 | ``` 115 | 116 | Point 'FILE' to the location of Nagios's log file if you want to 117 | allow people to subscribe to it. 118 | 119 | ``` 120 | -o, --allow-origin=ORIGIN 121 | ``` 122 | 123 | Modern web browsers implement the Cross-Origin Resource Sharing 124 | specification from W3C. This spec allows you to host your 125 | JavaScript/HTML on one host and have it access an endpoint on a 126 | different service. This requires setting a header on the endpoint, 127 | which this option allows you to do. 128 | 129 | You can simply set this header to ``` and not worry about it 130 | if you want to allow all access. For more information see the 131 | [CORS specification](http://www.w3.org/TR/cors/). 132 | 133 | ``` 134 | -q, --quiet 135 | ``` 136 | 137 | If present, we will only print warning/critical messages. Useful if 138 | you are running this in the background. 139 | 140 | ## API 141 | This program currently supports only a subset of the Nagios API. More 142 | is being added as it is needed. If you need something that isn't here, 143 | please consider submitting a patch! 144 | 145 | This section is organized into methods and sorted alphabetically. Each 146 | method is specified as a URL and may include an integer component on the 147 | path. Most data is passed as JSON objects in the body of a POST. 148 | 149 | ### `acknowledge_problem` 150 | This method allows you to acknowledge a given problem on a host or service. 151 | 152 | ``` 153 | { 154 | "host": "string", 155 | "service": "string", 156 | "comment": "string", 157 | "sticky": true, 158 | "notify": true, 159 | "persistent: true, 160 | "expire": 0, 161 | "author": "string" 162 | } 163 | ``` 164 | 165 | #### Fields 166 | `host` = `STRING [required]` 167 | 168 | Which host to act on. 169 | 170 | `service` = `STRING [optional]` 171 | 172 | If specified, act on this service. 173 | 174 | `comment` = `STRING [required]` 175 | 176 | This is required and should contain some sort of message that explains why 177 | this alert is being acknowledged. 178 | 179 | `sticky` = `BOOL [optional]` 180 | 181 | default TRUE. When true, this acknowledgement stays until the 182 | host enters an OK state. If false, the acknowledgement clears on ANY state 183 | change. 184 | 185 | `notify` = `BOOL [optional]` 186 | 187 | default TRUE. Whether or not to send a notification that this 188 | problem has been acknowledged. 189 | 190 | `persistent` = `BOOL [optional]` 191 | 192 | default FALSE. If this is enabled, the comment given will stay 193 | on the host or service. By default, when an acknowledgement expires, the 194 | comment associated with it is deleted. 195 | 196 | `expire` = `INTEGER [optional]` 197 | 198 | default 0. If set, it will (given icinga >= 1.6) expire the 199 | acknowledgement at the given timestamp. Seconds since the UNIX epoch. Defaults 200 | to 0 (off). 201 | 202 | `author` = `STRING [optional]` 203 | 204 | The name of the author. This is useful in UIs if you want 205 | to disambiguate who is doing what. 206 | 207 | ### `add_comment` 208 | For a given host and/or service, add a comment. This is free-form text that can 209 | include whatever you want and is visible in the Nagios UI and API output. 210 | 211 | ``` 212 | { 213 | "host": "string", 214 | "service": "string", 215 | "comment": "string", 216 | "persistent: true, 217 | "author": "string" 218 | } 219 | ``` 220 | 221 | #### Fields 222 | `host` = `STRING [required]` 223 | 224 | Which host to act on. 225 | 226 | `service` = `STRING [optional]` 227 | 228 | If specified, act on this service. 229 | 230 | `comment` = `STRING [required]` 231 | 232 | This is required and should contain the text of the comment you want to 233 | add to this host or service. 234 | 235 | `persistent` = `BOOL [optional]` 236 | 237 | Optional, default FALSE. If this is enabled, the comment given will stay 238 | on the host or service until deleted manually. By default, they only stay 239 | until Nagios is restarted. 240 | 241 | `author` = `STRING [optional]` 242 | 243 | The name of the author. This is useful in UIs if you want 244 | to disambiguate who is doing what. 245 | 246 | ### `cancel_downtime` 247 | Very simply, this immediately lifts a downtime that is currently in 248 | effect on a host or service. If you know the `downtime_id`, you can 249 | specify that as a URL argument like this: 250 | 251 | ``` 252 | curl -d "{}" http://localhost:8080/cancel_downtime/15 253 | ``` 254 | 255 | That would cancel the downtime with `downtime_id` of 15. Most of the 256 | time you will probably not have this information and so we allow you to 257 | cancel by host/service as well. 258 | 259 | ``` 260 | { 261 | "host": "string", 262 | "service": "string", 263 | "services_too": true 264 | } 265 | ``` 266 | 267 | #### Fields 268 | `host` = `STRING [required]` 269 | 270 | Which host to cancel downtime from. This must be specified if you 271 | are not using the `downtime_id` directly. 272 | 273 | `service` = `STRING [optional]` 274 | 275 | If specified, cancel any downtimes on this service. 276 | 277 | `services_too` = `BOOL [optional]` 278 | 279 | If true and you have not specified a `service` in 280 | specific, then we will cancel all downtimes on this host and all of 281 | the services it has. 282 | 283 | ### `disable_notifications` 284 | This disables alert notifications on a host or service. (As an operational 285 | note, you might want to schedule downtime instead. Disabling notifications 286 | has a habit of leaving things off and people forgetting about it.) 287 | 288 | ``` 289 | { 290 | "host": "string", 291 | "service": "string" 292 | } 293 | ``` 294 | 295 | #### Fields 296 | `host` = `STRING [required]` 297 | 298 | Which host to act on. 299 | 300 | `service` = `STRING [optional]` 301 | 302 | If specified, act on this service. 303 | 304 | ### `delete_comment` 305 | Deletes comments from a host or service. Can be used to delete all comments or 306 | just a particular comment. 307 | 308 | ``` 309 | { 310 | "host": "string", 311 | "service": "string", 312 | "comment_id": 1234 313 | } 314 | ``` 315 | 316 | #### Fields 317 | `host` = `STRING [required]` 318 | 319 | Which host to act on. 320 | 321 | `service` = `STRING [optional]` 322 | 323 | If specified, act on this service. 324 | 325 | `comment_id` = `INTEGER [required]` 326 | 327 | The ID of the comment you wish to delete. You may set this to `-1` to delete 328 | all comments on the given host or service. 329 | 330 | ### `enable_notifications` 331 | This enables alert notifications on a host or service. 332 | 333 | ``` 334 | { 335 | "host": "string", 336 | "service": "string" 337 | } 338 | ``` 339 | 340 | #### Fields 341 | `host` = `STRING [required]` 342 | 343 | Which host to act on. 344 | 345 | `service` = `STRING [optional]` 346 | 347 | If specified, act on this service. 348 | 349 | ### `log` 350 | Simply returns the most recent 1000 items in the Nagios event log. These 351 | are currently unparsed. There is a plan to parse this in the future and 352 | return event objects. 353 | 354 | ### `status` 355 | Simply returns a JSON that contains nagios status objects. 356 | 357 | ### `restart_nagios` 358 | Restarts the nagios service. 359 | 360 | ### `update_host` 361 | This method will create/update a nagios configuration file that contains devices. 362 | 363 | ``` 364 | { 365 | "file_name": "string", 366 | "text": "string" 367 | } 368 | ``` 369 | 370 | #### Fields 371 | `file_name` = `STRING [required]` 372 | 373 | File name for the configuration. 374 | 375 | `text` = `STRING [required]` 376 | 377 | Content of the configuration file. 378 | 379 | ### `objects` 380 | Returns a dict with the key being hostnames and the values being a list 381 | of services defined for that host. Use this method to get the contents 382 | of the world -- i.e., all hosts and services. 383 | 384 | ### `remove_acknowledgement` 385 | This method cancels an acknowledgement on a host or service. 386 | 387 | ``` 388 | { 389 | "host": "string", 390 | "service": "string" 391 | } 392 | ``` 393 | 394 | #### Fields 395 | `host` = `STRING [required]` 396 | 397 | Which host to act on. 398 | 399 | `service` = `STRING [optional]` 400 | 401 | If specified, act on this service. 402 | 403 | ### `schedule_check` 404 | This API lets you schedule a check for a host or service. This also allows 405 | you to force a check. 406 | 407 | ``` 408 | { 409 | "host": "string", 410 | "service": "string", 411 | "check_time": 1234, 412 | "forced: true, 413 | "output": "string" 414 | } 415 | ``` 416 | 417 | #### Fields 418 | `host` = `STRING [required]` 419 | 420 | The host to schedule a check for. Required. 421 | 422 | `service` = `STRING [optional]` 423 | 424 | If present, we'll schedule a check on this service at the given 425 | time. 426 | 427 | `all_services` = `BOOL [optional]` 428 | 429 | If present, we'll schedule a check on again all services at the given 430 | time. 431 | 432 | `check_time` = `INTEGER [optional]` 433 | 434 | Optional, defaults to now. You can specify what time you want the check 435 | to be run at. 436 | 437 | `forced` = `BOOL [optional]` 438 | 439 | Optional, defaults to FALSE. When true, then you force Nagios to run the 440 | check at the given time. By default, Nagios will only run the check if it 441 | meets the standard eligibility criteria. 442 | 443 | `output` = `STRING [required]` 444 | 445 | The plugin output to be displayed in the UI and stored. This is a 446 | single line of text, normally returned by checkers. 447 | 448 | ### `schedule_downtime` 449 | This general purpose method is used for creating fixed length downtimes. 450 | This method can be used on hosts and services. You are allowed to 451 | specify the author and comment to go with the downtime, too. The JSON 452 | parameters are: 453 | 454 | ``` 455 | { 456 | "host": "string", 457 | "duration": 1234, 458 | "service": "string", 459 | "services_too": true, 460 | "author": "string", 461 | "comment": "string" 462 | } 463 | ``` 464 | 465 | #### Fields 466 | `host` = `STRING [required]` 467 | 468 | Which host to schedule a downtime for. This must be specified. 469 | 470 | `duration` = `INTEGER [required]` 471 | 472 | How many seconds this downtime will last for. They begin immediately 473 | and continue for `duration` seconds before ending. 474 | 475 | `service` = `STRING [optional]` 476 | 477 | If specified, we will schedule a downtime for this service 478 | on the above host. If not specified, then the downtime will be 479 | scheduled for the host itself. 480 | 481 | `services_too` = `BOOL [optional]` 482 | 483 | If true and you have not specified a `service` in 484 | specific, then we will schedule a downtime for the host and all of 485 | the services on that host. Potentially many downtimes are scheduled. 486 | 487 | `author` = `STRING [optional]` 488 | 489 | The name of the author. This is useful in UIs if you want 490 | to disambiguate who is doing what. 491 | 492 | `comment` = `STRING [optional]` 493 | 494 | As above, useful in the UI. 495 | 496 | The result of this method is a text string that indicates whether or 497 | not the downtimes have been scheduled or if a different error occurred. 498 | We do not have the ability to get the `downtime_id` that is generated, 499 | unfortunately, as that would require waiting for Nagios to regenerate 500 | the status file. 501 | 502 | ### `schedule_hostgroup_downtime` 503 | This method is used for creating fixed length downtimes on all the hosts 504 | belonging to a hostgroup. You are allowed to specify the author and comment 505 | to go with the downtime, too. The JSON parameters are: 506 | 507 | ``` 508 | { 509 | "hostgroup": "string", 510 | "duration": 1234, 511 | "services_too": true, 512 | "author": "string", 513 | "comment": "string" 514 | } 515 | ``` 516 | 517 | #### Fields 518 | `hostgroup` = `STRING [required]` 519 | 520 | Which hostgroup to schedule a downtime for. This must be specified. 521 | 522 | `duration` = `INTEGER [required]` 523 | 524 | How many seconds this downtime will last for. They begin immediately 525 | and continue for `duration` seconds before ending. 526 | 527 | `services_too` = `BOOL [optional]` 528 | 529 | If true, then we will schedule a downtime for all the hosts in 530 | the hostgroup and all of the services on those hosts. 531 | Potentially many downtimes are scheduled. 532 | 533 | `author` = `STRING [optional]` 534 | 535 | The name of the author. This is useful in UIs if you want 536 | to disambiguate who is doing what. 537 | 538 | `comment` = `STRING [optional]` 539 | 540 | As above, useful in the UI. 541 | 542 | The result of this method is a text string that indicates whether or 543 | not the downtimes have been scheduled or if a different error occurred. 544 | We do not have the ability to get the `downtime_id` that is generated, 545 | unfortunately, as that would require waiting for Nagios to regenerate 546 | the status file. 547 | 548 | ### `state` 549 | This method takes no parameters. It returns a large JSON object 550 | containing all of the active state from Nagios. Included are all hosts, 551 | services, downtimes, comments, and other things that may be in the 552 | global state object. 553 | 554 | ### `submit_result` 555 | If you are using passive service checks or you just want to submit a 556 | result for a check, you can use this method to submit your result to 557 | Nagios. 558 | 559 | ``` 560 | { 561 | "host": "string", 562 | "service": "string", 563 | "status": 1234, 564 | "output": "string" 565 | } 566 | ``` 567 | 568 | #### Fields 569 | `host` = `STRING [required]` 570 | 571 | The host to submit a result for. This is required. 572 | 573 | `service` = `STRING [optional]` 574 | 575 | If specified, we will submit a result for this service on 576 | the above host. If not specified, then the result will be submitted 577 | for the host itself. 578 | 579 | `status` = `INTEGER [required]` 580 | 581 | The status code to set this host/service check to. If you are 582 | updating a host's status: 0 = OK, 1 = DOWN, 2 = UNREACHABLE. For 583 | service checks, 0 = OK, 1 = WARNING, 2 = CRITICAL, 3 = UNKNOWN. 584 | 585 | `output` = `STRING [required]` 586 | 587 | The plugin output to be displayed in the UI and stored. This is a 588 | single line of text, normally returned by checkers. 589 | 590 | The response indicates if we successfully wrote the command to the log. 591 | 592 | ## Docker 593 | A Docker container is available for convenience. It needs to be run on 594 | the same server as the nagios installation. 595 | 596 | First determine the location of the `status.dat`, `nagios.log`, and 597 | `nagios.cmd` files. Map these files into the Docker container. The 598 | container can be started using the following command: 599 | 600 | ``` 601 | docker run -v /var/lib/nagios3/rw/nagios.cmd:/opt/nagios.cmd \ 602 | -v /var/cache/nagios3/status.dat:/opt/status.dat \ 603 | -v /var/log/nagios3/nagios.log:/opt/nagios.log \ 604 | -p 2337:8080 inventid/nagios-python-api 605 | ``` 606 | 607 | In the above case, the API will be exposed on port 2337. 608 | 609 | ## Author 610 | Written by Mark Smith while under the employ of Bump 611 | Technologies, Inc. 612 | 613 | ## Copying 614 | See the `LICENSE` file for licensing information. 615 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "ubuntu/trusty64" 6 | 7 | config.vm.provider "virtualbox" do |vb| 8 | vb.cpus = 2 9 | vb.memory = 2048 10 | end 11 | 12 | config.vm.provision "shell", inline: <<-SHELL 13 | sudo apt-get update 14 | sudo apt-get install -y devscripts dh-virtualenv debhelper python-dev libssl-dev vim-nox git curl wget 15 | SHELL 16 | end 17 | -------------------------------------------------------------------------------- /build_deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rvf build nagios-api*egg* 3 | find . -name '*.pyc' -exec rm -vf '{}' \; 4 | dpkg-buildpackage -us -uc 5 | mkdir -p dist 6 | mv -v ../nagios-api_* ./dist/ 7 | rm -rf *egg* 8 | rm -rf build 9 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | nagios-api (1.2.2-1) stable; urgency=medium 2 | 3 | * Initial release. 4 | 5 | -- numekm Wed, 29 Jul 2015 19:20:38 +0000 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: nagios-api 2 | Section: python 3 | Priority: extra 4 | Maintainer: Sebastien Bariteau 5 | Build-Depends: debhelper (>= 9), python, dh-virtualenv (>= 0.6), python-dev, libssl-dev 6 | Standards-Version: 0.3.9 7 | 8 | Package: nagios-api 9 | Architecture: any 10 | Pre-Depends: dpkg (>= 1.16.1), python2.7, ${misc:Pre-Depends} 11 | Depends: ${python:Depends}, ${misc:Depends} 12 | Description: Nagios API -------------------------------------------------------------------------------- /debian/default: -------------------------------------------------------------------------------- 1 | # Nagios-api default configuraiton file 2 | 3 | # Port used by nagios-api 4 | PORT=6315 5 | 6 | # Nagios command file 7 | NAGIOS_COMMAND=/var/lib/nagios3/rw/nagios.cmd 8 | 9 | # Nagios status file (status.dat) 10 | NAGIOS_STATUS=/var/cache/nagios3/status.dat 11 | 12 | # Nagios log file 13 | NAGIOS_LOG=/var/log/nagios3/nagios.log 14 | 15 | # Nagios-api log file 16 | LOGFILE=/var/log/nagios3/nagios-api.log 17 | 18 | # Additional options 19 | #OPTION="-o '*'" -------------------------------------------------------------------------------- /debian/init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: nagios-api 5 | # Required-Start: 6 | # Required-Stop: 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 9 | # Short-Description: Nagios API 10 | ### END INIT INFO 11 | 12 | # Nagios-API start-stop-daemon 13 | 14 | # LSB functions 15 | . /lib/lsb/init-functions 16 | 17 | NAME=nagios-api 18 | PIDFILE=/var/run/$NAME.pid 19 | DAEMON=/usr/share/python/nagios-api/bin/nagios-api 20 | 21 | # Sourcing default 22 | . /etc/default/$NAME 23 | 24 | start_api() { 25 | start-stop-daemon \ 26 | --start \ 27 | --quiet \ 28 | --pidfile $PIDFILE \ 29 | --background \ 30 | --startas /bin/bash -- \ 31 | -c "exec $DAEMON -p $PORT -s $NAGIOS_STATUS -c $NAGIOS_COMMAND -l $NAGIOS_LOG -f $PIDFILE $OPTION >> $LOGFILE 2>&1" 32 | } 33 | 34 | case "$1" in 35 | start) 36 | echo -n "Starting daemon: "$NAME 37 | start_api 38 | echo 39 | ;; 40 | stop) 41 | echo -n "Stopping daemon: "$NAME 42 | start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE 43 | echo 44 | ;; 45 | restart) 46 | echo -n "Restarting daemon: "$NAME 47 | start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile $PIDFILE 48 | start_api 49 | echo 50 | ;; 51 | 52 | status) 53 | status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? 54 | ;; 55 | 56 | *) 57 | echo "Usage: "$1" {start|stop|restart}" 58 | exit 1 59 | esac 60 | 61 | exit 0 62 | -------------------------------------------------------------------------------- /debian/nagios-api.triggers: -------------------------------------------------------------------------------- 1 | # Register interest in Python interpreter changes (Python 2 for now); and 2 | # don't make the Python package dependent on the virtualenv package 3 | # processing (noawait) 4 | interest-noawait /usr/bin/python2.7 5 | 6 | # Also provide a symbolic trigger for all dh-virtualenv packages 7 | interest dh-virtualenv-interpreter-update 8 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --with python-virtualenv --no-test 5 | -------------------------------------------------------------------------------- /debian/upstart: -------------------------------------------------------------------------------- 1 | # nagios-api 2 | description "Nagios API" 3 | 4 | start on runlevel [2345] 5 | stop on runlevel [!2345] 6 | 7 | respawn 8 | 9 | script 10 | . /etc/default/nagios-api 11 | exec /usr/share/python/nagios-api/bin/nagios-api -p $PORT -s $NAGIOS_STATUS -c $NAGIOS_COMMAND -l $NAGIOS_LOG $OPTION >> $LOGFILE 2>&1 12 | end script 13 | -------------------------------------------------------------------------------- /nagios-api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''nagios-api - a REST-like, JSON API for Nagios 4 | 5 | This provides a simple REST interface to Nagios3. See the README for 6 | more information about this software. 7 | 8 | Originally from: 9 | 10 | https://github.com/xb95/nagios-api 11 | 12 | Copyright (c) 2011-2012 by Bump Technologies, Inc and other authors and 13 | contributors. See the LICENSE file for full licensing information. 14 | 15 | ''' 16 | 17 | 18 | import datetime 19 | import os 20 | import re 21 | import sys 22 | import time 23 | import types 24 | import atexit 25 | import urllib 26 | import pwd 27 | import grp 28 | from gzip import GzipFile 29 | from StringIO import StringIO 30 | from optparse import OptionParser 31 | from diesel import Application, Loop, Service, sleep 32 | from diesel.logmod import log, levels 33 | from diesel.util.lock import synchronized 34 | from diesel.protocols import http 35 | from json import dumps 36 | from nagios import Comment, Downtime, Nagios 37 | from werkzeug.exceptions import BadRequest 38 | from signal import signal, SIGTERM 39 | 40 | CMDFILE = None 41 | CMD_ENABLED = False 42 | LOG_ENABLED = False 43 | URL_REGEX = re.compile(r'^/(\w+)(?:/([\w\d\.\+\-\s]+)?)?$') 44 | NAGIOS = None 45 | NLOG = [] 46 | NLOGLINES = 0 47 | ALLOW_ORIGIN = None 48 | PID_FILE = "/var/run/nagios-api.pid" 49 | CFG_DIRECTORY = "/usr/local/nagios/etc/objects" 50 | 51 | 52 | def _send_json(req, success, content): 53 | '''Internal JSON sender. 54 | 55 | ''' 56 | global ALLOW_ORIGIN 57 | out = dumps({ 'success': success, 'content': content }, ensure_ascii=False) 58 | headers = { 59 | 'Content-Length': len(out), 60 | 'Content-Type': 'application/json' 61 | } 62 | if ALLOW_ORIGIN is not None: 63 | headers['Access-Control-Allow-Origin'] = ALLOW_ORIGIN 64 | if 'gzip' in req.headers.get('Accept-Encoding', ''): 65 | headers['Content-Encoding'] = 'gzip' 66 | strout = StringIO() 67 | f = GzipFile(fileobj=strout, mode='w') 68 | f.write(out) 69 | f.close() 70 | out = strout.getvalue() 71 | return http.Response(out, 200, headers) 72 | 73 | 74 | def json_error(req, msg): 75 | '''Return an error message to the caller. 76 | 77 | ''' 78 | return _send_json(req, False, msg) 79 | 80 | 81 | def json_response(req, msg): 82 | '''Return an error message to the caller. 83 | 84 | ''' 85 | return _send_json(req, True, msg) 86 | 87 | 88 | def http_problems(req, objid, reqobj): 89 | '''Get a host->service mapping for "All Problems". This is 90 | the method to use to get the data needed to display a simmilar 91 | view to the nagios "Problems" view. 92 | 93 | ''' 94 | global NAGIOS 95 | if objid is not None: 96 | return json_error(req, 'Unexpected object ID.') 97 | 98 | hosts = {} 99 | for host, host_data in NAGIOS.for_json().items(): 100 | for service,service_data in host_data['services'].items(): 101 | if (int(service_data['current_state']) == 0): 102 | del host_data['services'][service] 103 | if host_data['services']: 104 | hosts[host] = host_data 105 | return json_response(req, hosts) 106 | 107 | 108 | def http_state(req, objid, reqobj): 109 | '''Get a host->service mapping and return the basic state. This is 110 | the method to use to have status scripts or web interfaces that 111 | contain basic overview information. 112 | 113 | This should return everything you need to show the status of the 114 | world. 115 | 116 | ''' 117 | global NAGIOS 118 | if objid is not None: 119 | return json_error(req, 'Unexpected object ID.') 120 | return json_response(req, NAGIOS.for_json()) 121 | 122 | 123 | def http_objects(req, objid, reqobj): 124 | '''Get a host->service mapping. Does not return any data about what 125 | these objects are. This is to be used to efficiently get a list of 126 | everything that exists in the world. 127 | 128 | ''' 129 | global NAGIOS 130 | if objid is not None: 131 | return json_error(req, 'Unexpected object ID.') 132 | ret = {} 133 | for host in NAGIOS.hosts: 134 | ret[host] = [] 135 | if host in NAGIOS.services: 136 | for svc in NAGIOS.services[host]: 137 | ret[host].append(svc) 138 | return json_response(req, ret) 139 | 140 | 141 | def http_status(req, objid, reqobj): 142 | global NAGIOS 143 | if objid is not None: 144 | return json_error(req, 'Unexpected object ID.') 145 | ret = {} 146 | ret['info'] = NAGIOS.info.for_json() 147 | ret['program'] = NAGIOS.program.for_json() 148 | return json_response(req, ret) 149 | 150 | 151 | def http_host(req, objid, reqobj): 152 | '''Get a view of a single host. 153 | Shows all of the variables for the host object. 154 | ''' 155 | global NAGIOS 156 | if not objid: 157 | return json_error(req, 'No hostname provided.') 158 | elif objid not in NAGIOS.hosts.keys(): 159 | return json_error(req, 'Unknown hostname.') 160 | ret = {} 161 | ret['comment'] = [] 162 | ret['downtime'] = [] 163 | host_keys = [ x for x in dir(NAGIOS.hosts[objid]) if not x.startswith('__') ] 164 | for host_key in host_keys: 165 | value = getattr(NAGIOS.hosts[objid], host_key) 166 | if type(value) == types.StringType: 167 | ret[host_key] = value 168 | # return comment and downtime data too, if either exists 169 | elif type(value) == dict: 170 | for k, v in value.iteritems(): 171 | if isinstance( v, Comment ): 172 | ret['comment'].append( v.for_json() ) 173 | elif isinstance( v, Downtime ): 174 | ret['downtime'].append( v.for_json() ) 175 | if objid in NAGIOS.services: 176 | ret['services'] = [] 177 | for svc in NAGIOS.services[objid]: 178 | ret['services'].append(svc) 179 | return json_response(req, ret) 180 | 181 | 182 | def _mkdir_recursive(self, path): 183 | sub_path = os.path.dirname(path) 184 | if not os.path.exists(sub_path): 185 | _mkdir_recursive(self, sub_path) 186 | if not os.path.exists(path): 187 | os.mkdir(path) 188 | 189 | def http_restart_nagios(req, objid, reqobj): 190 | if not send_nagios_command('RESTART_PROGRAM'): 191 | return json_error(req, 'Restart failed.') 192 | else: 193 | return json_response(req, 'Restart successful') 194 | 195 | def http_send_raw_command(req, objid, reqobj): 196 | command = reqobj['command'] 197 | if not command: 198 | return json_error(req, "Missing command to send to nagios") 199 | 200 | if not send_nagios_command(command): 201 | return json_error(req, "Nagios command failed") 202 | 203 | return json_response(req, "Nagios command succeeded") 204 | 205 | def http_update_host(req, objid, reqobj): 206 | 207 | global CFG_DIRECTORY 208 | 209 | _mkdir_recursive(None, CFG_DIRECTORY) 210 | file_name = reqobj['file_name'] 211 | text = reqobj['text'] 212 | 213 | if not file_name: 214 | return json_error(req, 'No file path specified.') 215 | 216 | if not text: 217 | return json_error(req, 'No text specified.') 218 | 219 | file_path = CFG_DIRECTORY + "/" + file_name 220 | 221 | if os.path.isfile(file_path): 222 | os.remove(file_path) 223 | 224 | try: 225 | f = file(file_path, 'w') 226 | f.write(text) 227 | f.close() 228 | 229 | sys.stdout.write("file saved %s " % file_name) 230 | 231 | uid = pwd.getpwnam("nagios").pw_uid 232 | gid = grp.getgrnam("nagios").gr_gid 233 | 234 | if uid and gid: 235 | os.chown(file_path, uid, gid) 236 | 237 | ret = {} 238 | ret['message'] = "Update file "+file_path 239 | return json_response(req, ret) 240 | except Exception as e: 241 | sys.stderr.write("%s\n" % e) 242 | sys.stderr.write("Unable to write file: %s\n" % file_name) 243 | 244 | ret = {} 245 | ret['message'] = "Unable to write file: "+file_path 246 | return json_response(req, ret) 247 | 248 | 249 | def http_service(req, objid, reqobj): 250 | '''Get a view of a single service on one host. 251 | Shows all of the variables for the host object. 252 | ''' 253 | global NAGIOS 254 | if not objid: 255 | return json_error(req, 'No hostname provided.') 256 | elif objid not in NAGIOS.services.keys(): 257 | return json_error(req, 'Unknown hostname.') 258 | ret = {} 259 | for service in NAGIOS.services[objid]: 260 | ret[service] = {} 261 | service_keys = [ x for x in dir(NAGIOS.services[objid][service]) if not x.startswith('__') ] 262 | for service_key in service_keys: 263 | value = getattr(NAGIOS.services[objid][service], service_key) 264 | if type(value) == types.StringType: 265 | ret[service][service_key] = value 266 | return json_response(req, ret) 267 | 268 | 269 | def http_log(req, objid, reqobj): 270 | '''Return the recent Nagios log entries. This is useful if you just 271 | want to see what has happened recently. See the subscribe method if 272 | you want to be notified when new log lines are added. 273 | 274 | ''' 275 | global NLOG, LOG_ENABLED 276 | if objid is not None: 277 | return json_error(req, 'Unexpected object ID.') 278 | if not LOG_ENABLED: 279 | return json_error(req, 'Log file parsing is not enabled on nagios-api.') 280 | return json_response(req, NLOG) 281 | 282 | 283 | def http_disable_notifications(req, objid, reqobj): 284 | '''Given a service or host, disable notifications for it. 285 | 286 | If you specify the host parameter and also set 'services_too' to 287 | true, we will act on the host and all of the services on it. 288 | 289 | ''' 290 | global NAGIOS, CMD_ENABLED 291 | if not CMD_ENABLED: 292 | return json_error(req, 'External commands not enabled on nagios-api.') 293 | if objid is not None: 294 | return json_error(req, 'Unexpected object ID.') 295 | 296 | host = reqobj.get('host') 297 | service = reqobj.get('service') 298 | services_too = reqobj.get('services_too', False) 299 | 300 | obj = NAGIOS.host_or_service(host, service) 301 | if obj is None: 302 | return json_error(req, 'Host or service not found.') 303 | 304 | now = int(time.time()) 305 | if obj.service is not None: 306 | if not send_nagios_command('DISABLE_SVC_NOTIFICATIONS', host, service): 307 | return json_error(req, 'Failed sending command to Nagios.') 308 | else: 309 | if not send_nagios_command('DISABLE_HOST_NOTIFICATIONS', host): 310 | return json_error(req, 'Failed sending command to Nagios.') 311 | if services_too: 312 | if not send_nagios_command('DISABLE_HOST_SVC_NOTIFICATIONS', host): 313 | return json_error(req, 'Failed sending command to Nagios.') 314 | return json_response(req, 'disabled') 315 | 316 | 317 | def http_enable_notifications(req, objid, reqobj): 318 | '''Given a service or host, enable notifications for it. 319 | 320 | If you specify the host parameter and also set 'services_too' to 321 | true, we will act on the host and all of the services on it. 322 | 323 | ''' 324 | global NAGIOS, CMD_ENABLED 325 | if not CMD_ENABLED: 326 | return json_error(req, 'External commands not enabled on nagios-api.') 327 | if objid is not None: 328 | return json_error(req, 'Unexpected object ID.') 329 | 330 | host = reqobj.get('host') 331 | service = reqobj.get('service') 332 | services_too = reqobj.get('services_too', False) 333 | 334 | obj = NAGIOS.host_or_service(host, service) 335 | if obj is None: 336 | return json_error(req, 'Host or service not found.') 337 | 338 | now = int(time.time()) 339 | if obj.service is not None: 340 | if not send_nagios_command('ENABLE_SVC_NOTIFICATIONS', host, service): 341 | return json_error(req, 'Failed sending command to Nagios.') 342 | else: 343 | if not send_nagios_command('ENABLE_HOST_NOTIFICATIONS', host): 344 | return json_error(req, 'Failed sending command to Nagios.') 345 | if services_too: 346 | if not send_nagios_command('ENABLE_HOST_SVC_NOTIFICATIONS', host): 347 | return json_error(req, 'Failed sending command to Nagios.') 348 | return json_response(req, 'enabled') 349 | 350 | def http_disable_checks(req, objid, reqobj): 351 | '''Given a service or host, disable active checks for it. 352 | 353 | If you specify the host parameter and also set 'services_too' to 354 | true, we will act on the host and all of the services on it. 355 | 356 | ''' 357 | global NAGIOS, CMD_ENABLED 358 | if not CMD_ENABLED: 359 | return json_error(req, 'External commands not enabled on nagios-api.') 360 | if objid is not None: 361 | return json_error(req, 'Unexpected object ID.') 362 | 363 | host = reqobj.get('host') 364 | service = reqobj.get('service') 365 | services_too = reqobj.get('services_too', False) 366 | 367 | obj = NAGIOS.host_or_service(host, service) 368 | if obj is None: 369 | return json_error(req, 'Host or service not found.') 370 | 371 | now = int(time.time()) 372 | if obj.service is not None: 373 | if not send_nagios_command('DISABLE_SVC_CHECK', host, service): 374 | return json_error(req, 'Failed sending command to Nagios.') 375 | else: 376 | if not send_nagios_command('DISABLE_HOST_CHECK', host): 377 | return json_error(req, 'Failed sending command to Nagios.') 378 | if services_too: 379 | if not send_nagios_command('DISABLE_HOST_SVC_CHECKS', host): 380 | return json_error(req, 'Failed sending command to Nagios.') 381 | return json_response(req, 'disabled') 382 | 383 | 384 | def http_enable_checks(req, objid, reqobj): 385 | '''Given a service or host, enable active check for it. 386 | 387 | If you specify the host parameter and also set 'services_too' to 388 | true, we will act on the host and all of the services on it. 389 | 390 | ''' 391 | global NAGIOS, CMD_ENABLED 392 | if not CMD_ENABLED: 393 | return json_error(req, 'External commands not enabled on nagios-api.') 394 | if objid is not None: 395 | return json_error(req, 'Unexpected object ID.') 396 | 397 | host = reqobj.get('host') 398 | service = reqobj.get('service') 399 | services_too = reqobj.get('services_too', False) 400 | 401 | obj = NAGIOS.host_or_service(host, service) 402 | if obj is None: 403 | return json_error(req, 'Host or service not found.') 404 | 405 | now = int(time.time()) 406 | if obj.service is not None: 407 | if not send_nagios_command('ENABLE_SVC_CHECK', host, service): 408 | return json_error(req, 'Failed sending command to Nagios.') 409 | else: 410 | if not send_nagios_command('ENABLE_HOST_CHECK', host): 411 | return json_error(req, 'Failed sending command to Nagios.') 412 | if services_too: 413 | if not send_nagios_command('ENABLE_HOST_SVC_CHECKS', host): 414 | return json_error(req, 'Failed sending command to Nagios.') 415 | return json_response(req, 'enabled') 416 | 417 | 418 | def http_schedule_downtime(req, objid, reqobj): 419 | '''Given a service or host, schedule downtime for it. The main mode 420 | for this API is to schedule a hard downtime that starts now and ends 421 | after so many seconds. 422 | 423 | If you specify the host parameter and also set 'services_too' to 424 | true, we will schedule downtime for the host and the services on it. 425 | 426 | ''' 427 | global NAGIOS, CMD_ENABLED 428 | if not CMD_ENABLED: 429 | return json_error(req, 'External commands not enabled on nagios-api.') 430 | if objid is not None: 431 | return json_error(req, 'Unexpected object ID.') 432 | 433 | now = int(time.time()) 434 | 435 | host = reqobj.get('host') 436 | service = reqobj.get('service') 437 | services_too = reqobj.get('services_too', False) 438 | author = reqobj.get('author', 'nagios-api') 439 | comment = reqobj.get('comment', 'schedule downtime') 440 | start_time = reqobj.get('start_time', now) 441 | fixed = reqobj.get('fixed', 1) 442 | try: 443 | dur = int(reqobj.get('duration', 0)) 444 | except ValueError: 445 | dur = 0 446 | end_time = reqobj.get('end_time', start_time + dur) 447 | 448 | obj = NAGIOS.host_or_service(host, service) 449 | if obj is None: 450 | return json_error(req, 'Host or service not found.') 451 | if fixed == 1: 452 | # duration is unused by Nagios for fixed downtime 453 | dur = 0 454 | else: # flexible downtime 455 | # The Nagios C code defines duration as an unsigned long 456 | if dur < 1 or dur > 4294967295: 457 | return json_error(req, 'Flexible dowtime duration must be between 1 and 4,294,967,295 seconds') 458 | 459 | if obj.service is not None: 460 | if not send_nagios_command('SCHEDULE_SVC_DOWNTIME', host, service, start_time, 461 | end_time, fixed, 0, dur, author, comment): 462 | return json_error(req, 'Failed sending command to Nagios.') 463 | else: 464 | if not send_nagios_command('SCHEDULE_HOST_DOWNTIME', host, start_time, end_time, 465 | fixed, 0, dur, author, comment): 466 | return json_error(req, 'Failed sending command to Nagios.') 467 | if services_too: 468 | if not send_nagios_command('SCHEDULE_HOST_SVC_DOWNTIME', host, start_time, end_time, 469 | fixed, 0, dur, author, comment): 470 | return json_error(req, 'Failed sending command to Nagios.') 471 | return json_response(req, 'scheduled') 472 | 473 | 474 | def http_cancel_downtime(req, objid, reqobj): 475 | '''Given a downtime_id in objid, cancel that downtime. Alternately, 476 | this method will expect a host and/or service parameter and use that 477 | to cancel any matching downtimes. 478 | 479 | ''' 480 | global NAGIOS, CMD_ENABLED 481 | if not CMD_ENABLED: 482 | return json_error(req, 'External commands not enabled on nagios-api.') 483 | 484 | def cancel_downtime(obj): 485 | if obj.service is not None: 486 | return send_nagios_command('DEL_SVC_DOWNTIME', dt.downtime_id) 487 | else: 488 | return send_nagios_command('DEL_HOST_DOWNTIME', dt.downtime_id) 489 | 490 | dts = [] 491 | if objid is not None: 492 | if objid not in NAGIOS.downtimes: 493 | return json_error(req, 'Downtime ID does not seem valid.') 494 | dts += [NAGIOS.downtimes[objid]] 495 | else: 496 | host = reqobj.get('host') 497 | service = reqobj.get('service') 498 | services_too = reqobj.get('services_too', False) 499 | obj = NAGIOS.host_or_service(host, service) 500 | if obj is None: 501 | return json_error(req, 'Failed to get host or service for downtime.') 502 | if obj.service is None and services_too: 503 | for svc in obj.services.itervalues(): 504 | dts += svc.downtimes.values() 505 | dts += obj.downtimes.values() 506 | 507 | res = None 508 | for dt in dts: 509 | if res is None: 510 | res = True # So we know if we found any. 511 | res = cancel_downtime(dt) and res 512 | 513 | if res is not None: 514 | if res: 515 | return json_response(req, 'cancelled') 516 | else: 517 | return json_error(req, 'One or more cancels failed. Some may have succeeded.') 518 | else: 519 | return json_response(req, 'none found') 520 | 521 | 522 | 523 | def http_schedule_hostgroup_downtime(req, objid, reqobj): 524 | '''Given a hostgroup, schedule downtime for all the hosts in the hostgroup. 525 | The main mode for this API is to schedule a hard downtime that starts now and ends 526 | after so many seconds. 527 | 528 | If you specify the hostgroup parameter and also set 'services_too' to 529 | true, we will schedule downtime for the all the hosts and the services on it. 530 | 531 | ''' 532 | global NAGIOS, CMD_ENABLED 533 | if not CMD_ENABLED: 534 | return json_error(req, 'External commands not enabled on nagios-api.') 535 | if objid is not None: 536 | return json_error(req, 'Unexpected object ID.') 537 | 538 | now = int(time.time()) 539 | 540 | hostgroup = reqobj.get('hostgroup') 541 | services_too = reqobj.get('services_too', False) 542 | author = reqobj.get('author', 'nagios-api') 543 | comment = reqobj.get('comment', 'schedule downtime') 544 | start_time = reqobj.get('start_time', now) 545 | fixed = reqobj.get('fixed', 1) 546 | try: 547 | dur = int(reqobj.get('duration', 0)) 548 | except ValueError: 549 | dur = 0 550 | end_time = reqobj.get('end_time', start_time + dur) 551 | 552 | if hostgroup is None: 553 | return json_error(req, 'Hostgroup not specified.') 554 | if fixed == 1: 555 | # duration is unused by Nagios for fixed downtime 556 | dur = 0 557 | else: # flexible downtime 558 | # The Nagios C code defines duration as an unsigned long 559 | if dur < 1 or dur > 4294967295: 560 | return json_error(req, 'Flexible dowtime duration must be between 1 and 4,294,967,295 seconds') 561 | 562 | if not send_nagios_command('SCHEDULE_HOSTGROUP_HOST_DOWNTIME', hostgroup, start_time, 563 | end_time, fixed, 0, dur, author, comment): 564 | return json_error(req, 'Failed sending command to Nagios.') 565 | if services_too: 566 | if not send_nagios_command('SCHEDULE_HOSTGROUP_SVC_DOWNTIME', hostgroup, start_time, end_time, 567 | fixed, 0, dur, author, comment): 568 | return json_error(req, 'Failed sending command to Nagios.') 569 | return json_response(req, 'scheduled') 570 | 571 | 572 | 573 | def http_submit_result(req, objid, reqobj): 574 | '''Submits a check result for a host or service. This is mostly used 575 | for passive services where Nagios is not responsible for doing the 576 | checks itself. 577 | 578 | As with other APIs, you are required to supply a host parameter and 579 | you may supply a service. The output parameter is an opaque string 580 | that gets given to Nagios. Use the status parameter to specify an 581 | integer return code. 582 | 583 | Host status codes: 0 = UP, 1 = DOWN, 2 = UNREACHABLE. 584 | Service status codes: 0 = OK, 1 = WARNING, 2 = CRITICAL, 3 = UNKNOWN. 585 | 586 | ''' 587 | global NAGIOS, CMD_ENABLED 588 | if not CMD_ENABLED: 589 | return json_error(req, 'External commands not enabled on nagios-api.') 590 | 591 | host = reqobj.get('host') 592 | service = reqobj.get('service') 593 | 594 | obj = NAGIOS.host_or_service(host, service) 595 | if obj is None: 596 | return json_error(req, 'Failed to find host or service to update.') 597 | 598 | if 'status' not in reqobj: 599 | return json_error(req, 'Required parameter "status" not found.') 600 | if 'output' not in reqobj: 601 | return json_error(req, 'Required parameter "output" not found.') 602 | 603 | try: 604 | status = int(reqobj['status']) 605 | except ValueError: 606 | return json_error(req, 'Invalid status provided.') 607 | 608 | if obj.service is not None: 609 | if not send_nagios_command('PROCESS_SERVICE_CHECK_RESULT', host, 610 | service, status, reqobj['output']): 611 | return json_error(req, 'Failed sending command to Nagios.') 612 | else: 613 | if not send_nagios_command('PROCESS_HOST_CHECK_RESULT', host, status, 614 | reqobj['output']): 615 | return json_error(req, 'Failed sending command to Nagios.') 616 | return json_response(req, 'submitted') 617 | 618 | 619 | def http_acknowledge_problem(req, objid, reqobj): 620 | '''Submits an acknowledgement of a host or service problem. This is used 621 | when you have an open issue, that you want to acknowledge to prevent it 622 | from sending further alerts. 623 | 624 | You are required to send at minimum a host parameter, and you can also 625 | supply an optional service parameter. If no service is supplied, the 626 | host will be acknowledged, and if the service parameter is supplied 627 | only the specific service will be acknowledged. 628 | 629 | Additionally, you are required to supply a comment parameter as well. This 630 | is a text string that will be used to identify the reason for the 631 | acknowledgement. 632 | 633 | The following options are also available, but optional. 634 | 635 | sticky (0/1): If set to 1, the acknowledgement will remain until the service 636 | or host enters an OK state. If set to 0, it will clear the acknowledgement on 637 | the first state change, ex. CRITICAL->WARNING. Defaults to 1 (on). 638 | 639 | notify (0/1): If set to 0, no notification will be sent to the configure 640 | contacts for the host or service. Defaults to 1 (on). 641 | 642 | persistent (0/1): If set to 1, the comment will remain even after the service 643 | or host problem has been resolved. Defaults to 0 (off). 644 | 645 | expire (0/timestamp): If set, it will (given icinga >= 1.6) expire the 646 | acknowledgement at the given timestamp. Seconds since the UNIX epoch. Defaults 647 | to 0 (off). 648 | 649 | author (string): This is the name displayed as the creator of the 650 | acknowledgement. This will default to 'nagios-api'. 651 | 652 | ''' 653 | global NAGIOS, CMD_ENABLED 654 | if not CMD_ENABLED: 655 | return json_error(req, 'External commands not enabled on nagios-api.') 656 | 657 | host = reqobj.get('host') 658 | service = reqobj.get('service') 659 | comment = reqobj.get('comment') 660 | author = reqobj.get('author', 'nagios-api') 661 | sticky = reqobj.get('sticky', 1) 662 | notify = reqobj.get('notify', 1) 663 | persistent = reqobj.get('persistent', 0) 664 | expire = reqobj.get('expire', 0) 665 | 666 | obj = NAGIOS.host_or_service(host, service) 667 | if obj is None: 668 | return json_error(req, 'Failed to find host or service to update.') 669 | 670 | if not comment: 671 | return json_error(req, 'Required parameter "comment" not found.') 672 | 673 | try: 674 | sticky = int(sticky) 675 | notify = int(notify) 676 | persistent = int(persistent) 677 | expire = int(expire) 678 | except ValueError: 679 | return json_error(req, 'Invalid value provided for one or more of sticky, notify or persistent') 680 | 681 | if obj.service is not None: 682 | if expire > 0: 683 | if not send_nagios_command('ACKNOWLEDGE_SVC_PROBLEM_EXPIRE', host, service, 684 | sticky, notify, persistent, expire, author, comment): 685 | return json_error(req, 'Failed sending command to Nagios.') 686 | else: 687 | if not send_nagios_command('ACKNOWLEDGE_SVC_PROBLEM', host, service, 688 | sticky, notify, persistent, author, comment): 689 | return json_error(req, 'Failed sending command to Nagios.') 690 | else: 691 | if expire > 0: 692 | if not send_nagios_command('ACKNOWLEDGE_HOST_PROBLEM_EXPIRE', host, sticky, 693 | notify, persistent, expire, author, comment): 694 | return json_error(req, 'Failed sending command to Nagios.') 695 | else: 696 | if not send_nagios_command('ACKNOWLEDGE_HOST_PROBLEM', host, sticky, 697 | notify, persistent, author, comment): 698 | return json_error(req, 'Failed sending command to Nagios.') 699 | return json_response(req, 'submitted') 700 | 701 | 702 | def http_remove_acknowledgement(req, objid, reqobj): 703 | '''Removes an acknowledgement from a host or service. 704 | 705 | You are required to send at minimum a host parameter, and 706 | you can also supply an optional service parameter. If no 707 | service is supplied, the host will be acknowledged, and if the 708 | service parameter is supplied only the specific service will be 709 | acknowledged. 710 | 711 | ''' 712 | global NAGIOS, CMD_ENABLED 713 | if not CMD_ENABLED: 714 | return json_error(req, 'External commands not enabled on nagios-api.') 715 | 716 | host = reqobj.get('host') 717 | service = reqobj.get('service') 718 | 719 | obj = NAGIOS.host_or_service(host, service) 720 | if obj is None: 721 | return json_error(req, 'Failed to find host or service to update.') 722 | 723 | if obj.service is not None: 724 | if not send_nagios_command('REMOVE_SVC_ACKNOWLEDGEMENT', host, service): 725 | return json_error(req, 'Failed sending command to Nagios.') 726 | else: 727 | if not send_nagios_command('REMOVE_HOST_ACKNOWLEDGEMENT', host): 728 | return json_error(req, 'Failed sending command to Nagios.') 729 | return json_response(req, 'submitted') 730 | 731 | 732 | def http_add_comment(req, objid, reqobj): 733 | '''Adds a comment to a host or service. 734 | 735 | You are required to send at minimum a host and comment parameter, 736 | and you can also supply an optional service parameter. If the 737 | service parameter is supplied, the comment will be added to the 738 | specific service and if its omitted, the comment will be added to 739 | the host 740 | 741 | The following options are also available, but optional. 742 | 743 | persistent (0/1): If set to 1, the comment will remain until 744 | manually deleted, if set to 0, it will automatically be purged at 745 | the next restart of the Nagios process. Defaults to 0 (off) 746 | 747 | author (string): This is the name displayed as the creator of the 748 | comment. This will default to 'nagios-api'. 749 | 750 | ''' 751 | global NAGIOS, CMD_ENABLED 752 | if not CMD_ENABLED: 753 | return json_error(req, 'External commands not enabled on nagios-api.') 754 | 755 | host = reqobj.get('host') 756 | service = reqobj.get('service') 757 | persistent = reqobj.get('persistent', 0) 758 | comment = reqobj.get('comment') 759 | author = reqobj.get('author', 'nagios-api') 760 | 761 | obj = NAGIOS.host_or_service(host, service) 762 | if obj is None: 763 | return json_error(req, 'Failed to find host or service to update.') 764 | 765 | if not comment: 766 | return json_error(req, 'Required parameter "comment" not found.') 767 | 768 | try: 769 | persistent = int(persistent) 770 | except ValueError: 771 | return json_error(req, 'Invalid value provided for persistent') 772 | 773 | if obj.service is not None: 774 | if not send_nagios_command('ADD_SVC_COMMENT', host, service, 775 | persistent, author, comment): 776 | return json_error(req, 'Failed sending command to Nagios.') 777 | else: 778 | if not send_nagios_command('ADD_HOST_COMMENT', host, 779 | persistent, author, comment): 780 | return json_error(req, 'Failed sending command to Nagios.') 781 | return json_response(req, 'submitted') 782 | 783 | 784 | def http_delete_comment(req, objid, reqobj): 785 | '''Deletes one or all comments for a host or service. 786 | 787 | You are required to supply atleast the host parameter along with 788 | comment_id, detailing the comment to delete. You can also supply the 789 | service parameter to delete comments from a specific service. 790 | 791 | If comment_id is set to -1, all comments for the host or service 792 | will be deleted. 793 | 794 | ''' 795 | global NAGIOS, CMD_ENABLED 796 | if not CMD_ENABLED: 797 | return json_error(req, 'External commands not enabled on nagios-api.') 798 | 799 | host = reqobj.get('host') 800 | service = reqobj.get('service') 801 | comment_id = reqobj.get('comment_id') 802 | 803 | obj = NAGIOS.host_or_service(host, service) 804 | if obj is None: 805 | return json_error(req, 'Failed to find host or service to update.') 806 | 807 | if not comment_id: 808 | return json_error(req, 'Required parameter "comment" not found.') 809 | 810 | try: 811 | comment_id = int(comment_id) 812 | except ValueError: 813 | return json_error(req, 'Invalid value provided for comment_id') 814 | 815 | if obj.service is not None: 816 | if comment_id == -1: 817 | if not send_nagios_command('DEL_ALL_SVC_COMMENTS', host, service): 818 | return json_error(req, 'Failed sending command to Nagios.') 819 | else: 820 | if not send_nagios_command('DEL_SVC_COMMENT', comment_id): 821 | return json_error(req, 'Failed sending command to Nagios.') 822 | else: 823 | if comment_id == -1: 824 | if not send_nagios_command('DEL_ALL_HOST_COMMENTS', host): 825 | return json_error(req, 'Failed sending command to Nagios.') 826 | else: 827 | if not send_nagios_command('DEL_HOST_COMMENT', comment_id): 828 | return json_error(req, 'Failed sending command to Nagios.') 829 | return json_response(req, 'submitted') 830 | 831 | 832 | def http_schedule_check(req, objid, reqobj): 833 | '''Schedules a check for a host or service. 834 | 835 | Requires the host parameter to be sent, and also accepts a service 836 | parameter. If you wish to schedule checks for all services on a host, 837 | you can use the all_services parameter. 838 | 839 | Scheduling a non-forced check, does not mean the check will occur. 840 | If active checks have been disabled either on a program-wide or 841 | service level, or the services are already scheduled to be checked 842 | sooner than your requested time, the check will not be performed, 843 | unless forced mode is enabled (see below). 844 | 845 | The following optional parameters are also available: 846 | 847 | check_time (int): The time at which to schedule the check, in time_t 848 | (UNIX timestamp) format. Defaults to current time. 849 | 850 | forced (0/1): If set to 1, the scheduled check will be marked as 851 | forced, meaning it will be performed, even if there has been other 852 | checks run on the host or service in the mean time. Defaults to 0 853 | (off). 854 | 855 | all_services (0/1): If set to 1, all services for the specified 856 | host will be checked. Enabling this option, will cause the service 857 | parameter to be ignored, if supplied. Defaults to 0 (off). 858 | 859 | ''' 860 | from time import time 861 | global NAGIOS, CMD_ENABLED 862 | if not CMD_ENABLED: 863 | return json_error(req, 'External commands not enabled on nagios-api.') 864 | 865 | host = reqobj.get('host') 866 | service = reqobj.get('service') 867 | check_time = reqobj.get('check_time', time()) 868 | forced = reqobj.get('forced', 0) 869 | all_services = reqobj.get('all_services', 0) 870 | 871 | obj = NAGIOS.host_or_service(host, service) 872 | if obj is None: 873 | return json_error(req, 'Failed to find host or service to update.') 874 | 875 | try: 876 | check_time = int(check_time) 877 | forced = int(forced) 878 | all_services = int(all_services) 879 | except ValueError: 880 | return json_error(req, 'Invalid value provided for check_time, all_services or forced') 881 | 882 | if obj.service is not None: 883 | if forced: 884 | if not send_nagios_command('SCHEDULE_FORCED_SVC_CHECK', host, service, check_time): 885 | return json_error(req, 'Failed sending command to Nagios.') 886 | else: 887 | if not send_nagios_command('SCHEDULE_SVC_CHECK', host, service, check_time): 888 | return json_error(req, 'Failed sending command to Nagios.') 889 | else: 890 | if forced: 891 | if not send_nagios_command('SCHEDULE_FORCED_HOST_CHECK', host, check_time): 892 | return json_error(req, 'Failed sending command to Nagios.') 893 | else: 894 | if not send_nagios_command('SCHEDULE_HOST_CHECK', host, check_time): 895 | return json_error(req, 'Failed sending command to Nagios.') 896 | 897 | if all_services: 898 | if forced: 899 | if not send_nagios_command('SCHEDULE_FORCED_HOST_SVC_CHECKS', host, check_time): 900 | return json_error(req, 'Failed sending command to Nagios.') 901 | else: 902 | if not send_nagios_command('SCHEDULE_HOST_SVC_CHECKS', host, check_time): 903 | return json_error(req, 'Failed sending command to Nagios.') 904 | 905 | return json_response(req, 'submitted') 906 | 907 | 908 | def http_handler(req): 909 | '''Handle an incoming HTTP request. 910 | 911 | ''' 912 | # All requests should follow this very simple format. 913 | global URL_REGEX 914 | url = req.script_root + req.path 915 | url = urllib.unquote(url).decode('utf8') 916 | res = URL_REGEX.match(url) 917 | if res is None: 918 | return json_error(req, 'Invalid request URI') 919 | verb, objid = res.group(1), res.group(2) 920 | try: 921 | objid = int(objid) 922 | except ValueError: 923 | pass 924 | except TypeError: 925 | objid = None 926 | 927 | # If it's a POST, try to extract a JSON object from the body. 928 | reqobj = None 929 | if req.method == 'POST': 930 | try: 931 | reqobj = req.json 932 | except BadRequest: 933 | return json_error(req, 'Invalid JSON body') 934 | 935 | if reqobj is None: 936 | return json_error(req, 'No JSON detected, did you set ' 937 | 'the Content-Type to application/json?') 938 | 939 | # Dispatch table goes here 940 | dispatch = { 941 | 'GET': { 942 | 'log': http_log, 943 | 'state': http_state, 944 | 'objects': http_objects, 945 | 'host': http_host, 946 | 'service': http_service, 947 | 'problems': http_problems, 948 | 'status': http_status, 949 | 'service': http_service, 950 | 'restart_nagios': http_restart_nagios 951 | }, 952 | 'POST': { 953 | 'cancel_downtime': http_cancel_downtime, 954 | 'schedule_downtime': http_schedule_downtime, 955 | 'schedule_hostgroup_downtime': http_schedule_hostgroup_downtime, 956 | 'disable_notifications': http_disable_notifications, 957 | 'enable_notifications': http_enable_notifications, 958 | 'disable_checks': http_disable_checks, 959 | 'enable_checks': http_enable_checks, 960 | 'submit_result': http_submit_result, 961 | 'acknowledge_problem': http_acknowledge_problem, 962 | 'remove_acknowledgement': http_remove_acknowledgement, 963 | 'add_comment': http_add_comment, 964 | 'delete_comment': http_delete_comment, 965 | 'schedule_check': http_schedule_check, 966 | 'update_host': http_update_host, 967 | 'raw_command': http_send_raw_command 968 | } 969 | } 970 | 971 | if req.method not in dispatch: 972 | return json_error(req, 'Method %s not supported' % req.method) 973 | if verb not in dispatch[req.method]: 974 | return json_error(req, 'Verb %s (method %s) not supported' % (verb, req.method)) 975 | return dispatch[req.method][verb](req, objid, reqobj) 976 | 977 | 978 | def send_nagios_command(*args): 979 | '''Send a simple command to our local Nagios server. 980 | 981 | ''' 982 | global CMDFILE, CMD_ENABLED 983 | if not CMD_ENABLED: 984 | log.error('Command is disabled') 985 | return False # May not be enabled. 986 | if len(args) < 1: 987 | log.error('Not enough parameters to send command') 988 | return False 989 | arg = '[%d] ' % int(time.time()) + ';'.join(unicode(j) for j in args) 990 | arg = arg.encode('utf-8') 991 | with synchronized(): 992 | log.info('Sending command: %s' % arg) 993 | try: 994 | fd = os.open(CMDFILE, os.O_WRONLY) 995 | os.write(fd, arg + '\n') 996 | os.close(fd) 997 | except Exception as e: 998 | log.error(e) 999 | return False 1000 | return True 1001 | return True 1002 | 1003 | 1004 | def read_status(statusfile): 1005 | '''Monitor the Nagios status file and update our global data store 1006 | with the data as it changes. 1007 | 1008 | ''' 1009 | global NAGIOS 1010 | mtime = None 1011 | while True: 1012 | if not os.path.isfile(statusfile): 1013 | NAGIOS = Nagios(None) 1014 | sleep(5) 1015 | continue 1016 | 1017 | stat = os.stat(statusfile) 1018 | if mtime is None or stat.st_mtime > mtime: 1019 | try: 1020 | NAGIOS = Nagios(statusfile) 1021 | except ValueError,e: 1022 | print "You appear to have handed me a malformed status file - possibly the state retention file. Please check your arguments and try again." 1023 | sys.exit(-1) 1024 | mtime = stat.st_mtime 1025 | sleep(1) 1026 | 1027 | 1028 | def read_log(logfile): 1029 | '''This function reads the Nagios log file and parses events. This 1030 | allows us to provide a pubsub style interface so people can get 1031 | real-time updates from the Nagios system. 1032 | 1033 | ''' 1034 | global NLOG, NLOGLINES 1035 | f = open(logfile, 'r') 1036 | cts = 0 1037 | while True: 1038 | loc = f.tell() 1039 | line = f.readline() 1040 | if not line: 1041 | sleep(1) 1042 | f.seek(loc) 1043 | cts += 1 # Handle log rollovers by watching for no activity. 1044 | if cts >= 60: 1045 | f = open(logfile, 'r') 1046 | cts = 0 1047 | else: 1048 | NLOGLINES += 1 1049 | NLOG.append(line.strip()) 1050 | NLOG = NLOG[-1000:] # Keep the most recent 1k lines. 1051 | f.close() # Useless? 1052 | 1053 | def write_pid(): 1054 | '''Write pid file 1055 | 1056 | ''' 1057 | global PID_FILE 1058 | if os.path.isfile(PID_FILE): 1059 | sys.stderr.write("%s already exists, exiting\n" % PID_FILE) 1060 | sys.exit(1) 1061 | else: 1062 | try: 1063 | file(PID_FILE, 'w').write(str(os.getpid())) 1064 | except Exception as e: 1065 | sys.stderr.write("%s\n" % e) 1066 | sys.stderr.write("Unable to write pid file: %s\n" % PID_FILE) 1067 | sys.exit(1) 1068 | 1069 | def main(argv): 1070 | '''A simple REST API for Nagios3. 1071 | 1072 | ''' 1073 | global CMDFILE, CMD_ENABLED, LOG_ENABLED, ALLOW_ORIGIN, PID_FILE, CFG_DIRECTORY 1074 | app = Application() 1075 | 1076 | parser = OptionParser(description='Give Nagios a REST API.') 1077 | parser.add_option('-o', '--allow-origin', dest='alloworigin', metavar='ORIGIN', 1078 | help='Access-Control-Allow-Origin header contents') 1079 | parser.add_option('-s', '--status-file', dest='statusfile', metavar='FILE', 1080 | default='/var/cache/nagios3/status.dat', help='The file that contains ' 1081 | 'the Nagios status.') 1082 | parser.add_option('-c', '--command-file', dest='commandfile', metavar='FILE', 1083 | default='/var/lib/nagios3/rw/nagios.cmd', help='The file to write ' 1084 | 'Nagios commands to.') 1085 | parser.add_option('-d', '--config-directory', dest='cfgdirectory', metavar='PATH', 1086 | default='/usr/local/nagios/etc/objects', help='Storage location for update_host endpoint files') 1087 | parser.add_option('-l', '--log-file', dest='logfile', metavar='FILE', 1088 | default='/var/log/nagios3/nagios.log', help='The file Nagios writes ' 1089 | 'log events to.') 1090 | parser.add_option('-b', '--bind', dest='bind_addr', metavar='ADDR', 1091 | default='', help='The address to listen for requests on.') 1092 | parser.add_option('-p', '--port', dest='port', metavar='PORT', type='int', 1093 | default=6315, help='The port to listen for requests on.') 1094 | parser.add_option('-f', '--pid-file', dest='pid_file', metavar='PID_FILE', 1095 | default=PID_FILE, help='File to write pid to') 1096 | parser.add_option('-q', '--quiet', dest='quiet', action='store_true', 1097 | help='Quiet mode') 1098 | (options, args) = parser.parse_args(args=argv[1:]) 1099 | 1100 | if not os.path.isfile(options.statusfile): 1101 | parser.error('Status file not found: %s' % options.statusfile) 1102 | if options.port < 0 or options.port > 65535: 1103 | parser.error('Port must be in the range 1..65535.') 1104 | 1105 | if options.quiet: 1106 | log.min_level = levels.WARNING 1107 | 1108 | if os.path.exists(options.commandfile): 1109 | CMD_ENABLED = True 1110 | CMDFILE = options.commandfile 1111 | if options.alloworigin: 1112 | ALLOW_ORIGIN = options.alloworigin 1113 | 1114 | CFG_DIRECTORY = options.cfgdirectory 1115 | PID_FILE = options.pid_file 1116 | write_pid() 1117 | 1118 | log.info('Listening on port %d, starting to rock and roll!' % options.port) 1119 | app.add_service(Service(http.HttpServer(http_handler), options.port, 1120 | options.bind_addr)) 1121 | app.add_loop(Loop(read_status, options.statusfile), keep_alive=True) 1122 | if os.path.isfile(options.logfile): 1123 | LOG_ENABLED = True 1124 | app.add_loop(Loop(read_log, options.logfile), keep_alive=True) 1125 | app.run() 1126 | return 1 1127 | 1128 | 1129 | def _exitfunc(*args): 1130 | global PID_FILE 1131 | if PID_FILE is not None and os.path.isfile(PID_FILE): 1132 | print "Cleaning up PID." 1133 | os.unlink(PID_FILE) 1134 | print "Exiting." 1135 | sys.exit(0) 1136 | 1137 | 1138 | if __name__ == '__main__': 1139 | pid = str(os.getpid()) 1140 | 1141 | atexit.register(_exitfunc) 1142 | signal(SIGTERM, _exitfunc) 1143 | 1144 | sys.exit(main(sys.argv)) 1145 | -------------------------------------------------------------------------------- /nagios-api-initd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # /etc/rc.d/init.d/nagios-api 4 | # 5 | # Starts the Nagios API 6 | # (RedHat/CentOS only!) 7 | # 8 | # chkconfig: 2345 90 10 9 | # description: Nagios API Daemon 10 | 11 | # processname: nagios-api 12 | 13 | source /etc/rc.d/init.d/functions 14 | 15 | PROG="nagios-api" 16 | DESC="Nagios API Daemon" 17 | RETVAL=0 18 | LOGFILE="/var/log/nagios/nagios-api.log" 19 | 20 | NAG_API_BIN=/usr/bin/nagios-api 21 | NAG_API_PORT=6315 22 | NAG_API_PID=/var/run/$PROG.pid 23 | 24 | NAGIOS_STATUS_FILE=/var/log/nagios/status.dat 25 | NAGIOS_COMMAND_FILE=/var/spool/nagios/cmd/nagios.cmd 26 | NAGIOS_LOG_FILE=/var/log/nagios/nagios.log 27 | 28 | start() { 29 | echo -n "Starting $DESC ($PROG): " 30 | daemon $NAG_API_BIN -p $NAG_API_PORT -s $NAGIOS_STATUS_FILE -c $NAGIOS_COMMAND_FILE -l $NAGIOS_LOG_FILE > $LOGFILE 2>&1 & 31 | 32 | echo 33 | RETVAL=$? 34 | return $RETVAL 35 | } 36 | 37 | stop() { 38 | echo -n "Shutting down $DESC ($PROG): " 39 | killproc $PROG 40 | 41 | echo 42 | RETVAL=$? 43 | return $RETVAL 44 | } 45 | 46 | case "$1" in 47 | start) 48 | start 49 | ;; 50 | stop) 51 | stop 52 | ;; 53 | restart) 54 | stop 55 | start 56 | RETVAL=$? 57 | ;; 58 | *) 59 | echo $"Usage: $0 {start|stop|restart}" 60 | RETVAL=1 61 | esac 62 | 63 | exit $RETVAL 64 | -------------------------------------------------------------------------------- /nagios-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | '''usage: %prog [command-options] 4 | 5 | This script uses the JSON Nagios API to issue commands to Nagios. The 6 | goal is to make your life easier. If it doesn't, please return it 7 | unharmed and let me know what went wrong and I'll make it better. 8 | 9 | Usage: 10 | 11 | nagios-cli --host=nagios --port=6315 [command-options] 12 | 13 | This is the prefix. I recommend you stash this in an alias so you don't 14 | have to type in the host/port every time. From now on in this page, I 15 | will assume that 'nagios-cli' will do the right thing and leave out the 16 | host/port arguments. 17 | 18 | This tool operates like git, svn, and other tools you may now -- you 19 | invoke it, tell it what command to run, and then the rest of the 20 | arguments vary depending on the command. Some tools don't take any 21 | options, some take a few required arguments and then some. 22 | 23 | Status Viewing 24 | 25 | nagios-cli hosts 26 | 27 | nagios-cli services 28 | 29 | Notification Management 30 | 31 | nagios-cli disable-notifications [service] [--recursive|r] 32 | 33 | nagios-cli enable-notifications [service] [--recursive|r] 34 | 35 | Active Checks Management 36 | 37 | nagios-cli disable-checks [service] [--recursive|r] 38 | 39 | nagios-cli enable-checks [service] [--recursive|r] 40 | 41 | Downtime Management 42 | 43 | nagios-cli schedule-downtime [service] 44 | [--recursive|-r] [--author|-a=TEXT] [--comment|-c=TEXT] 45 | 46 | nagios-cli cancel-downtime [service] [--recursive|-r] 47 | 48 | Problem Management 49 | 50 | nagios-cli acknowledge-problem [service] <--comment|-c=TEXT> 51 | [--sticky|-s=TRUE|false] [--notify|-n=TRUE|false] 52 | [--persistent|-p=true|FALSE] [--author|-a=TEXT] 53 | 54 | Copyright 2011-2013 by Bump Technologies, Inc and other authors and 55 | contributors. See the LICENSE file for full licensing information. 56 | 57 | ''' 58 | 59 | import re 60 | import requests 61 | import sys 62 | from json import loads, dumps 63 | from optparse import OptionParser, OptionGroup 64 | 65 | 66 | URL = None # To the Nagios API "http://foo:6315" 67 | NAGIOS = {} # Host => [ Service, Service, Service, ... ] 68 | 69 | 70 | def time_to_seconds(inp): 71 | '''Possibly convert a time written like "2h" or "50m" into seconds. 72 | 73 | ''' 74 | match = re.match(r'^(\d+)([wdhms])?$', inp) 75 | if match is None: 76 | return None 77 | val, denom = match.groups() 78 | if denom is None: 79 | return int(val) 80 | multiplier = {'w': 604800, 'd': 86400, 'h': 3600, 'm': 60, 's': 1}[denom] 81 | return int(val) * multiplier 82 | 83 | 84 | def consume_host_service(args): 85 | '''Given a list of arguments, attempt to consume a host and service 86 | off of the front and return a list containing host=X service=X args 87 | that you can send to the API. 88 | 89 | ''' 90 | global NAGIOS 91 | if args is None or len(args) <= 0: 92 | return None 93 | if args[0] not in NAGIOS: 94 | return None 95 | host = args[0] 96 | selargs = ['host=%s' % host] 97 | del args[0] 98 | 99 | if len(args) > 0 and args[0].decode('utf-8') in NAGIOS[host]: 100 | selargs += ['service=%s' % args[0].decode('utf-8')] 101 | del args[0] 102 | return selargs 103 | 104 | 105 | def api(args): 106 | '''Send a call out to the API. Returns the response object (dict) or 107 | an integer exit code on failure. 108 | 109 | ''' 110 | global URL, NAGIOS 111 | 112 | # The rest of the data is now in args, build the URL 113 | verb = args[0] 114 | objid = args[1] if len(args) >= 2 and args[1].isdigit() else '' 115 | url = '%s/%s/%s' % (URL, verb, objid) 116 | 117 | # Now build the reqobj 118 | obj = {} 119 | for kv in args[2 if objid else 1:]: 120 | if not '=' in kv: 121 | return critical('Parameter "%s" does not conform to expected key=value format' % kv) 122 | key, value = kv.split('=', 1) 123 | obj[key] = value 124 | 125 | # Set the method to POST if we recognize the verb or if there is a payload 126 | method = 'POST' if len(obj) > 0 else 'GET' 127 | if verb in ('cancel_downtime'): 128 | method = 'POST' 129 | payload = dumps(obj) if method == 'POST' else None 130 | 131 | try: 132 | if payload is None: 133 | if AUTH : 134 | res = requests.get(url, auth=(USER,PASSWORD)) 135 | else: 136 | res = requests.get(url) 137 | else: 138 | if AUTH : 139 | res = requests.post(url, data=payload, auth=(USER,PASSWORD), 140 | headers={'Content-Type': 'application/json'}) 141 | else: 142 | res = requests.post(url, data=payload, 143 | headers={'Content-Type': 'application/json'}) 144 | except (requests.ConnectionError, requests.Timeout): 145 | return critical('Failed connecting to nagios-api server') 146 | if res is None: 147 | return critical('Failed requesting resource') 148 | if res.status_code == 401: 149 | return critical('User and or password provided are incorrect') 150 | 151 | # Probably a JSON response, get it 152 | try: 153 | if (res.headers['content-encoding'] == "gzip"): 154 | resobj = loads(res.content) 155 | else: 156 | resobj = loads(res.text) 157 | except ValueError: 158 | return critical('Failed parsing server response') 159 | except TypeError: 160 | return critical('Failed parsing JSON in server response') 161 | return resobj 162 | 163 | 164 | def do_schedule_downtime(cmd, args, opts): 165 | '''Create a scheduled downtime for a host or service. Usage: 166 | 167 | %prog schedule-downtime [service] [opts] 168 | 169 | host must be a hostname that Nagios knows about. If specified, 170 | service is a service that exists on that host. This combination of 171 | host and optional service indicates what you want the downtime to be 172 | scheduled on. 173 | 174 | The duration must be of the format like "2h". You can use w, d, h, 175 | m, or s as units. Seconds are assumed if you don't specify the units. 176 | 177 | Available options: 178 | 179 | --recursive Schedule downtime for all services on this host. 180 | You must not specify a specific service. 181 | 182 | --author=NAME Specify an author to record this downtime for. 183 | 184 | --comment=TEXT Leave descriptive text about the downtime. 185 | 186 | Example: 187 | 188 | %prog schedule-downtime web01 2h 189 | # schedule two hours of downtime for web01 190 | 191 | %prog schedule-downtime web01 "PING Check" 1w 192 | # schedule one week of downtime for PING Check on web01 193 | 194 | %prog schedule-downtime web03 1d --recursive 195 | # schedule one day of downtime for web03 and ALL services on it 196 | 197 | NOTE: This command schedules a fixed downtime that starts 198 | immediately and lasts for the specified duration. 199 | 200 | ''' 201 | p = OptionParser(usage=trim(do_schedule_downtime.__doc__)) 202 | p.disable_interspersed_args() 203 | p.add_option('-a', '--author', dest='author', metavar='NAME', 204 | help='Author to blame for this downtime') 205 | p.add_option('-c', '--comment', dest='comment', metavar='TEXT', 206 | help='Explanatory comment to leave on the downtime') 207 | p.add_option('-r', '--recursive', dest='recursive', action='store_true', 208 | help='Schedule for all services on the given host') 209 | p.set_defaults(recursive=False, author=None, comment=None) 210 | 211 | selargs = consume_host_service(args) 212 | if selargs is None: 213 | p.error('Failed to locate host/service to schedule downtime for') 214 | if len(args) <= 0: 215 | p.error('Must specify a duration in a format like "2h"') 216 | secs = time_to_seconds(args[0]) 217 | if secs is None: 218 | p.error('Invalid duration, must be in a format like "2h"') 219 | selargs += ['duration=%d' % secs] 220 | 221 | (options, args) = p.parse_args(opts) 222 | if options.recursive: 223 | selargs += ['services_too=true'] 224 | if options.author is not None: 225 | selargs += ['author=%s' % options.author] 226 | if options.comment is not None: 227 | selargs += ['comment=%s' % options.comment] 228 | 229 | res = api(['schedule_downtime'] + selargs) 230 | if isinstance(res, int): 231 | return res 232 | if not isinstance(res, dict): 233 | return critical('API returned unknown object type') 234 | if not res['success']: 235 | return critical('Failed: %s' % res['content']) 236 | return 0 237 | 238 | 239 | def do_cancel_downtime(cmd, args, opts): 240 | '''Cancel a scheduled downtime for a host or service. Usage: 241 | 242 | %prog cancel-downtime [service] [opts] 243 | 244 | host must be a hostname that Nagios knows about. If specified, 245 | service is a service that exists on that host. This combination of 246 | host and optional service indicates what you want the downtime to be 247 | cancelled from. 248 | 249 | Available options: 250 | 251 | --recursive Cancel downtime for all services on this host. 252 | You must not specify a specific service. 253 | 254 | Example: 255 | 256 | %prog cancel-downtime web01 257 | # cancel downtime for web01 258 | 259 | %prog cancel-downtime web01 "PING Check" 260 | # cancel downtime for PING Check on web01 261 | 262 | %prog cancel-downtime web03 --recursive 263 | # cancel downtime for web03 and ALL services on it 264 | 265 | NOTE: If you have just scheduled the downtime through the API, note 266 | that it may take a little while before you can cancel it. Nagios 267 | is not instant and it may not write out the status file (with the 268 | downtime id) for some time. 269 | 270 | ''' 271 | p = OptionParser(usage=trim(do_cancel_downtime.__doc__)) 272 | p.disable_interspersed_args() 273 | p.add_option('-r', '--recursive', dest='recursive', action='store_true', 274 | help='Cancel for all services on the given host') 275 | p.set_defaults(recursive=False, author=None, comment=None) 276 | 277 | selargs = consume_host_service(args) 278 | if selargs is None: 279 | p.error('Failed to locate host/service to cancel downtime for') 280 | 281 | (options, args) = p.parse_args(opts) 282 | if options.recursive: 283 | selargs += ['services_too=true'] 284 | 285 | res = api(['cancel_downtime'] + selargs) 286 | if isinstance(res, int): 287 | return res 288 | if not isinstance(res, dict): 289 | return critical('API returned unknown object type') 290 | if not res['success']: 291 | return critical('Failed: %s' % res['content']) 292 | return 0 293 | 294 | 295 | def do_disable_notifications(cmd, args, opts): 296 | '''Disable notifications for a host or service. Usage: 297 | 298 | %prog disable-notifications [service] [opts] 299 | 300 | host must be a hostname that Nagios knows about. If specified, 301 | service is a service that exists on that host. 302 | 303 | Available options: 304 | 305 | --recursive Target the host and all services on this host. 306 | You must not specify a specific service. 307 | 308 | Example: 309 | 310 | %prog disable-notifications web01 311 | # disable for for web01 312 | 313 | %prog disable-notifications web01 "PING Check" 314 | # disable for PING Check on web01 315 | 316 | %prog disable-notifications web03 --recursive 317 | # disable for web03 and ALL services on it 318 | 319 | ''' 320 | p = OptionParser(usage=trim(do_disable_notifications.__doc__)) 321 | p.disable_interspersed_args() 322 | p.add_option('-r', '--recursive', dest='recursive', action='store_true', 323 | help='Disable for all services on the given host') 324 | p.set_defaults(recursive=False, author=None, comment=None) 325 | 326 | selargs = consume_host_service(args) 327 | if selargs is None: 328 | p.error('Failed to locate host/service to act on') 329 | 330 | (options, args) = p.parse_args(opts) 331 | if options.recursive: 332 | selargs += ['services_too=true'] 333 | 334 | res = api(['disable_notifications'] + selargs) 335 | if isinstance(res, int): 336 | return res 337 | if not isinstance(res, dict): 338 | return critical('API returned unknown object type') 339 | if not res['success']: 340 | return critical('Failed: %s' % res['content']) 341 | return 0 342 | 343 | 344 | def do_enable_notifications(cmd, args, opts): 345 | '''Re-enable notifications for a host or service. Usage: 346 | 347 | %prog enable-notifications [service] [opts] 348 | 349 | host must be a hostname that Nagios knows about. If specified, 350 | service is a service that exists on that host. 351 | 352 | Available options: 353 | 354 | --recursive Target the host and all services on this host. 355 | You must not specify a specific service. 356 | 357 | Example: 358 | 359 | %prog enable-notifications web01 360 | # enable for for web01 361 | 362 | %prog enable-notifications web01 "PING Check" 363 | # enable for PING Check on web01 364 | 365 | %prog enable-notifications web03 --recursive 366 | # enable for web03 and ALL services on it 367 | 368 | ''' 369 | p = OptionParser(usage=trim(do_enable_notifications.__doc__)) 370 | p.disable_interspersed_args() 371 | p.add_option('-r', '--recursive', dest='recursive', action='store_true', 372 | help='Enable for all services on the given host') 373 | p.set_defaults(recursive=False, author=None, comment=None) 374 | 375 | selargs = consume_host_service(args) 376 | if selargs is None: 377 | p.error('Failed to locate host/service to act on') 378 | 379 | (options, args) = p.parse_args(opts) 380 | if options.recursive: 381 | selargs += ['services_too=true'] 382 | 383 | res = api(['enable_notifications'] + selargs) 384 | if isinstance(res, int): 385 | return res 386 | if not isinstance(res, dict): 387 | return critical('API returned unknown object type') 388 | if not res['success']: 389 | return critical('Failed: %s' % res['content']) 390 | return 0 391 | 392 | 393 | def do_disable_checks(cmd, args, opts): 394 | '''Disable checks for a host or service. Usage: 395 | 396 | %prog disable-checks [service] [opts] 397 | 398 | host must be a hostname that Nagios knows about. If specified, 399 | service is a service that exists on that host. 400 | 401 | Available options: 402 | 403 | --recursive Target the host and all services on this host. 404 | You must not specify a specific service. 405 | 406 | Example: 407 | 408 | %prog disable-checks web01 409 | # disable for for web01 410 | 411 | %prog disable-checks web01 "PING Check" 412 | # disable for PING Check on web01 413 | 414 | %prog disable-checks web03 --recursive 415 | # disable for web03 and ALL services on it 416 | 417 | ''' 418 | p = OptionParser(usage=trim(do_disable_checks.__doc__)) 419 | p.disable_interspersed_args() 420 | p.add_option('-r', '--recursive', dest='recursive', action='store_true', 421 | help='Disable for all services on the given host') 422 | p.set_defaults(recursive=False, author=None, comment=None) 423 | 424 | selargs = consume_host_service(args) 425 | if selargs is None: 426 | p.error('Failed to locate host/service to act on') 427 | 428 | (options, args) = p.parse_args(opts) 429 | if options.recursive: 430 | selargs += ['services_too=true'] 431 | 432 | res = api(['disable_checks'] + selargs) 433 | if isinstance(res, int): 434 | return res 435 | if not isinstance(res, dict): 436 | return critical('API returned unknown object type') 437 | if not res['success']: 438 | return critical('Failed: %s' % res['content']) 439 | return 0 440 | 441 | 442 | def do_enable_checks(cmd, args, opts): 443 | '''Re-enable checks for a host or service. Usage: 444 | 445 | %prog enable-checks [service] [opts] 446 | 447 | host must be a hostname that Nagios knows about. If specified, 448 | service is a service that exists on that host. 449 | 450 | Available options: 451 | 452 | --recursive Target the host and all services on this host. 453 | You must not specify a specific service. 454 | 455 | Example: 456 | 457 | %prog enable-checks web01 458 | # enable for for web01 459 | 460 | %prog enable-checks web01 "PING Check" 461 | # enable for PING Check on web01 462 | 463 | %prog enable-checks web03 --recursive 464 | # enable for web03 and ALL services on it 465 | 466 | ''' 467 | p = OptionParser(usage=trim(do_enable_checks.__doc__)) 468 | p.disable_interspersed_args() 469 | p.add_option('-r', '--recursive', dest='recursive', action='store_true', 470 | help='Enable for all services on the given host') 471 | p.set_defaults(recursive=False, author=None, comment=None) 472 | 473 | selargs = consume_host_service(args) 474 | if selargs is None: 475 | p.error('Failed to locate host/service to act on') 476 | 477 | (options, args) = p.parse_args(opts) 478 | if options.recursive: 479 | selargs += ['services_too=true'] 480 | 481 | res = api(['enable_checks'] + selargs) 482 | if isinstance(res, int): 483 | return res 484 | if not isinstance(res, dict): 485 | return critical('API returned unknown object type') 486 | if not res['success']: 487 | return critical('Failed: %s' % res['content']) 488 | return 0 489 | 490 | 491 | def do_acknowledge_problem(cmd, args, opts): 492 | '''Acknowledge problem for a host or service. Usage: 493 | 494 | %prog acknowledge-problem [service] --comment=TEXT [opts] 495 | 496 | host must be a hostname that Nagios knows about. If specified, 497 | service is a service that exists on that host. Comment must be specified. 498 | 499 | Available options: 500 | 501 | --comment=TEXT Comment on the acknowledged problem. 502 | 503 | --sticky=TRUE/FALSE [defaults to TRUE] If sticky, an acknowledgement hangs around 504 | until a host reaches "OK" - otherwise, it goes away on next 505 | state change. 506 | 507 | --notify=TRUE/FALSE [defaults to TRUE] If TRUE, sends notification that an 508 | acknowledgement has occurred. 509 | 510 | --persistent=TRUE/FALSE [defaults to FALSE] If TRUE, the comment will remain after the 511 | host returns to "OK" state. 512 | 513 | --author=STR [defaults to "nagios-api"] Who is doing the acknowledging. 514 | 515 | Example: 516 | 517 | %prog acknowledge-problem web01 --comment="Critical Failure" 518 | # acknowledge problem for web01 with comment "Critical Failure" 519 | 520 | %prog acknowledge-problem web03 "HTTP" --comment="Critical Failure" 521 | # acknowledge problem on HTTP service for web03 with comment "Critical Failure" 522 | 523 | ''' 524 | p = OptionParser(usage=trim(do_acknowledge_problem.__doc__)) 525 | p.disable_interspersed_args() 526 | p.add_option('-c', '--comment', dest='comment', action='store', 527 | help="Comment on the acknowledged problem.") 528 | p.add_option('-s', '--sticky', dest='sticky', action='store_false', 529 | help='Does acknowledgement hang around until "OK" reached by host?') 530 | p.add_option('-n', '--notify', dest='notify', action='store_false', 531 | help='Should we send a notification of the acknowledgment?') 532 | p.add_option('-p', '--persistent', dest='persistent', action='store_true', 533 | help='Should the comment hang around after the problem expires?') 534 | p.add_option('-a', '--author', dest='author', action='store', 535 | help='Name appearing for who does the acknowledgment.') 536 | p.set_defaults(sticky=True,notify=True,persistent=False,author=None) 537 | 538 | selargs = consume_host_service(args) 539 | if selargs is None: 540 | p.error('Failed to locate host/service to act on') 541 | 542 | (options, args) = p.parse_args(opts) 543 | if options.comment: 544 | selargs += ['comment=%s' % options.comment] 545 | else: 546 | return critical('Did not include comment when required!') 547 | if options.sticky is not True: 548 | selargs += ['sticky=FALSE'] 549 | if options.notify is not True: 550 | selargs += ['notify=FALSE'] 551 | if options.persistent: 552 | selargs += ['persistent=TRUE'] 553 | if options.author: 554 | selargs += ['author=%s' % options.author] 555 | 556 | res = api(['acknowledge_problem'] + selargs) 557 | if isinstance(res, int): 558 | return res 559 | if not isinstance(res, dict): 560 | return critical('API returned unknown object type') 561 | if not res['success']: 562 | return critical('Failed: %s' % res['content']) 563 | return 0 564 | 565 | 566 | def do_hosts(cmd, args, opts): 567 | '''Return a plain list of all hosts. 568 | 569 | ''' 570 | global NAGIOS 571 | for host in NAGIOS: 572 | print host 573 | return 0 574 | 575 | 576 | def do_services(cmd, args, opts): 577 | '''For a given host, return all services. Usage: 578 | 579 | %prog services 580 | 581 | Specify the host you wish to view the services of. 582 | ''' 583 | global NAGIOS 584 | p = OptionParser(usage=trim(do_services.__doc__)) 585 | if len(args) <= 0 or args[0] not in NAGIOS: 586 | p.error('First argument must be a valid hostname') 587 | for svc in NAGIOS[args[0]]: 588 | print svc 589 | return 0 590 | 591 | 592 | def critical(msg, retval=1): 593 | '''Print a message to STDERR and return a failure code. 594 | 595 | ''' 596 | print >>sys.stderr, msg 597 | return retval 598 | 599 | 600 | def do_raw(args): 601 | '''Allows the user to interact with the API directly and use this 602 | CLI as a JSON generator. Please know what you're doing. 603 | 604 | ''' 605 | resobj = api(args) 606 | if isinstance(resobj, int): 607 | return resobj 608 | if not isinstance(resobj, dict): 609 | return critical('API returned unknown object type') 610 | 611 | # Protocol failure check 612 | if not resobj['success']: 613 | return critical('Failure: %s' % resobj['content']) 614 | 615 | # These are simple responses, we can handle them here 616 | if type(resobj['content']) is str: 617 | print resobj['content'] 618 | else: 619 | print dumps(resobj['content']) 620 | return 0 621 | 622 | 623 | def trim(docstring): 624 | '''This is taken from PEP 257 for docstring usage. I'm duplicating 625 | it here so I can use it to preparse docstrings before sending them 626 | to OptionParser. Otherwise, I can either not indent my docstrings 627 | (in violation of the PEP) or I can have the usage outputs be 628 | indented. 629 | 630 | ''' 631 | if not docstring: 632 | return '' 633 | # Convert tabs to spaces (following the normal Python rules) 634 | # and split into a list of lines: 635 | lines = docstring.expandtabs().splitlines() 636 | # Determine minimum indentation (first line doesn't count): 637 | indent = sys.maxint 638 | for line in lines[1:]: 639 | stripped = line.lstrip() 640 | if stripped: 641 | indent = min(indent, len(line) - len(stripped)) 642 | # Remove indentation (first line is special): 643 | trimmed = [lines[0].strip()] 644 | if indent < sys.maxint: 645 | for line in lines[1:]: 646 | trimmed.append(line[indent:].rstrip()) 647 | # Strip off trailing and leading blank lines: 648 | while trimmed and not trimmed[-1]: 649 | trimmed.pop() 650 | while trimmed and not trimmed[0]: 651 | trimmed.pop(0) 652 | # Return a single string: 653 | return '\n'.join(trimmed) 654 | 655 | 656 | def main(argv): 657 | '''Where the fun begins. Actually do something useful. 658 | 659 | ''' 660 | global URL, NAGIOS, AUTH, USER, PASSWORD 661 | 662 | # Parse out command line options 663 | p = OptionParser(usage=trim(__doc__)) 664 | p.disable_interspersed_args() 665 | p.add_option('-H', '--host', dest='host', default='localhost', 666 | help='Host to connect to', metavar='HOST') 667 | p.add_option('-p', '--port', dest='port', type='int', default=6315, 668 | help='Port to connect to', metavar='PORT') 669 | p.add_option('-U', '--user', dest='user', 670 | help='User to connect with', metavar='USER') 671 | p.add_option('-P', '--password', dest='password', 672 | help='Password to connect with', metavar='PASSWORD') 673 | p.add_option('-u', '--url', dest='url', 674 | help='URL to use instead of host:port', metavar='URL') 675 | p.add_option('--raw', dest='raw', action='store_true', 676 | help='Enable raw mode for the CLI') 677 | (opts, args) = p.parse_args(argv[1:]) 678 | if opts.url: 679 | URL = opts.url 680 | else: 681 | URL = 'http://%s:%d' % (opts.host, opts.port) 682 | if opts.user is not None and opts.password is not None: 683 | AUTH = True 684 | USER = opts.user 685 | PASSWORD = opts.password 686 | else: 687 | AUTH = False 688 | 689 | # If no more arguments, show usage 690 | if len(args) <= 0: 691 | p.error('No command specified') 692 | 693 | # Now load the state of the world (cache this locally?) 694 | temp = api(['objects']) 695 | if isinstance(temp, dict): 696 | if not temp['success']: 697 | return critical('Failed to load objects from nagios-api') 698 | NAGIOS = temp['content'] 699 | else: 700 | return temp 701 | 702 | # If we're in raw mode, bail out for that now 703 | if opts.raw: 704 | return do_raw(args) 705 | 706 | # args will now contain the subcommand, some positional arguments, 707 | # and then the dashed options. Split them. 708 | command, posargs, otherargs = args[0], [], [] 709 | for arg in args[1:]: 710 | if len(otherargs) > 0: 711 | otherargs.append(arg) 712 | continue 713 | if arg[0] == '-': 714 | otherargs.append(arg) 715 | else: 716 | posargs.append(arg) 717 | 718 | # Dispatch table and then dispatch 719 | dispatch = { 720 | 'schedule-downtime': do_schedule_downtime, 721 | 'cancel-downtime': do_cancel_downtime, 722 | 'enable-notifications': do_enable_notifications, 723 | 'disable-notifications': do_disable_notifications, 724 | 'enable-checks': do_enable_checks, 725 | 'disable-checks': do_disable_checks, 726 | 'acknowledge-problem': do_acknowledge_problem, 727 | 'hosts': do_hosts, 728 | 'services': do_services, 729 | } 730 | for cmd in dispatch: 731 | if re.match(r'^' + command, cmd): 732 | return dispatch[cmd](command, posargs, otherargs) 733 | p.error('Command not found, see the usage') 734 | 735 | 736 | if __name__ == '__main__': 737 | sys.exit(main(sys.argv[0:])) 738 | -------------------------------------------------------------------------------- /nagios/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Nagios class. 4 | # 5 | 6 | version = "1.2.2" 7 | 8 | from core import * 9 | -------------------------------------------------------------------------------- /nagios/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # core Nagios classes. 4 | # 5 | 6 | class Nagios: 7 | '''This class represents the current state of a Nagios installation, as read 8 | from the status file that Nagios maintains. 9 | 10 | ''' 11 | def __init__(self, statusfile=None): 12 | '''Create a new Nagios state store. One argument, statusfile, is used to 13 | indicate where the status file is. This object is intended to be read-only 14 | once it has been created. 15 | 16 | ''' 17 | self.info = Info({}) 18 | self.program = Program({}) 19 | self.hosts = {} 20 | self.services = {} 21 | self.comments = {} 22 | self.downtimes = {} 23 | if statusfile is not None: 24 | self._update(statusfile) 25 | 26 | def _update(self, statusfile): 27 | '''Read the status file from Nagios and parse it. Responsible for building 28 | our internal representation of the tree. 29 | 30 | ''' 31 | # Generator to get the next status stanza. 32 | def next_stanza(f): 33 | cur = None 34 | for line in f: 35 | line = line.strip() 36 | if line.endswith('{'): 37 | if cur is not None: 38 | yield cur 39 | cur = {'type': line.split(' ', 1)[0]} 40 | elif '=' in line: 41 | key, val = line.split('=', 1) 42 | if key == "performance_data": 43 | # performance_data is special 44 | performance_data = {} 45 | split = val.split(' ') 46 | for dat in split: 47 | chunks = dat.split(';', 1) 48 | if chunks and len(chunks) > 0 and '=' in chunks[0]: 49 | (c_key, c_val) = chunks[0].split('=', 1) 50 | # convert to int or float if possible 51 | try: 52 | n_val = float(c_val) 53 | if (n_val == int(n_val)): 54 | n_val = int(n_val) 55 | except ValueError: 56 | n_val = c_val 57 | performance_data[c_key] = n_val 58 | val = performance_data 59 | cur[key] = val 60 | elif "#" in line: 61 | if not line.find("NAGIOS STATE RETENTION FILE"): 62 | raise ValueError("You appear to have used the state retention file instead of the status file. Please change your arguments and try again.") 63 | if cur is not None: 64 | yield cur 65 | 66 | f = open(statusfile, 'r') 67 | for obj in next_stanza(f): 68 | host = obj['host_name'] if 'host_name' in obj else None 69 | service = obj['service_description'] if 'service_description' in obj else None 70 | 71 | if obj['type'] == 'hoststatus': 72 | self.hosts[host] = Host(obj) 73 | elif obj['type'] == 'servicestatus': 74 | if host not in self.services: 75 | self.services[host] = {} 76 | self.services[host][service] = Service(obj) 77 | elif obj['type'].endswith('comment'): 78 | self.comments[int(obj['comment_id'])] = Comment(obj) 79 | elif obj['type'].endswith('downtime'): 80 | self.downtimes[int(obj['downtime_id'])] = Downtime(obj) 81 | elif obj['type'] == 'info': 82 | self.info = Info(obj) 83 | elif obj['type'] == 'programstatus': 84 | self.program = Program(obj) 85 | f.close() 86 | 87 | for host in self.services: 88 | for s in self.services[host].itervalues(): 89 | self.host_or_service(host).attach_service(s) 90 | for c in self.comments.itervalues(): 91 | tmp = self.host_or_service(c.host, c.service) 92 | if (tmp is None): 93 | # FIXME: throw something? 94 | pass 95 | else: 96 | tmp.attach_comment(c) 97 | for d in self.downtimes.itervalues(): 98 | self.host_or_service(d.host, d.service).attach_downtime(d) 99 | 100 | def host_or_service(self, host, service=None): 101 | '''Return a Host or Service object for the given host/service combo. 102 | Note that Service may be None, in which case we return a Host. 103 | 104 | ''' 105 | if service is not None: 106 | try: 107 | service = service.encode('utf-8') 108 | except: 109 | pass 110 | if host not in self.hosts: 111 | return None 112 | if service is None: # Only a Host if they really want it. 113 | return self.hosts[host] 114 | if host not in self.services or service not in self.services[host]: 115 | return None 116 | return self.services[host][service] 117 | 118 | def for_json(self): 119 | '''Given a Nagios state object, return a pruned down dict that is 120 | ready to be serialized to JSON. 121 | 122 | ''' 123 | out = {} 124 | for host in self.hosts: 125 | out[host] = self.hosts[host].for_json() 126 | return out 127 | 128 | 129 | class NagiosObject: 130 | '''A base class that does a little fancy parsing. That's it. 131 | 132 | ''' 133 | def __init__(self, obj): 134 | '''Builder for the base.''' 135 | for key in obj: 136 | self.__dict__[key] = obj[key] 137 | self.host = getattr(self, 'host_name', None) 138 | self.service = getattr(self, 'service_description', None) 139 | self.essential_keys = [] 140 | 141 | def for_json(self): 142 | '''Return a dict of ourselves that is ready to be serialized out 143 | to JSON. This only returns the data that we think is essential for 144 | any UI to show. 145 | 146 | ''' 147 | obj = {} 148 | for key in self.essential_keys: 149 | obj[key] = getattr(self, key, None) 150 | return obj 151 | 152 | 153 | class Info(NagiosObject): 154 | def __init__(self, obj): 155 | NagiosObject.__init__(self, obj) 156 | self.essential_keys = ['created', 'version', 'last_update_check', 157 | 'update_available', 'last_version', 'new_version'] 158 | 159 | 160 | class Program(NagiosObject): 161 | def __init__(self, obj): 162 | NagiosObject.__init__(self, obj) 163 | self.essential_keys = [ 164 | 'modified_host_attributes', 165 | 'modified_service_attributes', 166 | 'nagios_pid', 167 | 'daemon_mode', 168 | 'program_start', 169 | 'last_log_rotation', 170 | 'enable_notifications', 171 | 'active_service_checks_enabled', 172 | 'passive_service_checks_enabled', 173 | 'active_host_checks_enabled', 174 | 'passive_host_checks_enabled', 175 | 'enable_event_handlers', 176 | 'obsess_over_services', 177 | 'obsess_over_hosts', 178 | 'check_service_freshness', 179 | 'check_host_freshness', 180 | 'enable_flap_detection', 181 | 'process_performance_data', 182 | 'global_host_event_handle', 183 | 'global_service_event_handle', 184 | 'next_comment_id', 185 | 'next_downtime_id', 186 | 'next_event_id', 187 | 'next_problem_id', 188 | 'next_notification_id', 189 | 'active_scheduled_host_check_stats', 190 | 'active_ondemand_host_check_stats', 191 | 'passive_host_check_stats', 192 | 'active_scheduled_service_check_stats', 193 | 'active_ondemand_service_check_stats', 194 | 'passive_service_check_stats', 195 | 'cached_host_check_stats', 196 | 'cached_service_check_stats', 197 | 'external_command_stats', 198 | 'parallel_host_check_stats', 199 | 'serial_host_check_stats' 200 | ] 201 | 202 | 203 | class HostOrService(NagiosObject): 204 | '''Represent a single host or service. 205 | 206 | ''' 207 | def __init__(self, obj): 208 | '''Custom build a HostOrService object.''' 209 | NagiosObject.__init__(self, obj) 210 | self.downtimes = {} 211 | self.comments = {} 212 | self.essential_keys = ['current_state', 'plugin_output', 213 | 'notifications_enabled', 'last_check', 'last_notification', 214 | 'active_checks_enabled', 'problem_has_been_acknowledged', 215 | 'last_hard_state', 'scheduled_downtime_depth', 'performance_data', 216 | 'last_state_change', 'current_attempt', 'max_attempts'] 217 | 218 | def attach_downtime(self, dt): 219 | '''Given a Downtime object, store a record to it for lookup later.''' 220 | self.downtimes[dt.downtime_id] = dt 221 | 222 | def attach_comment(self, cmt): 223 | '''Given a Comment object, store a record to it for lookup later.''' 224 | self.comments[cmt.comment_id] = cmt 225 | 226 | 227 | 228 | class Host(HostOrService): 229 | '''Represent a single host. 230 | 231 | ''' 232 | def __init__(self, obj): 233 | '''Custom build a Host object.''' 234 | HostOrService.__init__(self, obj) 235 | self.services = {} 236 | 237 | def attach_service(self, svc): 238 | '''Attach a Service to this Host.''' 239 | self.services[svc.service] = svc 240 | 241 | def for_json(self): 242 | '''Represent ourselves and also get attached data.''' 243 | obj = NagiosObject.for_json(self) 244 | for key in ('services', 'comments', 'downtimes'): 245 | obj[key] = {} 246 | for idx in self.__dict__[key]: 247 | obj[key][idx] = self.__dict__[key][idx].for_json() 248 | return obj 249 | 250 | 251 | class Service(HostOrService): 252 | '''Represent a single service. 253 | 254 | ''' 255 | def for_json(self): 256 | '''Represent ourselves and also get attached data.''' 257 | obj = NagiosObject.for_json(self) 258 | for key in ('comments', 'downtimes'): 259 | obj[key] = {} 260 | for idx in self.__dict__[key]: 261 | obj[key][idx] = self.__dict__[key][idx].for_json() 262 | return obj 263 | 264 | 265 | class Comment(NagiosObject): 266 | '''Represent a single comment. 267 | 268 | ''' 269 | def __init__(self, obj): 270 | '''Custom build a Comment object.''' 271 | NagiosObject.__init__(self, obj) 272 | self.essential_keys = ['comment_id', 'entry_type', 'source', 273 | 'persistent', 'entry_time', 'expires', 'expire_time', 'author', 274 | 'comment_data'] 275 | self.comment_id = int(self.comment_id) 276 | 277 | 278 | class Downtime(NagiosObject): 279 | '''Represent a single downtime event. 280 | 281 | ''' 282 | def __init__(self, obj): 283 | '''Custom build a Downtime object.''' 284 | NagiosObject.__init__(self, obj) 285 | self.essential_keys = ['downtime_id', 'entry_time', 'start_time', 286 | 'end_time', 'triggered_by', 'fixed', 'duration', 'author', 287 | 'comment'] 288 | self.downtime_id = int(self.downtime_id) 289 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.9 2 | Jinja2==2.6 3 | Twiggy==0.4.5 4 | Werkzeug==0.8.3 5 | diesel==3.0.24 6 | dnspython==1.11.1 7 | greenlet==0.4.0 8 | http-parser==0.8.1 9 | pyOpenSSL==16.2.0 10 | requests==1.2.0 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import nagios 3 | 4 | setup(name='nagios-api', 5 | version=nagios.version, 6 | description='Control nagios using an API', 7 | author='Mark Smith', 8 | author_email='mark@qq.is', 9 | license='BSD New (3-clause) License', 10 | long_description=open('README.md').read(), 11 | url='https://github.com/xb95/nagios-api', 12 | packages=['nagios'], 13 | scripts=['nagios-cli', 'nagios-api'], 14 | install_requires=[ 15 | 'diesel>=3.0', 16 | 'greenlet==0.3.4', 17 | 'requests' 18 | ] 19 | ) 20 | --------------------------------------------------------------------------------