diff --git a/requirements.txt b/requirements.txt index 522f2c9f..d063bf13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ apscheduler>=3.0.1,<3.1.0 python_dateutil>=2.4.0 oslo.concurrency>=3.8.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 +paho-mqtt>=1.3.1 diff --git a/storyboard/notifications/conf.py b/storyboard/notifications/conf.py index e6d96591..ae089eb5 100644 --- a/storyboard/notifications/conf.py +++ b/storyboard/notifications/conf.py @@ -18,6 +18,6 @@ from oslo_config import cfg CONF = cfg.CONF OPTS = [ - cfg.StrOpt('driver', choices=['pika'], + cfg.StrOpt('driver', choices=['pika', 'mqtt'], help='The notification driver to use', default='pika') ] diff --git a/storyboard/notifications/mqtt/__init__.py b/storyboard/notifications/mqtt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/storyboard/notifications/mqtt/conf.py b/storyboard/notifications/mqtt/conf.py new file mode 100644 index 00000000..60b7fca6 --- /dev/null +++ b/storyboard/notifications/mqtt/conf.py @@ -0,0 +1,51 @@ +# Copyright 2018 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg + + +MQTT_OPTS = [ + cfg.StrOpt('hostname', help="MQTT broker address/name"), + cfg.IntOpt('port', default=1883, help='MQTT broker port'), + cfg.StrOpt('username', + help="Username to authenticate against the broker."), + cfg.StrOpt('password', secret=True, + help='Password to authenticate against the broker.'), + cfg.IntOpt('qos', default=0, min=0, max=2, + help='Max MQTT QoS available on messages. This can be 0, 1, ' + 'or 2'), + cfg.StrOpt('client_id', + help='MQTT client identifier, default is hostname + pid'), + cfg.StrOpt('base_topic', default='storyboard', + help='The base MQTT topic to publish to'), + cfg.StrOpt('ca_certs', + help="The path to the Certificate Authority certificate files " + "that are to be treated as trusted. If this is the only " + "certificate option given then the client will operate in " + "a similar manner to a web browser. That is to say it will" + "require the broker to have a certificate signed by the " + "Certificate Authorities in ca_certs and will communicate " + "using TLS v1, but will not attempt any form of TLS" + "certificate based authentication."), + cfg.StrOpt('certfile', + help="The path pointing to the PEM encoded client certificate. " + "If this is set it will be used as client information for " + "TLS based authentication. Support for this feature is " + "broker dependent."), + cfg.StrOpt('keyfile', + help="The path pointing to the PEM encoded client private key." + "If this is set it will be used as client information for " + "TLS based authentication. Support for this feature is " + "broker dependent"), +] diff --git a/storyboard/notifications/mqtt/publisher.py b/storyboard/notifications/mqtt/publisher.py new file mode 100644 index 00000000..50ef3cb2 --- /dev/null +++ b/storyboard/notifications/mqtt/publisher.py @@ -0,0 +1,104 @@ +# Copyright 2018 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from oslo_config import cfg +import paho.mqtt.publish as mqtt_publish + +from storyboard.notifications.mqtt import conf + +CONF = cfg.CONF +CONF.register_opts(conf.MQTT_OPTS, group='mqtt_notifications') + + +class PushMQTT(object): + def __init__(self, hostname, port=1883, client_id=None, + keepalive=60, will=None, auth=None, tls=None, qos=0): + self.hostname = hostname + self.port = port + self.client_id = client_id + self.keepalive = 60 + self.will = will + self.auth = auth + self.tls = tls + self.qos = qos + + def publish_single(self, topic, msg): + mqtt_publish.single(topic, msg, hostname=self.hostname, + port=self.port, client_id=self.client_id, + keepalive=self.keepalive, will=self.will, + auth=self.auth, tls=self.tls, qos=self.qos) + + def publish_multiple(self, topic, msg): + mqtt_publish.multiple(topic, msg, hostname=self.hostname, + port=self.port, client_id=self.client_id, + keepalive=self.keepalive, will=self.will, + auth=self.auth, tls=self.tls, qos=self.qos) + + +def config_publisher(): + auth = None + if CONF.mqtt_notifications.username: + auth = {'username': CONF.mqtt_notifications.username, + 'password': CONF.mqtt_notifications.password} + tls = None + if CONF.mqtt_notifications.ca_certs: + tls = {'ca_certs': CONF.mqtt_notifications.ca_certs, + 'certfile': CONF.mqtt_notifications.certfile, + 'keyfile': CONF.mqtt_notifications.keyfile} + return PushMQTT(CONF.mqtt_notifications.hostname, + port=CONF.mqtt_notifications.port, + client_id=CONF.mqtt_notifications.client_id, + auth=auth, tls=tls, qos=CONF.mqtt_notifications.qos) + + +def _generate_topic(resource, resource_id=None, author_id=None, + sub_resource=None, sub_resource_id=None): + topic = [CONF.mqtt_notifications.base_topic] + if resource: + topic.append(resource) + if resource_id: + topic.append(resource_id) + if author_id: + topic.append(author_id) + if sub_resource: + topic.extend(['sub_resource', sub_resource]) + if sub_resource_id: + topic.append(sub_resource_id) + return '/'.join(topic) + + +def publish(resource, author_id=None, method=None, url=None, path=None, + query_string=None, status=None, resource_id=None, + sub_resource=None, sub_resource_id=None, resource_before=None, + resource_after=None): + mqtt_publish = config_publisher() + topic = _generate_topic(resource, resource_id, author_id, sub_resource, + sub_resource_id) + payload = { + "author_id": author_id, + "method": method, + "url": url, + "path": path, + "query_string": query_string, + "status": status, + "resource": resource, + "resource_id": resource_id, + "sub_resource": sub_resource, + "sub_resource_id": sub_resource_id, + "resource_before": resource_before, + "resource_after": resource_after + } + mqtt_publish.publish_single(topic, json.dumps(payload))