diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb811ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# IDE +*.idea + +# Build +packages/build/ + +# Compiled Object files +*.pyc + +# Test +.pytest_cache diff --git a/Description b/Description new file mode 100644 index 0000000..f133544 --- /dev/null +++ b/Description @@ -0,0 +1 @@ +FogLAMP South Roxtec plugin for Transit service \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..1ec4087 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2018 Dianomic Systems Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Package b/Package new file mode 100644 index 0000000..9c6b571 --- /dev/null +++ b/Package @@ -0,0 +1,26 @@ +# A set of variables that define how we package this repository +# +plugin_name=roxtec +plugin_type=south +plugin_install_dirname=${plugin_name} + +# Now build up the runtime requirements list. This has 3 components +# 1. Generic packages we depend on in all architectures and package managers +# 2. Architecture specific packages we depend on +# 3. Package manager specific packages we depend on +requirements="foglamp" + +case "$arch" in + x86_64) + ;; + armhf) + ;; + aarch64) + ;; +esac +case "$package_manager" in + deb) + ;; + rpm) + ;; +esac diff --git a/VERSION.south.roxtec b/VERSION.south.roxtec index 3f68f6a..4c6bc2d 100644 --- a/VERSION.south.roxtec +++ b/VERSION.south.roxtec @@ -1,2 +1,2 @@ -foglamp_south_roxtec_version=1.0.0 -foglamp_version>=1.3 +foglamp_south_roxtec_version=1.6.0 +foglamp_version>=1.6 diff --git a/make_deb b/make_deb index b185e58..c6764fb 100755 --- a/make_deb +++ b/make_deb @@ -1,7 +1,7 @@ #!/bin/bash ##-------------------------------------------------------------------- -## Copyright (c) 2018 OSIsoft, LLC +## Copyright (c) 2018 Dianomic Systems ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. diff --git a/packages/Debian/DEBIAN/postinst b/packages/Debian/DEBIAN/postinst index a3bc4da..950a20f 100755 --- a/packages/Debian/DEBIAN/postinst +++ b/packages/Debian/DEBIAN/postinst @@ -1,7 +1,7 @@ #!/bin/sh ##-------------------------------------------------------------------- -## Copyright (c) 2018 OSIsoft, LLC +## Copyright (c) 2018 Dianomic Systems ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. @@ -31,11 +31,5 @@ set_files_ownership () { chown -R root:root /usr/local/foglamp/python/foglamp/plugins/south/roxtec } -add_service () { - output=$(curl -sX POST http://localhost:8081/foglamp/service -d '{"name": "Roxtec South", "type": "south", "plugin": "roxtec", "enabled": true}') - echo ${output} -} - set_files_ownership -add_service echo "Roxtec South plugin is installed." diff --git a/packages/Debian/DEBIAN/preinst b/packages/Debian/DEBIAN/preinst deleted file mode 100755 index d614ec2..0000000 --- a/packages/Debian/DEBIAN/preinst +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/sh - -##-------------------------------------------------------------------- -## Copyright (c) 2018 OSIsoft, LLC -## -## 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. -##-------------------------------------------------------------------- - -##-------------------------------------------------------------------- -## -## @preinst DEBIAN/preinst -## This script is used to execute pre installation tasks. -## -## Author: Ivan Zoratti, Ashwin Gopalakrishnan -## -##-------------------------------------------------------------------- - -set -e -get_foglamp_script () { - foglamp_script=$(dpkg -L foglamp | grep 'foglamp/bin/foglamp$') - echo $foglamp_script -} - -is_foglamp_running () { - set +e - foglamp_script=$(get_foglamp_script) - foglamp_status_output=$($foglamp_script status 2>&1 | grep 'FogLAMP Uptime') - rc=$((!$?)) - echo $rc - set -e -} - -# main - -# exit if foglamp is not running -IS_FOGLAMP_RUNNING=$(is_foglamp_running) -if [ "$IS_FOGLAMP_RUNNING" -eq "0" ] -then - echo "*** ERROR. FogLAMP is currently not running. Start FogLAMP and try again. ***" - exit 1 -fi - diff --git a/python/foglamp/plugins/south/roxtec/roxtec.py b/python/foglamp/plugins/south/roxtec/roxtec.py index 2784a9e..7c6ced0 100644 --- a/python/foglamp/plugins/south/roxtec/roxtec.py +++ b/python/foglamp/plugins/south/roxtec/roxtec.py @@ -10,17 +10,18 @@ import os import ssl import logging -import uuid +import uuid import datetime +from threading import Thread from aiohttp import web from foglamp.common import logger from foglamp.common.web import middleware from foglamp.plugins.common import utils -from foglamp.services.south.ingest import Ingest +import async_ingest -__author__ = "Mark Riddoch" +__author__ = "Mark Riddoch, Ashish Jabble" __copyright__ = "Copyright (c) 2018 Dianomic Systems" __license__ = "Apache 2.0" __version__ = "${VERSION}" @@ -30,54 +31,70 @@ _FOGLAMP_DATA = os.getenv("FOGLAMP_DATA", default=None) _FOGLAMP_ROOT = os.getenv("FOGLAMP_ROOT", default='/usr/local/foglamp') +c_callback = None +c_ingest_ref = None +loop = None +t = None _DEFAULT_CONFIG = { 'plugin': { - 'description': 'Roxtec South Plugin', - 'type': 'string', - 'default': 'roxtec' + 'description': 'Roxtec South Plugin', + 'type': 'string', + 'default': 'roxtec', + 'readonly': 'true' }, 'port': { 'description': 'Port to listen on', 'type': 'integer', 'default': '8608', + 'order': '2', + 'displayName': 'Port' }, 'httpsPort': { 'description': 'Port to accept HTTPS connections on', 'type': 'integer', - 'default': '1608' + 'default': '1608', + 'order': '5', + 'displayName': 'Https Port' }, 'enableHttp': { 'description': 'Enable HTTP connections', 'type': 'boolean', - 'default': 'false' + 'default': 'false', + 'order': '4', + 'displayName': 'Enable Http' }, 'certificateName': { 'description': 'Certificate file name', 'type': 'string', - 'default': 'foglamp' + 'default': 'foglamp', + 'order': '6', + 'displayName': 'Certificate Name' }, - 'host': { 'description': 'Address to accept data on', 'type': 'string', 'default': '0.0.0.0', + 'order': '1', + 'displayName': 'Host' }, 'uri': { 'description': 'URI to accept data on', 'type': 'string', 'default': 'transit', + 'order': '3', + 'displayName': 'URI' } } def plugin_info(): return { - 'name': 'Roxtec Trsnsit', - 'version': '1.0', - 'mode': 'async', - 'type': 'south', - 'interface': '1.0', - 'config': _DEFAULT_CONFIG + 'name': 'Roxtec Transit', + 'version': '1.5.0', + 'mode': 'async', + 'type': 'south', + 'interface': '1.0', + 'config': _DEFAULT_CONFIG } @@ -90,22 +107,24 @@ def plugin_init(config): handle: JSON object to be used in future calls to the plugin Raises: """ - handle = config + handle = copy.deepcopy(config) return handle def plugin_start(data): + global loop, t + try: host = data['host']['value'] port = data['port']['value'] uri = data['uri']['value'] - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() - app = web.Application(middlewares=[middleware.error_middleware]) + app = web.Application(middlewares=[middleware.error_middleware], loop=loop) app.router.add_route('PUT', '/{}'.format(uri), RoxtecTransitIngest.render_put) app.router.add_route('POST', '/{}'.format(uri), RoxtecTransitIngest.render_put) - handler = app.make_handler() + handler = app.make_handler(loop=loop) # SSL context ssl_ctx = None @@ -113,14 +132,14 @@ def plugin_start(data): is_https = True if data['enableHttp']['value'] == 'false' else False if is_https: port = data['httpsPort']['value'] - cert_name = data['certificateName']['value'] + cert_name = data['certificateName']['value'] ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) cert, key = get_certificate(cert_name) _LOGGER.info('Loading TLS certificate %s and key %s', cert, key) ssl_ctx.load_cert_chain(cert, key) server_coro = loop.create_server(handler, host, port, ssl=ssl_ctx) - future = asyncio.ensure_future(server_coro) + future = asyncio.ensure_future(server_coro, loop=loop) data['app'] = app data['handler'] = handler @@ -133,6 +152,13 @@ def f_callback(f): data['server'] = f.result() future.add_done_callback(f_callback) + + def run(): + global loop + loop.run_forever() + + t = Thread(target=run) + t.start() except Exception as e: _LOGGER.exception(str(e)) @@ -152,23 +178,25 @@ def plugin_reconfigure(handle, new_config): """ _LOGGER.info("Old config for Roxtec plugin {} \n new config {}".format(handle, new_config)) - # Find diff between old config and new config - diff = utils.get_diff(handle, new_config) + global loop + plugin_shutdown(handle) + new_handle = plugin_init(new_config) + plugin_start(new_handle) - # Plugin should re-initialize and restart if key configuration is changed - if 'port' in diff or 'httpsPort' in diff or 'certificateName' in diff or 'enableHttp' in diff or 'host' in diff: - _plugin_stop(handle) - new_handle = plugin_init(new_config) - new_handle['restart'] = 'yes' - _LOGGER.info("Restarting Roxtec plugin due to change in configuration keys [{}]".format(', '.join(diff))) - else: - new_handle = copy.deepcopy(new_config) - new_handle['restart'] = 'no' return new_handle -def _plugin_stop(handle): - _LOGGER.info('Stopping Roxtec Transit plugin.') +def plugin_shutdown(handle): + """ Shutdowns the plugin doing required cleanup, to be called prior to the South service being shut down. + + Args: + handle: handle returned by the plugin initialisation call + Returns: + Raises: + """ + _LOGGER.info('Roxtec Transit plugin shutting down.') + + global loop try: app = handle['app'] handler = handle['handler'] @@ -176,30 +204,30 @@ def _plugin_stop(handle): if server: server.close() - asyncio.ensure_future(server.wait_closed()) - asyncio.ensure_future(app.shutdown()) - asyncio.ensure_future(handler.shutdown(60.0)) - asyncio.ensure_future(app.cleanup()) + asyncio.ensure_future(server.wait_closed(), loop=loop) + asyncio.ensure_future(app.shutdown(), loop=loop) + asyncio.ensure_future(handler.shutdown(60.0), loop=loop) + asyncio.ensure_future(app.cleanup(), loop=loop) + loop.stop() except Exception as e: _LOGGER.exception(str(e)) raise -def plugin_shutdown(handle): - """ Shutdowns the plugin doing required cleanup, to be called prior to the South service being shut down. +def plugin_register_ingest(handle, callback, ingest_ref): + """Required plugin interface component to communicate to South C server Args: handle: handle returned by the plugin initialisation call - Returns: - Raises: + callback: C opaque object required to passed back to C->ingest method + ingest_ref: C opaque object required to passed back to C->ingest method """ - _plugin_stop(handle) - _LOGGER.info('Roxtec Transit plugin shut down.') - + global c_callback, c_ingest_ref + c_callback = callback + c_ingest_ref = ingest_ref def get_certificate(cert_name): - if _FOGLAMP_DATA: certs_dir = os.path.expanduser(_FOGLAMP_DATA + '/etc/certs') else: @@ -218,8 +246,9 @@ def get_certificate(cert_name): return cert, key + class RoxtecTransitIngest(object): - """Handles incoming sensor readings from HTTP Listener""" + """Handles incoming sensor readings from Roxtec Transit Listener""" @staticmethod async def render_put(request): @@ -231,9 +260,9 @@ async def render_put(request): .. code-block:: python - { + { "guard_id": "444DF705F0F8", - "gateway_id": "device-0" + "gateway_id": "device-0", "state": 70, "transit_id": "t11", "battery": 4, @@ -243,26 +272,19 @@ async def render_put(request): } Example: - curl -X PUT http://localhost:1608/transit -d '[{ "guard_id": "444DF705F0F8", "gateway_id": "device-0" "state": 70, "transit_id": "t11", "battery": 4, "pressure": 722, "temperature": 0, "last_seen": 1533816739126 }]' + curl --insecure -X PUT https://localhost:1608/transit -d '[{ "guard_id": "444DF705F0F8", "gateway_id": "device-0", "state": 70, "transit_id": "t11", "battery": 4, "pressure": 722, "temperature": 0, "last_seen": 1533816739126 }]' + curl -X PUT http://localhost:8608/transit -d '[{ "guard_id": "444DF705F0F8", "gateway_id": "device-0", "state": 70, "transit_id": "t11", "battery": 4, "pressure": 722, "temperature": 0, "last_seen": 1533816739126 }]' """ - message = {'result': 'success'} try: - if not Ingest.is_available(): - message = {'busy': True} - raise web.HTTPServiceUnavailable(reason=message) - - try: - payload_block = await request.json() - except Exception: - raise ValueError('Payload block must be a valid json') - + message = {'result': 'success'} + payload_block = await request.json() if type(payload_block) is not list: raise ValueError('Payload block must be a valid list') for payload in payload_block: asset = "Guard " + payload['guard_id'] - epochMs = payload['last_seen'] / 1000.0 - timestamp = datetime.datetime.fromtimestamp(epochMs).strftime('%Y-%m-%d %H:%M:%S.%f') + epoch_ms = payload['last_seen'] / 1000.0 + timestamp = datetime.datetime.fromtimestamp(epoch_ms).strftime('%Y-%m-%d %H:%M:%S.%f') key = str(uuid.uuid4()) readings = { "gateway_id": payload['gateway_id'], @@ -274,14 +296,17 @@ async def render_put(request): if 'transit_id' in payload and payload['transit_id'] is not None: readings['transit_id'] = payload['transit_id'] - await Ingest.add_readings(asset=asset, timestamp=timestamp, key=key, readings=readings) - + data = { + 'asset': asset, + 'timestamp': timestamp, + 'key': key, + 'readings': readings + } + async_ingest.ingest_callback(c_callback, c_ingest_ref, data) except (KeyError, ValueError, TypeError) as e: - Ingest.increment_discarded_readings() _LOGGER.exception("%d: %s", web.HTTPBadRequest.status_code, e) raise web.HTTPBadRequest(reason=e) except Exception as ex: - Ingest.increment_discarded_readings() _LOGGER.exception("%d: %s", web.HTTPInternalServerError.status_code, str(ex)) raise web.HTTPInternalServerError(reason=str(ex)) diff --git a/test/test_roxtec.py b/test/test_roxtec.py new file mode 100644 index 0000000..5401397 --- /dev/null +++ b/test/test_roxtec.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# FOGLAMP_BEGIN +# See: http://foglamp.readthedocs.io/ +# FOGLAMP_END + +"""Unit test for python.foglamp.plugins.south.roxtec""" + +import pytest + +from python.foglamp.plugins.south.roxtec import roxtec +from python.foglamp.plugins.south.roxtec.roxtec import _DEFAULT_CONFIG as config + +__author__ = "Ashish Jabble" +__copyright__ = "Copyright (c) 2019 Dianomic Systems" +__license__ = "Apache 2.0" +__version__ = "${VERSION}" + + +_NEW_CONFIG = { + 'plugin': { + 'description': 'Roxtec South Plugin', + 'type': 'string', + 'default': 'roxtec', + 'readonly': 'true' + }, + 'port': { + 'description': 'Port to listen on', + 'type': 'integer', + 'default': '8608', + 'order': '2', + 'displayName': 'Port' + }, + 'httpsPort': { + 'description': 'Port to accept HTTPS connections on', + 'type': 'integer', + 'default': '1608', + 'order': '5', + 'displayName': 'Https Port' + }, + 'enableHttp': { + 'description': 'Enable HTTP connections', + 'type': 'boolean', + 'default': 'false', + 'order': '4', + 'displayName': 'Enable Http' + }, + 'certificateName': { + 'description': 'Certificate file name', + 'type': 'string', + 'default': 'foglamp', + 'order': '6', + 'displayName': 'Certificate Name' + }, + 'host': { + 'description': 'Address to accept data on', + 'type': 'string', + 'default': '0.0.0.0', + 'order': '1', + 'displayName': 'Host' + }, + 'uri': { + 'description': 'URI to accept data on', + 'type': 'string', + 'default': 'transit', + 'order': '3', + 'displayName': 'URI' + } +} + + +def test_plugin_contract(): + # Evaluates if the plugin has all the required methods + assert callable(getattr(roxtec, 'plugin_info')) + assert callable(getattr(roxtec, 'plugin_init')) + assert callable(getattr(roxtec, 'plugin_start')) + assert callable(getattr(roxtec, 'plugin_shutdown')) + assert callable(getattr(roxtec, 'plugin_reconfigure')) + + assert callable(getattr(roxtec, 'plugin_register_ingest')) + + +def test_plugin_info(): + assert roxtec.plugin_info() == { + 'name': 'Roxtec Transit', + 'version': '1.5.0', + 'mode': 'async', + 'type': 'south', + 'interface': '1.0', + 'config': config + } + + +def test_plugin_init(): + assert roxtec.plugin_init(config) == config + + +@pytest.mark.asyncio +@pytest.mark.skip(reason='Not Implemented Yet') +async def test_plugin_start(): + pass + + +@pytest.mark.asyncio +@pytest.mark.skip(reason='Not Implemented Yet') +async def test_plugin_reconfigure(): + pass + + +@pytest.mark.asyncio +@pytest.mark.skip(reason='Not Implemented Yet') +async def test_plugin_shutdown(): + pass