├── .gitignore ├── README.md ├── attic └── check_mqtt ├── check-mqtt.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/*.json 2 | *.pem 3 | *.crt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # check-mqtt 2 | 3 | A [Nagios]/[Icinga] plugin for checking connectivity to an [MQTT] broker. Or with --readonly monitor an mqtt application. Or for checking the status of MQTT clients maintaining the status on an MQTT broker. 4 | 5 | This plugin connects to the specified broker and subscribes to a topic. Upon successful subscription, a message is published to said topic, and the plugin expects to receive that payload within `max_wait` seconds. 6 | 7 | ## Prerequisite 8 | This module uses the [paho-mqtt] package. Currently it is not compatible to the latest version of the package (paho-mqtt 2.0.0). Please use `pip install paho-mqtt<2`, or use a virtual environment. 9 | 10 | This module can use jsonpath-rw. To install, use `$ pip install jsonpath-rw` 11 | 12 | ## Configuration 13 | 14 | Configuration can be done via the following command line arguments: 15 | 16 | ``` 17 | usage: check-mqtt.py [-h] [-d|--debug] [-H ] [-P ] [-u ] 18 | [-p ] [-m ] [-e ] 19 | [--sleep ] [-a ] [-C ] 20 | [-k ] [-n] [-t ] [-s ] [-r] 21 | [-l ] [-j ] [-v ] 22 | [-o ] [-w ] [-c ] [-S] [-V] 23 | 24 | Nagios/Icinga plugin for checking connectivity or status of MQTT clients on an 25 | MQTT broker. 26 | 27 | optional arguments: 28 | -h, --help show this help message and exit 29 | -d, --debug enable MQTT logging 30 | -H , --host 31 | mqtt host to connect to (default: 'localhost') 32 | -P , --port 33 | network port to connect to (default: 1883) 34 | -u , --username 35 | MQTT username (default: None) 36 | -p , --password 37 | MQTT password (default: None) 38 | -m , --max-wait 39 | maximum time to wait for the check (default: 4 40 | seconds) 41 | -e , --keepalive 42 | maximum period in seconds allowed between 43 | communications with the broker (default: 60 seconds) 44 | --sleep main loop sleep period in seconds (default: 0.1 45 | seconds) 46 | -a , --cafile 47 | cafile (default: None) 48 | -C , --certfile 49 | certfile (default: None) 50 | -k , --keyfile 51 | keyfile (default: None) 52 | -n, --insecure suppress TLS verification of server hostname 53 | -t , --topic 54 | topic to use for the active check (default: 55 | 'nagios/test') 56 | -s , --subscription 57 | topic to use for the passive check (default: 'None') 58 | -r, --readonly just read the value of the topic 59 | -l , --payload 60 | payload which will be PUBLISHed (default: PiNG). If it 61 | starts with an exclamation mark (!) the output of the 62 | command will be used 63 | -j , --jsonpath 64 | if given, payload is interpreted as JSON string and 65 | value is extracted using (default: 'None') 66 | -v , --value 67 | value to compare against received payload (default: 68 | 'PiNG'). If it starts with an exclamation mark (!) the 69 | output of the command will be used 70 | -o , --operator 71 | operator to compare received value with value. Choose 72 | from ['eq', 'equal', 'lt', 'lessthan', 'gt', 73 | 'greaterthan', 'ct', 'contains'] (default: 'equal'). 74 | 'eq' compares Strings, the other convert the arguments 75 | to float before compare 76 | -w , --warning 77 | Exit with WARNING status if is true (default: 78 | 'None'). can be any Python expression, use 79 | within expression for current payload value. 80 | -c , --critical 81 | Exit with CRITICAL status if is true (default: 82 | 'None'). can be any Python expression, use 83 | within expression for current payload value. 84 | -S, --short use a shorter string on output 85 | -V, --version show program's version number and exit 86 | 87 | ``` 88 | 89 | There are no required arguments, defaults are displayed using `--help`. If `--warning` and/or `--critical` is used then possible given `--operator` and `--value` arguments are ignored. 90 | 91 |
92 |
hostname, port, username, password
93 | used to connect to a MQTT broker 94 | 95 |
cafile certfile keyfile insecure
96 | optional used for an encrypted TLS connection, for details see mosquitto.conf - Certificate based SSL/TLS Support. 97 | 98 |
max_wait
99 |
is the time (integer) we're willing to wait for a SUB to the topic we PUBlish on. If we don't receive the MQTT PUB within this many seconds we exit with _CRITICAL_
100 | 101 |
keepalive
102 |
maximum period in seconds (integer) allowed between communications with the broker. If no other messages are being exchanged, this controls the rate at which the client will send ping messages to the broker
103 | 104 |
sleep
105 |
period in seconds (float) to sleep in main loop - may reduce cpu load if a lot of processes (>100) are running.
106 | 107 |
topic
108 |
topic where the payload will be published when we have received the subscribed message.
109 | 110 |
payload
111 |
payload to publish on topic.
112 | 113 |
subscription
114 |
topic to use for the passive check - read only. If subscription is not given it will be set to topic
115 | 116 |
readonly
117 |
just read on subscription, do not publish any payload on topic
118 | 119 |
jsonpath
120 |
a JSONPath expression refering to a JSON structure (for JSONPath syntax see JSONPath expressions)
121 | 122 |
value, operator
123 |
value to compare against received payload. The comparison is done using one of the listed (see help above) operators. The returned status is OK if the comparison is true, otherwise it will return CRITICAL. If -w (--warning) or -c (--critical) argument is used, value and operator will be ignored.
124 | 125 |
warning, critical
126 |
a warning and/or critical expression. Use the word payload within your formular to refer to the read payload value.
If both are given (warning and critical) the critical expression overrule the warning. <exp> can be any valid pyhton expression inclusive build-in and standard library functions e. g. conversion like str(), float()...
Using one of them a possible --value and/or --operator argument will be ignored.
127 | 128 |
short
129 |
if set it will use a short string layout for returned message
130 |
131 | 132 | 133 | ## Examples 134 | 135 | #### simple 136 | 137 | ``` 138 | ./check-mqtt.py -H localhost -P 1883 -u user -p password -t nagios/test -m 10 139 | 140 | OK - message from nagios/test at localhost in 0.00 | response_time=0.10 value=PiNG 141 | ``` 142 | 143 | #### Status check 144 | 145 | ``` 146 | ./check-mqtt.py -H localhost -t devices/mydevice/lastevent -v '!expr `date +%s` - 216000' -r -o greaterthan 147 | 148 | OK - message from devices/mydevice/lastevent at localhost in 0.05s | response_time=0.05 value=1472626997 149 | ``` 150 | 151 | #### Ping Pong check 152 | 153 | ``` 154 | ./check-mqtt.py -H localhost -t nagios/ListenForPing -s nagios/PublishPongTo -l ping -v pong 155 | 156 | OK - message from nagios/PublishPongTo at localhost in 0.05s | response_time=0.05 value=pong 157 | ``` 158 | 159 | #### Jsonpath check 160 | 161 | ``` 162 | ./check-mqtt.py -H localhost -t devices/mydevice/sensor -v '950' -j '$.BME280.Pressure' -r -o greaterthan 163 | 164 | OK - message from devices/mydevice/sensor at localhost in 0.06s | response_time=0.06 value=1005.0 165 | ``` 166 | 167 | #### Jsonpath check using range (warning if lower than 4° or higher than 28°, critical if minus or higher than 35°) 168 | 169 | ``` 170 | ./check-mqtt.py -H localhost -t devices/mydevice/sensor -v '950' -j '$.BME280.Temperature' -r --warning 'payload < 4 or payload >28' --critical 'payload < 0 or payload >35' 171 | 172 | OK - message from devices/mydevice/sensor at localhost in 0.06s | response_time=0.06 value=20.1 173 | ``` 174 | 175 | 176 | ## Nagios Configuration 177 | 178 | ### command definition 179 | ``` 180 | define command{ 181 | command_name check_mqtt 182 | command_line $USER1$/check_mqtt 183 | } 184 | 185 | define command{ 186 | command_name check_myapplication 187 | command_line $USER1$/check_mqtt -i pong -t mytopic/test/myapplication 188 | } 189 | ``` 190 | 191 | ### icinga2 command definition 192 | ``` 193 | 194 | object CheckCommand "check-mqtt" { 195 | import "plugin-check-command" 196 | 197 | command = [ PluginDir + "/check-mqtt.py" ] //constants.conf -> const PluginDir 198 | 199 | arguments = { 200 | "-H" = "$mqtt_host$" 201 | "-u" = "$mqtt_user$" 202 | "-p" = "$mqtt_password$" 203 | "-P" = "$mqtt_port$" 204 | "-a" = "$mqtt_cafile$" 205 | "-c" = "$mqtt_certfile$" 206 | "-k" = "$mqtt_keyfile$" 207 | "-t" = "$mqtt_topic$" 208 | "-m" = { 209 | set_if = "$mqtt_max$" 210 | value = "$mqtt_max$" 211 | } 212 | 213 | "-l" = "$mqtt_payload$" 214 | "-v" = "$mqtt_value$" 215 | "-o" = "$mqtt_operator$" 216 | 217 | "-r" = { 218 | set_if = "$mqtt_readonly$" 219 | description = "Don't write." 220 | } 221 | "-n" = { 222 | set_if = "$mqtt_insecure$" 223 | description = "suppress TLS hostname check" 224 | } 225 | } 226 | } 227 | 228 | 229 | ``` 230 | ### service definition 231 | ``` 232 | define service{ 233 | use local-service 234 | host_name localhost 235 | service_description mqtt broker 236 | check_command check_mqtt 237 | notifications_enabled 0 238 | } 239 | 240 | define service{ 241 | use local-service 242 | host_name localhost 243 | service_description check if myapplication is running 244 | check_command check_myapplication 245 | notifications_enabled 0 246 | } 247 | 248 | ``` 249 | 250 | ### icinga2 host definition 251 | ``` 252 | object Host "wemos1" { 253 | import "generic-host" 254 | check_command = "check-mqtt" 255 | 256 | vars.homie = true 257 | vars.lastevent = true 258 | 259 | vars.mqtt_host = "localhost" 260 | # vars.mqtt_port = 1883 261 | # vars.mqtt_user = "user" 262 | # vars.mqtt_password = "password" 263 | # vars.mqtt_cafile = "cafile" 264 | # vars.mqtt_certfile = "certfile" 265 | # vars.mqtt_keyfile = "keyfile" 266 | vars.mqtt_prefix = "devices/mydevice" 267 | 268 | vars.mqtt_topic = vars.mqtt_prefix + "/$$online" 269 | vars.mqtt_payload = "true" 270 | vars.mqtt_value = "true" 271 | vars.mqtt_operator = "equal" 272 | vars.mqtt_readonly = true 273 | 274 | vars.os = "Homie" 275 | vars.sla = "24x7" 276 | } 277 | ``` 278 | 279 | ### icinga2 service definition 280 | 281 | ``` 282 | apply Service "mqtt-health" { 283 | import "generic-service" 284 | 285 | check_command = "check-mqtt" 286 | 287 | assign where host.vars.mqtt == true 288 | ignore where host.vars.no_health_check == true 289 | } 290 | 291 | apply Service "homie-health" { 292 | import "generic-service" 293 | 294 | check_command = "check-mqtt" 295 | vars.mqtt_topic = host.vars.mqtt_prefix + "/$$online" 296 | vars.mqtt_payload = "true" 297 | vars.mqtt_value = "true" 298 | vars.mqtt_operator = "equal" 299 | vars.mqtt_readonly = true 300 | 301 | assign where host.vars.homie == true 302 | ignore where host.vars.no_health_check == true 303 | } 304 | 305 | 306 | apply Service "lastevent-health" { 307 | import "generic-service" 308 | 309 | check_command = "check-mqtt" 310 | 311 | vars.mqtt_topic = host.vars.mqtt_prefix + "/lastevent" 312 | vars.mqtt_payload = "true" 313 | vars.mqtt_value = "!expr `date +%s` - 21600" 314 | vars.mqtt_operator = "greaterthan" 315 | vars.mqtt_readonly = true 316 | 317 | assign where host.vars.lastevent == true 318 | ignore where host.vars.no_health_check == true 319 | } 320 | ``` 321 | 322 | [nagios]: http://nagios.org 323 | [icinga]: http://icinga.org 324 | [mqtt]: http://mqtt.org 325 | [paho-mqtt]: https://pypi.org/project/paho-mqtt 326 | -------------------------------------------------------------------------------- /attic/check_mqtt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | check_myrightweight.py makes use of some code from Jan-Piet Mens check-mqtt.py. 5 | https://github.com/jpmens/check-mqtt 6 | 7 | Copyright (c) 2016 Jeff Albrecht 8 | All rights reserved. 9 | 10 | Use this to monitor an mqtt application with Nagios. 11 | 12 | Unlike check-mqtt which this is based on that monitors the mqtt broker, 13 | check_myrightweight will not publish a response. 14 | check_myrightweight waits to hear a 'pong' sent from the application it is 15 | monitoring in response to the'ping' sent from check_myrightweight.py. 16 | 17 | ''' 18 | 19 | # Copyright (c) 2013-2015 Jan-Piet Mens 20 | # All rights reserved. 21 | # 22 | # Redistribution and use in source and binary forms, with or without 23 | # modification, are permitted provided that the following conditions are met: 24 | # 25 | # 1. Redistributions of source code must retain the above copyright notice, 26 | # this list of conditions and the following disclaimer. 27 | # 2. Redistributions in binary form must reproduce the above copyright 28 | # notice, this list of conditions and the following disclaimer in the 29 | # documentation and/or other materials provided with the distribution. 30 | # 3. Neither the name of mosquitto nor the names of its 31 | # contributors may be used to endorse or promote products derived from 32 | # this software without specific prior written permission. 33 | # 34 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 35 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 36 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 37 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 38 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 39 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 40 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 41 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 42 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 43 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 44 | # POSSIBILITY OF SUCH DAMAGE. 45 | 46 | import paho.mqtt.client as paho 47 | import ssl 48 | import time 49 | import sys 50 | import os 51 | import argparse 52 | 53 | check_payload = 'ping' 54 | 55 | status = 0 56 | message = '' 57 | 58 | nagios_codes = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ] 59 | 60 | def on_connect(mosq, userdata, flags, rc): 61 | """ 62 | Upon successfully being connected, we subscribe to the check_topic 63 | """ 64 | 65 | mosq.subscribe(args.check_topic, 0) 66 | 67 | def on_publish(mosq, userdata, mid): 68 | pass 69 | 70 | def on_subscribe(mosq, userdata, mid, granted_qos): 71 | """ 72 | When the subscription is confirmed, we publish our payload 73 | on the check_topic. Since we're subscribed to this same topic, 74 | on_message() will fire when we see that same message 75 | """ 76 | 77 | #(res, mid) = mosq.publish(args.check_topic, check_payload, qos=2, retain=False) 78 | (res, mid) = mosq.publish(args.check_topic, check_payload, qos=2, retain=False) 79 | 80 | def on_message(mosq, userdata, msg): 81 | """ 82 | This is invoked when we get our own message back. Verify that it 83 | is actually our message and if so, we've completed a round-trip. 84 | """ 85 | 86 | global message 87 | global status 88 | 89 | check_this = check_payload 90 | if (args.ignore_ping != None): 91 | check_this = args.ignore_ping 92 | if str(msg.payload == 'ping'): # if we're loooking for a 'pong' from a monitored application skip our ping. 93 | pass 94 | 95 | if str(msg.payload) == check_this: 96 | userdata['have_response'] = True 97 | status = 0 98 | elapsed = (time.time() - userdata['start_time']) 99 | message = "Publish to %s at %s responded in %.2f with msg %s" % (args.check_topic, args.mqtt_host, elapsed, msg.payload) 100 | 101 | def on_disconnect(mosq, userdata, rc): 102 | 103 | if rc != 0: 104 | exitus(1, "Unexpected disconnection. Incorrect credentials?") 105 | 106 | def exitus(status=0, message="all is well"): 107 | """ 108 | Produce a Nagios-compatible single-line message and exit according 109 | to status 110 | """ 111 | 112 | print "%s - %s" % (nagios_codes[status], message) 113 | sys.exit(status) 114 | 115 | 116 | parser = argparse.ArgumentParser() 117 | parser.add_argument('-H', '--host', metavar="", help="mqtt host to connect to (defaults to localhost)", dest='mqtt_host', default="localhost") 118 | parser.add_argument('-P', '--port', metavar="", help="network port to connect to (defaults to 1883)", dest='mqtt_port', default=1883, type=int) 119 | parser.add_argument('-u', '--username', metavar="", help="username", dest='mqtt_username', default=None) 120 | parser.add_argument('-p', '--password', metavar="", help="password", dest='mqtt_password', default=None) 121 | parser.add_argument('-t', '--topic', metavar="", help="topic to use for the check (defaults to nagios/test)", dest='check_topic', default='nagios/test') 122 | parser.add_argument('-m', '--max-wait', metavar="", help="maximum time to wait for the check (defaults to 4 seconds)", dest='max_wait', default=4, type=int) 123 | parser.add_argument('-i', '--ignoreping', metavar="", help="ignore ping, test on pong sent from monitored clients", dest='ignore_ping', default=None) 124 | args = parser.parse_args() 125 | 126 | userdata = { 127 | 'have_response' : False, 128 | 'start_time' : time.time(), 129 | } 130 | mqttc = paho.Client('nagios-%d' % (os.getpid()), clean_session=True, userdata=userdata, protocol=4) 131 | mqttc.on_message = on_message 132 | mqttc.on_connect = on_connect 133 | mqttc.on_disconnect = on_disconnect 134 | mqttc.on_publish = on_publish 135 | mqttc.on_subscribe = on_subscribe 136 | 137 | #mqttc.tls_set('root.ca', 138 | # cert_reqs=ssl.CERT_REQUIRED, 139 | # tls_version=1) 140 | 141 | #mqttc.tls_set('root.ca', certfile='c1.crt', keyfile='c1.key', cert_reqs=ssl.CERT_REQUIRED, tls_version=3, ciphers=None) 142 | #mqttc.tls_insecure_set(True) # optional: avoid check certificate name if true 143 | 144 | # username & password may be None 145 | if args.mqtt_username is not None: 146 | mqttc.username_pw_set(args.mqtt_username, args.mqtt_password) 147 | 148 | # Attempt to connect to broker. If this fails, issue CRITICAL 149 | 150 | try: 151 | mqttc.connect(args.mqtt_host, args.mqtt_port, 60) 152 | except Exception, e: 153 | status = 2 154 | message = "Connection to %s:%d failed: %s" % (args.mqtt_host, args.mqtt_port, str(e)) 155 | exitus(status, message) 156 | 157 | rc = 0 158 | while userdata['have_response'] == False and rc == 0: 159 | rc = mqttc.loop() 160 | if time.time() - userdata['start_time'] > args.max_wait: 161 | message = 'timeout waiting for PUB' 162 | status = 2 163 | break 164 | 165 | mqttc.disconnect() 166 | 167 | exitus(status, message) 168 | 169 | 170 | -------------------------------------------------------------------------------- /check-mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | VER = '3.1' 4 | 5 | # Copyright (c) 2013-2015 Jan-Piet Mens 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # 1. Redistributions of source code must retain the above copyright notice, 12 | # this list of conditions and the following disclaimer. 13 | # 2. 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 | # 3. Neither the name of mosquitto nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | import paho.mqtt.client as paho 33 | try: 34 | from jsonpath_rw import jsonpath, parse 35 | module_jsonpath_rw = True 36 | except ImportError: 37 | module_jsonpath_rw = False 38 | try: 39 | import json 40 | module_json = True 41 | except ImportError: 42 | module_json = False 43 | import ssl 44 | import time 45 | import sys 46 | import os 47 | import argparse 48 | import subprocess 49 | try: 50 | import math 51 | module_math = True 52 | except ImportError: 53 | module_math = False 54 | 55 | PROG='{} v{}'.format(os.path.basename(sys.argv[0]),VER) 56 | 57 | class Status: 58 | OK = 0 59 | WARNING=1 60 | CRITICAL=2 61 | UNKNOWN=3 62 | nagios_codes = [ 'OK', 'WARNING', 'CRITICAL', 'UNKNOWN' ] 63 | 64 | DEFAULTS = { 65 | 'mqtt_host': 'localhost', 66 | 'mqtt_port': 1883, 67 | 'mqtt_username': None, 68 | 'mqtt_password': None, 69 | 'max_wait': 4, 70 | 'keepalive': 60, 71 | 'sleep': 0.1, 72 | 'mqtt_cafile': None, 73 | 'mqtt_certfile': None, 74 | 'mqtt_keyfile': None, 75 | 'mqtt_insecure': False, 76 | 'check_topic': 'nagios/test', 77 | 'check_subscription':None, 78 | 'mqtt_readonly': False, 79 | 'mqtt_payload': 'PiNG', 80 | 'mqtt_jsonpath': None, 81 | 'mqtt_value': 'PiNG', 82 | 'mqtt_operator': 'equal', 83 | 'warning': None, 84 | 'critical': None, 85 | 'short_output': False, 86 | 'debug': False, 87 | } 88 | 89 | operators = ['eq','equal','lt','lessthan','gt','greaterthan','ct','contains','any'] 90 | 91 | status = Status.OK 92 | message = '' 93 | args = {} 94 | 95 | 96 | def on_connect(mosq, userdata, flags, rc): 97 | """ 98 | Upon successfully being connected, we subscribe to the check_topic 99 | """ 100 | 101 | mosq.subscribe(args.check_subscription, 0) 102 | mosq.loop() 103 | 104 | def on_publish(mosq, userdata, mid): 105 | pass 106 | 107 | def on_subscribe(mosq, userdata, mid, granted_qos): 108 | """ 109 | When the subscription is confirmed, we publish our payload 110 | on the check_topic. Since we're subscribed to this same topic, 111 | on_message() will fire when we see that same message 112 | """ 113 | 114 | if not args.mqtt_readonly: 115 | (res, mid) = mosq.publish(args.check_topic, args.mqtt_payload, qos=2, retain=False) 116 | mosq.loop() 117 | 118 | def on_message(mosq, userdata, msg): 119 | """ 120 | This is invoked when we get our own message back. Verify that it 121 | is actually our message and if so, we've completed a round-trip. 122 | """ 123 | 124 | 125 | global message 126 | global status 127 | 128 | payload = msg.payload.decode("utf-8") 129 | 130 | if module_jsonpath_rw and module_json: 131 | if args.mqtt_jsonpath is not None: 132 | try: 133 | jspayload = json.loads(payload) 134 | jspath = parse(args.mqtt_jsonpath) 135 | extractpayload = [match.value for match in jspath.find(jspayload)] 136 | payload = extractpayload[0] 137 | except: 138 | payload = '' 139 | pass 140 | 141 | 142 | elapsed = (time.time() - userdata['start_time']) 143 | userdata['have_response'] = True 144 | 145 | if args.short_output: 146 | message = "value=%s | response_time=%.2f value=%s" % (str(payload), elapsed, str(payload)) 147 | else: 148 | message = "message from %s at %s in %.2fs | response_time=%.2f value=%s" % (args.check_subscription, args.mqtt_host, elapsed, elapsed, str(payload)) 149 | 150 | if module_math and (args.critical is not None or args.warning is not None): 151 | status = Status.OK 152 | if args.critical is not None: 153 | try: 154 | if eval(args.critical): 155 | status = Status.CRITICAL 156 | except: 157 | status = Status.CRITICAL 158 | message = "critical expression error '{}'".format(args.critical) 159 | pass 160 | if status == Status.OK and args.warning is not None: 161 | try: 162 | if eval(args.warning): 163 | status = Status.WARNING 164 | except: 165 | status = Status.CRITICAL 166 | message = "warning expression error '{}'".format(args.warning) 167 | pass 168 | else: 169 | status = Status.CRITICAL 170 | try: 171 | if (args.mqtt_operator == 'lt' or args.mqtt_operator == 'lessthan') and float(payload) < float(args.mqtt_value): 172 | status = Status.OK 173 | if (args.mqtt_operator == 'gt' or args.mqtt_operator == 'greaterthan') and float(payload) > float(args.mqtt_value): 174 | status = Status.OK 175 | if (args.mqtt_operator == 'eq' or args.mqtt_operator == 'equal') and str(payload) == args.mqtt_value: 176 | status = Status.OK 177 | if (args.mqtt_operator == 'ct' or args.mqtt_operator == 'contains') and str(payload).find(args.mqtt_value) != -1: 178 | status = Status.OK 179 | if (args.mqtt_operator == 'any') and payload: 180 | status = Status.OK 181 | except: 182 | status = Status.CRITICAL 183 | pass 184 | 185 | def on_log(mosq, userdata, level, buf): 186 | print(buf, file=sys.stderr) 187 | 188 | def on_disconnect(mosq, userdata, rc): 189 | 190 | if rc != 0: 191 | exitus(1, "Unexpected disconnection. Incorrect credentials?") 192 | 193 | def exitus(status=Status.OK, message="all is well"): 194 | """ 195 | Produce a Nagios-compatible single-line message and exit according 196 | to status 197 | """ 198 | 199 | print("%s - %s" % (nagios_codes[status], message)) 200 | sys.exit(status) 201 | 202 | parser = argparse.ArgumentParser(description='Nagios/Icinga plugin for checking connectivity or status of MQTT clients on an MQTT broker.', 203 | epilog='There are no required arguments, defaults are displayed using --help. If --warning and/or --critical is used then possible given --operator and --value arguments are ignored.') 204 | 205 | parser.add_argument('-d', '--debug', default=False, help="enable MQTT logging", action='store_true', dest='debug') 206 | 207 | parser.add_argument('-H', '--host', metavar="", help="mqtt host to connect to (default: '{}')".format(DEFAULTS['mqtt_host']), dest='mqtt_host', default=DEFAULTS['mqtt_host']) 208 | parser.add_argument('-P', '--port', metavar="", help="network port to connect to (default: {})".format(DEFAULTS['mqtt_port']), dest='mqtt_port', default=DEFAULTS['mqtt_port'], type=int) 209 | 210 | parser.add_argument('-u', '--username', metavar="", help="MQTT username (default: {})".format(DEFAULTS['mqtt_username']), dest='mqtt_username', default=DEFAULTS['mqtt_username']) 211 | parser.add_argument('-p', '--password', metavar="", help="MQTT password (default: {})".format(DEFAULTS['mqtt_password']), dest='mqtt_password', default=DEFAULTS['mqtt_password']) 212 | 213 | parser.add_argument('-m', '--max-wait', metavar="", help="maximum time to wait for the check (default: {} seconds)".format(DEFAULTS['max_wait']), dest='max_wait', default=DEFAULTS['max_wait'], type=int) 214 | parser.add_argument('-e', '--keepalive', metavar="", help="maximum period in seconds allowed between communications with the broker (default: {} seconds)".format(DEFAULTS['keepalive']), dest='keepalive', default=DEFAULTS['keepalive'], type=int) 215 | parser.add_argument( '--sleep', metavar="", help="main loop sleep period in seconds (default: {} seconds)".format(DEFAULTS['sleep']), dest='sleep', default=DEFAULTS['sleep'], type=float) 216 | 217 | parser.add_argument('-a', '--cafile', metavar="", help="cafile (default: {})".format(DEFAULTS['mqtt_cafile']), dest='mqtt_cafile', default=DEFAULTS['mqtt_cafile']) 218 | parser.add_argument('-C', '--certfile', metavar="", help="certfile (default: {})".format(DEFAULTS['mqtt_certfile']), dest='mqtt_certfile', default=DEFAULTS['mqtt_certfile']) 219 | parser.add_argument('-k', '--keyfile', metavar="", help="keyfile (default: {})".format(DEFAULTS['mqtt_keyfile']), dest='mqtt_keyfile', default=DEFAULTS['mqtt_keyfile']) 220 | parser.add_argument('-n', '--insecure', help="suppress TLS verification of server hostname{}".format(" (default)" if DEFAULTS['mqtt_insecure'] else ""), dest='mqtt_insecure', default=DEFAULTS['mqtt_insecure'], action='store_true') 221 | 222 | parser.add_argument('-t', '--topic', metavar="", help="topic to use for the active check (default: '{}')".format(DEFAULTS['check_topic']), dest='check_topic', default=DEFAULTS['check_topic']) 223 | parser.add_argument('-s', '--subscription', metavar="", help="topic to use for the passive check (default: '{}')".format(DEFAULTS['check_subscription']), dest='check_subscription', default=DEFAULTS['check_subscription']) 224 | parser.add_argument('-r', '--readonly', help="just read the value of the topic{}".format(" (default)" if DEFAULTS['mqtt_readonly'] else ""), dest='mqtt_readonly', default=DEFAULTS['mqtt_readonly'], action='store_true') 225 | parser.add_argument('-l', '--payload', metavar="", help="payload which will be PUBLISHed (default: {}). If it starts with an exclamation mark (!) the output of the command will be used".format(DEFAULTS['mqtt_payload']), dest='mqtt_payload', default=DEFAULTS['mqtt_payload']) 226 | if module_jsonpath_rw and module_json: 227 | parser.add_argument('-j', '--jsonpath', metavar="", help="if given, payload is interpreted as JSON string and value is extracted using (default: '{}')".format(DEFAULTS['mqtt_jsonpath']), dest='mqtt_jsonpath', default=DEFAULTS['mqtt_jsonpath']) 228 | parser.add_argument('-v', '--value', metavar="", help="value to compare against received payload (default: '{}'). If it starts with an exclamation mark (!) the output of the command will be used".format(DEFAULTS['mqtt_value']), dest='mqtt_value', default=DEFAULTS['mqtt_value']) 229 | parser.add_argument('-o', '--operator', metavar="", help="operator to compare received value with value. Choose from {} (default: '{}'). 'eq' compares Strings, the other convert the arguments to float before compare".format(operators, DEFAULTS['mqtt_operator']), dest='mqtt_operator', default=DEFAULTS['mqtt_operator'], choices=operators) 230 | if module_math: 231 | parser.add_argument('-w', '--warning', metavar="", help="Exit with WARNING status if is true (default: '{}'). can be any Python expression, use within expression for current payload value.".format(DEFAULTS['warning']), dest='warning', default=DEFAULTS['warning']) 232 | parser.add_argument('-c', '--critical', metavar="", help="Exit with CRITICAL status if is true (default: '{}'). can be any Python expression, use within expression for current payload value.".format(DEFAULTS['critical']), dest='critical', default=DEFAULTS['critical']) 233 | parser.add_argument('-S', '--short', help="use a shorter string on output{}".format(" (default)" if DEFAULTS['short_output'] else ""), dest='short_output', default=DEFAULTS['short_output'], action='store_true') 234 | parser.add_argument('-V', '--version', action='version', version=PROG) 235 | 236 | args = parser.parse_args() 237 | 238 | if args.mqtt_payload.startswith('!'): 239 | try: 240 | args.mqtt_payload = subprocess.check_output(args.mqtt_payload[1:], shell=True) 241 | except: 242 | pass 243 | 244 | if args.mqtt_value.startswith('!'): 245 | try: 246 | args.mqtt_value = subprocess.check_output(args.mqtt_value[1:], shell=True) 247 | except: 248 | pass 249 | 250 | if args.check_subscription is None: 251 | args.check_subscription = args.check_topic 252 | 253 | userdata = { 254 | 'have_response' : False, 255 | 'start_time' : time.time(), 256 | } 257 | mqttc = paho.Client('nagios-%d' % (os.getpid()), clean_session=True, userdata=userdata, protocol=4) 258 | mqttc.on_message = on_message 259 | mqttc.on_connect = on_connect 260 | mqttc.on_disconnect = on_disconnect 261 | mqttc.on_publish = on_publish 262 | mqttc.on_subscribe = on_subscribe 263 | 264 | if args.debug: 265 | mqttc.on_log = on_log 266 | 267 | # cafile controls TLS usage 268 | if args.mqtt_cafile is not None: 269 | if args.mqtt_certfile is not None: 270 | mqttc.tls_set(args.mqtt_cafile, 271 | certfile=args.mqtt_certfile, 272 | keyfile=args.mqtt_keyfile, 273 | cert_reqs=ssl.CERT_REQUIRED) 274 | else: 275 | mqttc.tls_set(args.mqtt_cafile, 276 | cert_reqs=ssl.CERT_REQUIRED) 277 | mqttc.tls_insecure_set(args.mqtt_insecure) 278 | 279 | # username & password may be None 280 | if args.mqtt_username is not None: 281 | mqttc.username_pw_set(args.mqtt_username, args.mqtt_password) 282 | 283 | # Attempt to connect to broker. If this fails, issue CRITICAL 284 | 285 | try: 286 | mqttc.connect(args.mqtt_host, args.mqtt_port, args.keepalive) 287 | except Exception as e: 288 | status = Status.CRITICAL 289 | message = "Connection to %s:%d failed: %s" % (args.mqtt_host, args.mqtt_port, str(e)) 290 | exitus(status, message) 291 | 292 | rc = 0 293 | while userdata['have_response'] == False and rc == 0: 294 | rc = mqttc.loop() 295 | if time.time() - userdata['start_time'] > args.max_wait: 296 | message = 'timeout waiting for message' 297 | status = Status.CRITICAL 298 | break 299 | time.sleep(args.sleep) 300 | 301 | mqttc.disconnect() 302 | 303 | exitus(status, message) 304 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt<2 2 | #jsonpath-rw 3 | --------------------------------------------------------------------------------