Browse Source

initial import

Jan-Piet Mens 11 years ago
parent
commit
0ff6326498
4 changed files with 256 additions and 1 deletions
  1. 2 0
      .gitignore
  2. 83 1
      README.md
  3. 26 0
      launcher.conf.example
  4. 145 0
      mqtt-launcher.py

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+launcher.conf
+logfile

+ 83 - 1
README.md

@@ -1,7 +1,89 @@
 # mqtt-launcher
 
-_mqtt-launcher_ is a Python program which subscribes to a set of [MQTT] topics and executes processes on the host it's running on. Processes are configured on a per/wildcard basis.
+_mqtt-launcher_ is a Python program which subscribes to a set of [MQTT] topics
+and executes processes on the host it's running on. Launchable processes are
+configured on a per/wildcard basis, and they can be constrained to run only if
+a particular text payload is contained in the message.
 
+For example, I can publish a message to my MQTT broker requesting _mqtt-launcher_ 
+create a particular semaphore file for me:
+
+```
+mosquitto_pub -t sys/file -m create
+```
+
+The configuration file must be valid Python and it is loaded once. It contains
+the topic / process associations.
+
+```python
+# topic                     payload value       program & arguments
+"sys/file"  :   {
+                    'create'        :   [ '/usr/bin/touch', '/tmp/file.one' ],
+                    'false'         :   [ '/bin/rm', '-f', '/tmp/file.one'    ],
+                    'info'          :   [ '/bin/ls', '-l', '/tmp/file.one' ],
+                },
+```
+
+Above snippet instructs _mqtt-launcher_ to:
+
+* subscribe to the [MQTT] topic `sys/file`
+* look up the payload string and launch the associated programs:
+  * if the payload is `create`, then _touch_ a file
+  * if the payload is the string `false`, remove a file
+  * if the payload is `info`, return information on the file
+
+_mqtt-launcher_ publishes _stdout_ and _stderr_ of the launched program
+to the configured topic with `/report` added to it. So, in the example
+above, a non-retained message will be published to `sys/file/report`.
+(Note that this message contains whatever the command outputs; trailing
+white space is truncated.)
+
+## Screenshot
+
+Here's the obligatory "screenshot".
+
+```
+Publishes					Subscribes
+-----------------------------------		------------------------------------------------------------------
+						$ mosquitto_sub -v -t 'dev/#' -t 'sys/file/#' -t 'prog/#' 
+
+
+mosquitto_pub -t prog/pwd -n
+						prog/pwd (null)
+						prog/pwd/report /private/tmp
+
+mosquitto_pub -t sys/file -m create
+						sys/file create
+						sys/file/report (null)	# command has no output
+
+mosquitto_pub -t sys/file -m info
+						sys/file info
+						sys/file/report -rw-r--r--  1 jpm  wheel  0 Jan 22 16:10 /tmp/file.one
+
+mosquitto_pub -t sys/file -m remove
+						sys/file remove
+						# report not published: subcommand ('remove') doesn't exist
+						# log file says:
+						2014-01-22 16:11:30,393 No matching param (remove) for sys/file
+
+mosquitto_pub -t dev/1 -m hi
+						dev/1 hi
+						dev/1/report total 16231
+						drwxrwxr-x+ 157 root  admin     5338 Jan 20 10:48 Applications
+						drwxrwxr-x@   8 root  admin      272 Jan 25  2013 Developer
+						drwxr-xr-x+  72 root  wheel     2448 Oct 14 10:54 Library
+						...
+```
+
+## Configuration
+
+_mqtt-launcher_ loads a Python configuration from the path contained in
+the environment variable `$MQTTLAUNCHERCONFIG`; if unset, the path
+defaults to `launcher.conf`. See the provided `launcher.conf.example`.
+
+## Logging
+
+_mqtt-launcher_ logs its operation in the file configured as `logfile`.
 
 ## Requirements
 

+ 26 - 0
launcher.conf.example

@@ -0,0 +1,26 @@
+
+logfile = 'logfile'
+mqtt_broker = 'localhost'       # default: 'localhost'
+mqtt_port = 1883                # default: 1883
+mqtt_clientid = 'mqtt-launcher-1'
+# mqtt_username = 'jane'
+# mqtt_password = 'secret'
+
+topiclist = {
+
+    # topic                     payload value       program & arguments
+    "sys/file"          :   {
+                                'create'        :   [ '/usr/bin/touch', '/tmp/file.one' ],
+                                'false'         :   [ '/bin/rm', '-f', '/tmp/file.one'    ],
+                                'info'          :   [ '/bin/ls', '-l', '/tmp/file.one' ],
+                            },
+    "prog/pwd"          :   {
+                                None            :   [ 'pwd' ],
+                            },
+    "dev/1"             :   {
+                                None            :   [ 'ls', '-l', '/' ],
+                            },
+    "dev/2"             :   {
+                                None            :   [ "/bin/echo", "111", "*", "@!@", "222", "@!@", "333" ],
+                            },
+}

+ 145 - 0
mqtt-launcher.py

@@ -0,0 +1,145 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2014 Jan-Piet Mens <jpmens()gmail.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+# 3. Neither the name of mosquitto nor the names of its
+#    contributors may be used to endorse or promote products derived from
+#    this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+__author__    = 'Jan-Piet Mens <jpmens()gmail.com>'
+__copyright__ = 'Copyright 2014 Jan-Piet Mens'
+
+import os
+import sys
+import subprocess
+import logging
+import mosquitto
+import socket
+import string
+
+qos=2
+CONFIG=os.getenv('MQTTLAUNCHERCONFIG', 'launcher.conf')
+
+class Config(object):
+    def __init__(self, filename=CONFIG):
+        self.config = {}
+        execfile(filename, self.config)
+
+    def get(self, key, default=None):
+        return self.config.get(key, default)
+
+cf = Config()
+LOGFILE = cf.get('logfile', 'logfile')
+LOGFORMAT = '%(asctime)-15s %(message)s'
+DEBUG=True
+
+if DEBUG:
+    logging.basicConfig(filename=LOGFILE, level=logging.DEBUG, format=LOGFORMAT)
+else:
+    logging.basicConfig(filename=LOGFILE, level=logging.INFO, format=LOGFORMAT)
+
+logging.info("Starting")
+logging.debug("DEBUG MODE")
+
+def runprog(topic, param=None):
+
+    publish = "%s/report" % topic
+
+    if param is not None and all(c in string.printable for c in param) == False:
+        logging.debug("Param for topic %s is not printable; skipping" % (topic))
+        return
+
+    if not topic in topiclist:
+        logging.info("Topic %s isn't configured" % topic)
+        return
+
+    if param is not None and param in topiclist[topic]:
+        cmd = topiclist[topic].get(param)
+    else:
+        if None in topiclist[topic]: ### and topiclist[topic][None] is not None:
+            cmd = []
+            for p in topiclist[topic][None]:
+                if p == '@!@':
+                    p = param
+                cmd.append(p)
+        else:
+            logging.info("No matching param (%s) for %s" % (param, topic))
+            return
+
+    logging.debug("Running t=%s: %s" % (topic, cmd))
+
+    try:
+        res = subprocess.check_output(cmd, stdin=None, stderr=subprocess.STDOUT, shell=False, universal_newlines=True, cwd='/tmp')
+    except Exception, e:
+        res = "*****> %s" % str(e)
+
+    payload = res.rstrip('\n')
+    (res, mid) =  mqttc.publish(publish, payload, qos=qos, retain=False)
+
+
+def on_message(mosq, userdata, msg):
+    logging.debug(msg.topic+" "+str(msg.qos)+" "+str(msg.payload))
+
+    runprog(msg.topic, str(msg.payload))
+
+
+def on_disconnect(mosq, userdata, rc):
+    logging.debug("OOOOPS! launcher disconnects")
+    time.sleep(10)
+
+if __name__ == '__main__':
+
+    userdata = {
+    }
+    topiclist = cf.get('topiclist')
+
+    if topiclist is None:
+        logging.info("No topic list. Aborting")
+        sys.exit(2)
+
+    clientid = cf.get('mqtt_clientid', 'mqtt-launcher-%s' % os.getpid())
+    mqttc = mosquitto.Mosquitto(clientid, userdata=userdata, clean_session=False)
+    mqttc.on_message = on_message
+    mqttc.on_disconnect = on_disconnect
+
+    mqttc.will_set('clients/mqtt-launcher', payload="Adios!", qos=0, retain=False)
+
+    # Delays will be: 3, 6, 12, 24, 30, 30, ...
+    mqttc.reconnect_delay_set(delay=3, delay_max=30, exponential_backoff=True)
+
+    mqttc.username_pw_set(cf.get('username'), cf.get('password'))
+
+    mqttc.connect(cf.get('mqtt_broker', 'localhost'), int(cf.get('mqtt_port', '1883')), 60)
+
+    for topic in topiclist:
+        mqttc.subscribe(topic, qos)
+
+    while True:
+        try:
+            mqttc.loop_forever()
+        except socket.error:
+            time.sleep(5)
+        except KeyboardInterrupt:
+            sys.exit(0)
+