diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06c4263 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.pyc +.directory +*.swp +*~ +.tox/ +.idea/ +iotronic_lightningrod.egg-info +build +AUTHORS +Authors +ChangeLog \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..36986fe --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/iotronic-lightning-rod \ No newline at end of file diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..b9d2c78 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +iotronic_lightningrod Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..90f8a7a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..99b84ab --- /dev/null +++ b/README.rst @@ -0,0 +1,20 @@ +=============================== +Iotronic Lightning-rod Agent +=============================== + +Python implementation of Lightning-rod Agent, +the Stack4Things (http://stack4things.unime.it/) board-side probe. + +Please fill here a long description which must be at least 3 lines wrapped on +80 cols, so that distribution package maintainers can use it in their packages. +Note that this is a hard requirement. + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/iotronic_lightningrod +* Source: https://github.com/openstack/iotronic-lightning-rod +* Bugs: https://bugs.launchpad.net/iotronic-lightning-rod + +Features +-------- + +* TODO \ No newline at end of file diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..b7646ca --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# 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 os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'iotronic_lightningrod' +copyright = u'2016, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..ed77c12 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..36c5c07 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. iotronic_lightningrod documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to iotronic_lightningrod's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..d156e38 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install iotronic_lightningrod + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv iotronic_lightningrod + $ pip install iotronic_lightningrod \ No newline at end of file diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..38ba804 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..503a6cd --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use iotronic_lightningrod in a project:: + + import iotronic_lightningrod \ No newline at end of file diff --git a/etc/init.d/lightning-rod b/etc/init.d/lightning-rod new file mode 100644 index 0000000..57a384e --- /dev/null +++ b/etc/init.d/lightning-rod @@ -0,0 +1,73 @@ +#!/bin/ash + +workdir=/usr/bin + +start() { + + cd $workdir + + pid=`ps www | grep "/usr/bin/python $workdir/lightning-rod" | grep -v grep | awk '{ print $1 }'` + + + if [ -r $pid ]; then + /usr/bin/python $workdir/lightning-rod & + sleep 2 + pid=`ps www | grep "/usr/bin/python $workdir/lightning-rod" | grep -v grep | awk '{ print $1 }'` + echo "PID:" $pid + echo "Lightning-rod is started." + else + echo "Lightning-rod is already started with PID $pid." + fi + +} + +stop() { + + pid=`ps www | grep "/usr/bin/python $workdir/lightning-rod" | grep -v grep | awk '{ print $1 }'` + + if [ -r $pid ]; then + echo "Lightning-rod is already stopped!" + else + + echo "PID:" $pid + kill -9 $pid + sleep 2 + echo "Lightning-rod stopped." + fi + +} + + +status(){ + + pid=`ps www | grep "/usr/bin/python $workdir/lightning-rod" | grep -v grep | awk '{ print $1 }'` + + if [ -r $pid ]; then + echo "Lightning-rod is stopped." + else + + echo "PID:" $pid + echo "Lightning-rod is started." + fi + +} +file:///home/webwolf/Scrivania/Stack4Things/test_ipk/s4t-lr +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + status) + status + ;; + *) + echo "Usage: /etc/init.d/lightning-rod {start|stop|restart|status}" + exit 1 +esac +exit 0 \ No newline at end of file diff --git a/etc/iotronic/iotronic.conf b/etc/iotronic/iotronic.conf new file mode 100644 index 0000000..cdf00d5 --- /dev/null +++ b/etc/iotronic/iotronic.conf @@ -0,0 +1,3 @@ +[DEFAULT] +debug = True +log_file = /var/log/s4t-lightning-rod.log diff --git a/etc/systemd/system/s4t-lightning-rod.service b/etc/systemd/system/s4t-lightning-rod.service new file mode 100644 index 0000000..3751d71 --- /dev/null +++ b/etc/systemd/system/s4t-lightning-rod.service @@ -0,0 +1,17 @@ +[Unit] +Description=Iotronic Lightning-Rod +After=network.target + +[Service] +Type=simple +User=root +Group=root +StandardInput=null +StandardOutput=journal +StandardError=journal +WorkingDirectory=/usr/bin/ +ExecStart=/usr/bin/python /usr/bin/lightning-rod +Restart=on-abort + +[Install] +WantedBy=multi-user.target diff --git a/iotronic_lightningrod/Board.py b/iotronic_lightningrod/Board.py new file mode 100644 index 0000000..6972b53 --- /dev/null +++ b/iotronic_lightningrod/Board.py @@ -0,0 +1,165 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 datetime import datetime +# from dateutil.tz import tzlocal +import json +import os + +from iotronic_lightningrod.config import iotronic_home + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +SETTINGS = iotronic_home + '/settings.json' + + +class Board(object): + + def __init__(self): + self.iotronic_config = {} + + self.board_config = {} + self.name = None + self.type = None + self.status = None + self.uuid = None + self.code = None + self.agent = None + self.mobile = None + self.session = None + self.session_id = None + + self.location = {} + + self.device = None + + self.wamp_config = None + self.extra = {} + + self.loadSettings() + + def loadConf(self): + """This method loads the JSON configuraton file: settings.json. + + :return: + + """ + + try: + + with open(SETTINGS) as settings: + lr_settings = json.load(settings) + + except Exception as err: + LOG.error("Parsing error in " + SETTINGS + ": " + str(err)) + lr_settings = None + + return lr_settings + + def loadSettings(self): + '''This method gets and sets the board attributes from the conf file. + + ''' + + # Load all settings.json file + self.iotronic_config = self.loadConf() + + try: + # STATUS OPERATIVE + board_config = self.iotronic_config['iotronic']['board'] + self.uuid = board_config['uuid'] + self.code = board_config['code'] + self.name = board_config['name'] + self.status = board_config['status'] + self.type = board_config['type'] + self.mobile = board_config['mobile'] + self.extra = board_config['extra'] + self.agent = board_config['agent'] + self.created_at = board_config['created_at'] + self.updated_at = board_config['updated_at'] # self.getTimestamp() + self.location = board_config['location'] + + self.extra = self.iotronic_config['iotronic']['extra'] + + LOG.info('Board settings:') + LOG.info(' - code: ' + str(self.code)) + LOG.info(' - uuid: ' + str(self.uuid)) + # LOG.debug(" - conf:\n" + json.dumps(board_config, indent=4)) + + self.getWampAgent(self.iotronic_config) + + except Exception as err: + LOG.warning("settings.json file exception: " + str(err)) + # STATUS REGISTERED + try: + self.code = board_config['code'] + LOG.info('First registration board settings: ') + LOG.info(' - code: ' + str(self.code)) + self.getWampAgent(self.iotronic_config) + except Exception as err: + LOG.error("Wrong code: " + str(err)) + os._exit(1) + + def getWampAgent(self, config): + '''This method gets and sets the WAMP Board attributes from the conf file. + + ''' + try: + self.wamp_config = config['iotronic']['wamp']['main-agent'] + LOG.info('WAMP Agent settings:') + + except Exception: + if (self.status is None) | (self.status == "registered"): + self.wamp_config = \ + config['iotronic']['wamp']['registration-agent'] + LOG.info('Registration Agent settings:') + else: + LOG.error( + "WAMP Agent configuration is wrong... " + "please check settings.json WAMP configuration... Bye!" + ) + os._exit(1) + + LOG.info(' - agent: ' + str(self.agent)) + LOG.info(' - url: ' + str(self.wamp_config['url'])) + LOG.info(' - realm: ' + str(self.wamp_config['realm'])) + # LOG.debug("- conf:\n" + json.dumps(self.wamp_config, indent=4)) + + def setConf(self, conf): + # LOG.info("\nNEW CONFIGURATION:\n" + str(json.dumps(conf, indent=4))) + + with open(SETTINGS, 'w') as f: + json.dump(conf, f, indent=4) + + # Reload configuration + self.loadSettings() + + def updateStatus(self, status): + self.iotronic_config['iotronic']['board']["status"] = status + + with open(SETTINGS, 'w') as f: + json.dump(self.iotronic_config, f, indent=4) + + def getTimestamp(self): + # datetime.now(tzlocal()).isoformat() + return datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f') + + def setUpdateTime(self): + self.iotronic_config['iotronic']['board']["updated_at"] = \ + self.updated_at + + with open(SETTINGS, 'w') as f: + json.dump(self.iotronic_config, f, indent=4) diff --git a/iotronic_lightningrod/__init__.py b/iotronic_lightningrod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_lightningrod/common/__init__.py b/iotronic_lightningrod/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_lightningrod/common/exception.py b/iotronic_lightningrod/common/exception.py new file mode 100644 index 0000000..5938462 --- /dev/null +++ b/iotronic_lightningrod/common/exception.py @@ -0,0 +1,79 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 os +import signal + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +def manageTimeout(error_message, action): + try: + + raise TimeoutError(error_message, action) + + except TimeoutError as err: + details = err.args[0] + LOG.warning("Board connection call timeout: " + str(details)) + os._exit(1) + + +class TimeoutError(Exception): + + def __init__(self, message, action): + # Call the base class constructor with the parameters it needs + super(TimeoutError, self).__init__(message) + + # Now for your custom code... + self.action = action + + +class timeout(object): + + def __init__(self, seconds=1, error_message='Timeout', action=None): + self.seconds = seconds + self.error_message = error_message + self.action = action + + def handle_timeout(self, signum, frame): + raise TimeoutError(self.error_message, self.action) + + def __enter__(self): + signal.signal(signal.SIGALRM, self.handle_timeout) + signal.alarm(self.seconds) + + def __exit__(self, type, value, traceback): + signal.alarm(0) + + +class timeoutRPC(object): + + def __init__(self, seconds=1, error_message='Timeout', action=None): + self.seconds = seconds + self.error_message = error_message + self.action = action + + def handle_timeout(self, signum, frame): + manageTimeout(self.error_message, self.action) + # LOG.warning("RPC timeout: " + str(self.error_message)) + # os._exit(1) + + def __enter__(self): + signal.signal(signal.SIGALRM, self.handle_timeout) + signal.alarm(self.seconds) + + def __exit__(self, type, value, traceback): + signal.alarm(0) diff --git a/iotronic_lightningrod/config.py b/iotronic_lightningrod/config.py new file mode 100644 index 0000000..85c56ce --- /dev/null +++ b/iotronic_lightningrod/config.py @@ -0,0 +1,28 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 os +import pkg_resources + +dist = pkg_resources.get_distribution(__package__) +entry_points_name = \ + os.path.join(dist.location, dist.egg_name()) + ".egg-info/entry_points.txt" + +# Iotronic python package folder +package_path = os.path.join(dist.location, __package__) + +# Iotronic home folder +iotronic_home = "/opt/stack4things/iotronic" diff --git a/iotronic_lightningrod/devices/Device.py b/iotronic_lightningrod/devices/Device.py new file mode 100644 index 0000000..df8abba --- /dev/null +++ b/iotronic_lightningrod/devices/Device.py @@ -0,0 +1,36 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +__author__ = "MDSLAB Team" + +import abc +import six + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class Device(object): + """Base class for each s4t Lightning-rod device. + + """ + + def __init__(self, device_type): + self.device_type = device_type + + def finalize(self): + pass diff --git a/iotronic_lightningrod/devices/__init__.py b/iotronic_lightningrod/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_lightningrod/devices/gpio/Gpio.py b/iotronic_lightningrod/devices/gpio/Gpio.py new file mode 100644 index 0000000..bd3b216 --- /dev/null +++ b/iotronic_lightningrod/devices/gpio/Gpio.py @@ -0,0 +1,45 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 abc +import six + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + +from iotronic_lightningrod.config import package_path + + +@six.add_metaclass(abc.ABCMeta) +class Gpio(object): + def __init__(self, name): + self.name = name + self.path = package_path + "/gpio/" + self.name + ".py" + + @abc.abstractmethod + def EnableGPIO(self): + """Enable reading and writing functionalities of the GPIO module + + :return: status of the operation (String) + """ + + @abc.abstractmethod + def DisableGPIO(self): + """Disable reading and writing functionalities of the GPIO module + + :return: status of the operation (String) + """ diff --git a/iotronic_lightningrod/devices/gpio/__init__.py b/iotronic_lightningrod/devices/gpio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_lightningrod/devices/gpio/server.py b/iotronic_lightningrod/devices/gpio/server.py new file mode 100644 index 0000000..60742d6 --- /dev/null +++ b/iotronic_lightningrod/devices/gpio/server.py @@ -0,0 +1,36 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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_log import log as logging + +from iotronic_lightningrod.devices.gpio import Gpio + +LOG = logging.getLogger(__name__) + + +class ServerGpio(Gpio.Gpio): + def __init__(self): + super(ServerGpio, self).__init__("server") + LOG.info("Server GPIO module importing...") + + # Enable GPIO + def EnableGPIO(self): + result = ' - GPIO not available for server device!' + LOG.info(result) + + def DisableGPIO(self): + result = ' - GPIO not available for server device!' + LOG.info(result) diff --git a/iotronic_lightningrod/devices/gpio/yun.py b/iotronic_lightningrod/devices/gpio/yun.py new file mode 100644 index 0000000..7998a46 --- /dev/null +++ b/iotronic_lightningrod/devices/gpio/yun.py @@ -0,0 +1,213 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 iotronic_lightningrod.devices.gpio import Gpio +import os +import time + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +i2c_path = "/sys/devices/mcuio/0:0.0/0:1.4/i2c-0" +device1_path = i2c_path + "/0-0060/iio:device1/" +device0_path = "/sys/bus/iio/devices/iio:device0/" + + +class YunGpio(Gpio.Gpio): + + def __init__(self): + super(YunGpio, self).__init__("yun") + + self.MAPPING = { + 'D8': '104', + 'D9': '105', + 'D10': '106', + 'D11': '107', + 'D5': '114', + 'D13': '115', + 'D3': '116', + 'D2': '117', + 'D4': '120', + 'D12': '122', + 'D6': '123'} + + # LOG.info("Arduino YUN gpio module importing...") + + def EnableGPIO(self): + """Enable GPIO (device0). + + """ + try: + + with open('/sys/bus/iio/devices/iio:device0/enable', 'a') as f: + f.write('1') + + result = " - GPIO enabled!\n" + LOG.info(result) + + except Exception as err: + LOG.error("Error enabling GPIO (device0): " + str(err)) + + def DisableGPIO(self): + """Disable GPIO (device0). + + """ + try: + with open('/sys/bus/iio/devices/iio:device0/enable', 'a') as f: + f.write('0') + + result = " - GPIO disabled!\n" + LOG.info(result) + except Exception as err: + LOG.error("Error disabling GPIO (device0): " + str(err)) + + def EnableI2c(self): + """Enable i2c device (device1). + + From ideino-linino-lib library: + Board.prototype.addI2c = function(name, driver, addr, bus) + board.addI2c('BAR', 'mpl3115', '0x60', 0): + - i2c_device.driver: mpl3115 + - i2c_device.addr: 0x60 + - i2c_device.name: BAR + - i2c_device.bus: 0 + + """ + + try: + + if os.path.exists('/sys/bus/i2c/devices/i2c-0/0-0060'): + result = " - I2C device already enabled!" + + else: + + with open('/sys/bus/i2c/devices/i2c-0/new_device', 'a') as f: + # 'echo '+i2c_device.driver+' '+i2c_device.addr+ ' + f.write('mpl3115 0x60') + result = " - I2C device enabled!" + + LOG.info(result) + + except Exception as err: + LOG.error("Error enabling I2C (device1): " + str(err)) + + def i2cRead(self, sensor): + """Read i2c raw value. + + sensor options: + - in_pressure_raw + - in_temp_raw + + :param sensor: name of the sensor connected to I2C port + :return: I2C raw value + + """ + try: + + with open(device1_path + "in_" + sensor + "_raw") as raw: + value = raw.read() + + except Exception as err: + LOG.error("Error reading I2C device: " + str(err)) + value = None + + return value + + def setPIN(self, DPIN, value): + """Function to set digital PIN value. + + :param DPIN: pin + :param value: value to set the pin + + """ + try: + with open('/sys/class/gpio/' + DPIN + '/value', 'a') as f: + f.write(value) + + except Exception as err: + LOG.error("Error setting PIN value: " + str(err)) + + def _setGPIOs(self, Dpin, direction, value): + """GPIO mapping on lininoIO + + ------------------------- + GPIO n. OUTPUT + 104 D8 + 105 D9 + 106 D10 + 107 D11 + 114 D5 + 115 D13 + 116 D3 + 117 D2 + 120 D4 + 122 D12 + 123 D6 + + """ + + try: + + with open('/sys/class/gpio/export', 'a') as f_export: + f_export.write(self.MAPPING[Dpin]) + + with open('/sys/class/gpio/' + Dpin + '/direction', 'a') as f_dir: + f_dir.write(direction) + + with open('/sys/class/gpio/' + Dpin + '/value', 'a') as f_value: + f_value.write(value) + + with open('/sys/class/gpio/' + Dpin + '/value') as f_value: + result = "PIN " + Dpin + " value " + f_value.read() + + except Exception as err: + LOG.error("Error setting GPIO value: " + str(err)) + result = None + + return result + + def _readVoltage(self, pin): + + try: + with open(device0_path + "in_voltage_" + pin + "_raw") as raw: + voltage = raw.read() + # print("VOLTAGE: " + voltage) + + except Exception as err: + LOG.error("Error reading voltage: " + str(err)) + voltage = None + + return voltage + + def blinkLed(self): + """LED: 13. There is a built-in LED connected to digital pin 13. + + When the pin has HIGH value, the LED is on, + when the pin has LOW value, it is off. + + """ + with open('/sys/class/gpio/export', 'a') as f: + f.write('115') + + with open('/sys/class/gpio/D13/direction', 'a') as f: + f.write('out') + + with open('/sys/class/gpio/D13/value', 'a') as f: + f.write('1') + + time.sleep(2) + + with open('/sys/class/gpio/D13/value', 'a') as f: + f.write('0') diff --git a/iotronic_lightningrod/devices/server.py b/iotronic_lightningrod/devices/server.py new file mode 100644 index 0000000..f44a808 --- /dev/null +++ b/iotronic_lightningrod/devices/server.py @@ -0,0 +1,55 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 inspect +from twisted.internet.defer import returnValue + +from iotronic_lightningrod.devices import Device +from iotronic_lightningrod.devices.gpio import server + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +def whoami(): + return inspect.stack()[1][3] + + +def makeNothing(): + pass + + +class System(Device.Device): + + def __init__(self): + super(System, self).__init__("server") + + server.ServerGpio().EnableGPIO() + + def finalize(self): + """Function called at the end of module loading (after RPC registration). + + :return: + + """ + pass + + def testRPC(self): + rpc_name = whoami() + LOG.info("RPC " + rpc_name + " CALLED...") + yield makeNothing() + result = " - " + rpc_name + " result: testRPC is working!!!\n" + LOG.info(result) + returnValue(result) diff --git a/iotronic_lightningrod/devices/yun.py b/iotronic_lightningrod/devices/yun.py new file mode 100644 index 0000000..80507d7 --- /dev/null +++ b/iotronic_lightningrod/devices/yun.py @@ -0,0 +1,72 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +# Linino references: http://wiki.linino.org/doku.php?id=wiki:lininoio_sysfs + +from twisted.internet.defer import returnValue + +from iotronic_lightningrod.devices import Device +from iotronic_lightningrod.devices.gpio import yun + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +class System(Device.Device): + + def __init__(self): + super(System, self).__init__("yun") + + self.gpio = yun.YunGpio() + + self.gpio.EnableGPIO() + + def finalize(self): + """Function called at the end of module loading (after RPC registration). + + :return: + + """ + pass + + def testLED(self): + LOG.info(" - testLED CALLED...") + + yield self.gpio.blinkLed() + + result = "testLED: LED blinking!\n" + LOG.info(result) + returnValue(result) + + def setGPIOs(self, Dpin, direction, value): + + LOG.info(" - setGPIOs CALLED... digital pin " + Dpin + + " (GPIO n. " + self.gpio.MAPPING[Dpin] + ")") + + result = yield self.gpio._setGPIOs(Dpin, direction, value) + LOG.info(result) + returnValue(result) + + def readVoltage(self, Apin): + """To read the voltage applied on the pin A0,A1,A2,A3,A4,A5 + + """ + LOG.info(" - readVoltage CALLED... reading pin " + Apin) + + voltage = self.gpio._readVoltage(Apin) + + result = yield "read voltage for " + Apin + " pin: " + voltage + LOG.info(result) + returnValue(result) diff --git a/iotronic_lightningrod/lightningrod.py b/iotronic_lightningrod/lightningrod.py new file mode 100644 index 0000000..c9998c8 --- /dev/null +++ b/iotronic_lightningrod/lightningrod.py @@ -0,0 +1,656 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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. + + +# Autobahn and Twisted imports +from autobahn.twisted import wamp +from autobahn.twisted.wamp import ApplicationSession +from autobahn.twisted import websocket +from autobahn.wamp import exception +from autobahn.wamp import types +from twisted.internet.defer import inlineCallbacks +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet import reactor + +# OSLO imports +from oslo_config import cfg +from oslo_log import log as logging + +# MODULES imports +import inspect +import os +import pkg_resources +import signal +import socket +from stevedore import extension +import sys + + +# IoTronic imports +from iotronic_lightningrod.Board import Board +from iotronic_lightningrod.common.exception import timeoutRPC +import iotronic_lightningrod.wampmessage as WM + + +# Global variables +LOG = logging.getLogger(__name__) +CONF = cfg.CONF +SESSION = None +global board +board = None +reconnection = False +RPC = {} +RPC_devices = {} + + +def moduleReloadInfo(session): + """This function is used in the reconnection stage to register + + again the RPCs of each module and for device. + + :param session: WAMP session object. + + """ + + LOG.info("Modules reloading after WAMP recovery...") + + try: + + # Register RPCs for each Lightning-rod module + for mod in RPC: + LOG.info("- Reloading module RPcs for " + str(mod)) + moduleWampRegister(session, RPC[mod]) + + # Register RPCs for the device + for dev in RPC_devices: + LOG.info("- Reloading device RPCs for " + str(dev)) + moduleWampRegister(session, RPC_devices[dev]) + + except Exception as err: + LOG.warning("Board modules reloading error: " + str(err)) + Bye() + + +def moduleWampRegister(session, meth_list): + """This function register for each module methods the relative RPC. + + :param session: + :param meth_list: + + """ + + if len(meth_list) == 2: + + LOG.info(" - No procedures to register!") + + else: + + for meth in meth_list: + # We don't considere the __init__ and finalize methods + if (meth[0] != "__init__") & (meth[0] != "finalize"): + rpc_addr = u'iotronic.' + board.uuid + '.' + meth[0] + session.register(inlineCallbacks(meth[1]), rpc_addr) + LOG.info(" --> " + str(meth[0])) + # LOG.info(" --> " + str(rpc_addr)) + + +def modulesLoader(session): + """Modules loader method thorugh stevedore libraries. + + :param session: + + """ + + LOG.info("Available modules: ") + + ep = [] + + for ep in pkg_resources.iter_entry_points(group='s4t.modules'): + LOG.info(" - " + str(ep)) + + if not ep: + + LOG.info("No modules available!") + sys.exit() + + else: + + modules = extension.ExtensionManager( + namespace='s4t.modules', + # invoke_on_load=True, + # invoke_args=(session,), + ) + + LOG.info('Modules to load:') + + for ext in modules.extensions: + + # LOG.debug(ext.name) + + if (ext.name == 'gpio') & (board.type == 'server'): + LOG.info('- GPIO module disabled for laptop devices') + + else: + mod = ext.plugin(board, session) + + # Methods list for each module + meth_list = inspect.getmembers(mod, predicate=inspect.ismethod) + + global RPC + RPC[mod.name] = meth_list + + if len(meth_list) == 2: + # there are at least two methods for each module: + # "__init__" and "finalize" + + LOG.info(" - No RPC to register for " + + str(ext.name) + " module!") + + else: + LOG.info(" - RPC list of " + str(mod.name) + ":") + moduleWampRegister(SESSION, meth_list) + + # Call the finalize procedure for each module + mod.finalize() + + LOG.info("Lightning-rod modules loaded.") + LOG.info("\n\nListening...") + + +@inlineCallbacks +def IotronicLogin(board, session, details): + """Function called to connect the board to Iotronic. + + The board: + 1. logs in to Iotronic + 2. loads the modules + + :param board: + :param session: + :param details: + + """ + + LOG.info("IoTronic Authentication:") + + global reconnection + + global SESSION + SESSION = session + + try: + + rpc = str(board.agent) + u'.stack4things.connection' + + with timeoutRPC(seconds=3, action=rpc): + res = yield session.call(rpc, + uuid=board.uuid, + session=details.session + ) + + w_msg = WM.deserialize(res) + + if w_msg.result == WM.SUCCESS: + + LOG.info(" - Access granted to Iotronic.") + + # LOADING BOARD MODULES + try: + + yield modulesLoader(session) + + except Exception as e: + LOG.warning("WARNING - Could not register procedures: " + + str(e)) + + # Reset flag to False + reconnection = False + + else: + LOG.error(" - Access denied to Iotronic.") + Bye() + + except exception.ApplicationError as e: + LOG.error(" - Iotronic Connection RPC error: " + str(e)) + # Iotronic is offline the board can not call + # the "stack4things.connection" RPC. + # The board will disconnect from WAMP agent and retry later. + reconnection = True + session.disconnect() + + except Exception as e: + LOG.warning("Iotronic board connection error: " + str(e)) + + +class WampFrontend(ApplicationSession): + """Function to manage the WAMP connection events. + + """ + + @inlineCallbacks + def onJoin(self, details): + """Execute the following procedures when the board connects to WAMP server. + + :param details: WAMP session details + + """ + + # LIGHTNING-ROD STATE: + # - REGISTRATION STATE: the first connection to Iotronic + # - FIRST CONNECTION: the board become operative after registration + # - LIGHTNING-ROD BOOT: the first connection to WAMP + # after Lightning-rod starting + # - WAMP RECOVERY: when the established WAMP connection fails + + global reconnection + + # reconnection flag is False when the board is: + # - LIGHTNING-ROD BOOT + # - REGISTRATION STATE + # - FIRST CONNECTION + # + # reconnection flag is True when the board is: + # - WAMP RECOVERY + + global SESSION + SESSION = self + + # LOG.debug(" - session: " + str(details)) + + board.session = self + board.session_id = details.session + + LOG.info(" - Joined in realm " + board.wamp_config['realm'] + ":") + LOG.info(" - WAMP Agent: " + str(board.agent)) + LOG.info(" - Session ID: " + str(details.session)) + + if reconnection is False: + + if board.uuid is None: + + ###################### + # REGISTRATION STATE # + ###################### + + # If in the LR configuration file there is not the Board UUID + # specified it means the board is a new one and it has to call + # IoTronic in order to complete the registration. + + try: + + LOG.info(" - Board needs to be registered to Iotronic.") + + rpc = u'stack4things.register' + + with timeoutRPC(seconds=3, action=rpc): + res = yield self.call( + rpc, + code=board.code, + session=details.session + ) + + w_msg = WM.deserialize(res) + + # LOG.info(" - Board registration result: \n" + + # json.loads(w_msg.message, indent=4)) + + if w_msg.result == WM.SUCCESS: + + LOG.info("Registration authorized by Iotronic:\n" + + str(w_msg.message)) + + # the 'message' field contains + # the board configuration to load + board.setConf(w_msg.message) + + # We need to disconnect the client from the + # registration-agent inorder to reconnect + # to the WAMP agent assigned by Iotronic + # at the provisioning stage + LOG.info("\n\nDisconnecting from Registration Agent " + "to load new settings...\n\n") + self.disconnect() + + else: + LOG.error("Registration denied by Iotronic: " + + str(w_msg.message)) + Bye() + + except exception.ApplicationError as e: + LOG.error("IoTronic registration error: " + str(e)) + # Iotronic is offline the board can not call the + # "stack4things.connection" RPC. + # The board will disconnect from WAMP agent and retry later + + # TO ACTIVE BOOT CONNECTION RECOVERY MODE + reconnection = True + self.disconnect() + + except Exception as e: + LOG.warning(" - Board registration call error: " + str(e)) + Bye() + + else: + + if board.status == "registered": + #################### + # FIRST CONNECTION # + #################### + + # In this case we manage the first reconnection + # after the registration stage: + # Lightining-rod sets its status to "operative" + # completing the provisioning and configuration stage. + LOG.info("\n\n\nBoard is becoming operative...\n\n\n") + board.updateStatus("operative") + board.loadSettings() + IotronicLogin(board, self, details) + + elif board.status == "operative": + ###################### + # LIGHTNING-ROD BOOT # + ###################### + + # After join to WAMP agent, Lightning-rod will: + # - authenticate to Iotronic + # - load the enabled modules + + # The board will keep at this tage until it will succeed + # to connect to Iotronic. + IotronicLogin(board, self, details) + + else: + LOG.error("Wrong board status '" + board.status + "'.") + Bye() + + else: + + ################# + # WAMP RECOVERY # + ################# + + LOG.info("IoTronic connection recovery:") + + try: + + rpc = str(board.agent) + u'.stack4things.connection' + + with timeoutRPC(seconds=3, action=rpc): + res = yield self.call( + rpc, + uuid=board.uuid, + session=details.session + ) + + w_msg = WM.deserialize(res) + + if w_msg.result == WM.SUCCESS: + + LOG.info(" - Access granted to Iotronic.") + + # LOADING BOARD MODULES + # If the board is in WAMP connection recovery state + # we need to register again the RPCs of each module + try: + + yield moduleReloadInfo(self) + + # Reset flag to False + reconnection = False + + LOG.info("WAMP Session Recovered!") + + LOG.info("\n\nListening...\n\n") + + except Exception as e: + LOG.warning("WARNING - Could not register procedures: " + + str(e)) + Bye() + + else: + LOG.error("Access to IoTronic denied: " + + str(w_msg.message)) + Bye() + + except exception.ApplicationError as e: + LOG.error("IoTronic connection error: " + str(e)) + # Iotronic is offline the board can not call + # the "stack4things.connection" RPC. + # The board will disconnect from WAMP agent and retry later + + # TO ACTIVE WAMP CONNECTION RECOVERY MODE + reconnection = False + self.disconnect() + + except Exception as e: + LOG.warning("Board connection error after WAMP recovery: " + + str(e)) + Bye() + + @inlineCallbacks + def onLeave(self, details): + LOG.warning('WAMP Session Left: ' + str(details)) + + +class WampClientFactory(websocket.WampWebSocketClientFactory, + ReconnectingClientFactory): + + def clientConnectionFailed(self, connector, reason): + """Procedure triggered on WAMP connection failure. + + :param connector: WAMP connector object + :param reason: WAMP connection failure reason + + """ + LOG.warning("WAMP Connection Failed: Crossbar server unreachable.") + ReconnectingClientFactory.clientConnectionFailed( + self, + connector, + reason + ) + + def clientConnectionLost(self, connector, reason): + """Procedure triggered on WAMP connection lost. + + :param connector: WAMP connector object + :param reason: WAMP connection failure reason + + """ + + LOG.warning("WAMP Connection Lost.") + + global reconnection + + LOG.warning("WAMP status: board = " + str(board.status) + + " - reconnection = " + str(reconnection)) + + if board.status == "operative" and reconnection is False: + + ################# + # WAMP RECOVERY # + ################# + + # we need to recover wamp session and + # we set reconnection flag to True in order to activate + # the RPCs module registration procedure for each module + + reconnection = True + + LOG.info("Reconnecting to " + str(connector.getDestination().host) + + ":" + str(connector.getDestination().port)) + + ReconnectingClientFactory.clientConnectionLost( + self, + connector, + reason + ) + + elif board.status == "operative" and reconnection is True: + + ###################### + # LIGHTNING-ROD BOOT # + ###################### + + # At this stage if the reconnection flag was set to True + # it means that we forced the reconnection procedure + # because of the board is not able to connect to IoTronic + # calling "stack4things.connection" RPC... + # it means IoTronic is offline! + + # We need to reset the recconnection flag to False in order to + # do not enter in RPCs module registration procedure... + # At this stage the board tries to reconnect to + # IoTronic until it will come online again. + reconnection = False + + LOG.info("Connecting to " + str(connector.getDestination().host) + + ":" + str(connector.getDestination().port)) + + ReconnectingClientFactory.clientConnectionLost( + self, + connector, + reason + ) + + elif (board.status == "registered"): + ###################### + # REGISTRATION STATE # + ###################### + + # LR was disconnected from Registration Agent + # in order to connect it to the assigned WAMP Agent. + + LOG.debug("\n\nReconnecting after registration...\n\n") + + # LR load the new configuration and gets the new WAMP Agent + board.loadSettings() + + # LR has to connect to the assigned WAMP Agent + wampConnect(board.wamp_config) + + else: + LOG.error("Reconnection wrong status!") + + +def wampConnect(wamp_conf): + """WAMP connection procedure. + + :param wamp_conf: WAMP configuration from settings.json file + + """ + + LOG.info("WAMP connection precedures:") + + try: + + component_config = types.ComponentConfig( + realm=unicode(wamp_conf['realm']) + ) + session_factory = wamp.ApplicationSessionFactory( + config=component_config + ) + session_factory.session = WampFrontend + + transport_factory = WampClientFactory( + session_factory, + url=wamp_conf['url'] + ) + transport_factory.autoPingInterval = 5 + transport_factory.autoPingTimeout = 5 + + connector = websocket.connectWS(transport_factory) + + try: + + addr = str(connector.getDestination().host) + socket.inet_pton(socket.AF_INET, addr) + LOG.info(" - establishing connection to " + + board.agent + ": " + addr) + + except socket.error as err: + LOG.error(" - IP address validation error: " + str(err)) + Bye() + + except Exception as err: + LOG.error(" - URI validation error: " + str(err)) + Bye() + + +class WampManager(object): + """WAMP Manager: through this LR manages the connection to Crossbar server. + + """ + + def __init__(self, wamp_conf): + # Connection to Crossbar server. + wampConnect(wamp_conf) + + def start(self): + LOG.info(" - starting Lightning-rod WAMP server...") + reactor.run() + + def stop(self): + LOG.info("Stopping WAMP agent server...") + reactor.stop() + LOG.info("WAMP server stopped!") + + +def Bye(): + LOG.info("Bye!") + os._exit(1) + + +def LogoLR(): + LOG.info('') + LOG.info('##############################') + LOG.info(' Stack4Things Lightning-rod') + LOG.info('##############################') + + +class LightningRod(object): + + def __init__(self): + + logging.register_options(CONF) + DOMAIN = "s4t-lightning-rod" + CONF(project='iotronic') + logging.setup(CONF, DOMAIN) + + signal.signal(signal.SIGINT, self.stop_handler) + + LogoLR() + + global board + board = Board() + + LOG.info('Info:') + LOG.info(' - Logs: /var/log/s4t-lightning-rod.log') + current_time = board.getTimestamp() + LOG.info(" - Current time: " + current_time) + + self.w = WampManager(board.wamp_config) + + self.w.start() + + def stop_handler(self, signum, frame): + LOG.info("LR is shutting down...") + + self.w.stop() + + Bye() + + +def main(): + LightningRod() diff --git a/iotronic_lightningrod/modules/Module.py b/iotronic_lightningrod/modules/Module.py new file mode 100644 index 0000000..bd59c44 --- /dev/null +++ b/iotronic_lightningrod/modules/Module.py @@ -0,0 +1,42 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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. + +__author__ = "MDSLAB Team" + +import abc +import six + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class Module(object): + """Base class for each s4t Lightning-rod module. + + """ + + # __metaclass__ = abc.ABCMeta + + def __init__(self, name, board): + + self.name = name + self.board = board + + LOG.info("Loading module " + self.name + "...") + + @abc.abstractmethod + def finalize(self): + pass diff --git a/iotronic_lightningrod/modules/__init__.py b/iotronic_lightningrod/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_lightningrod/modules/device_manager.py b/iotronic_lightningrod/modules/device_manager.py new file mode 100644 index 0000000..6e46740 --- /dev/null +++ b/iotronic_lightningrod/modules/device_manager.py @@ -0,0 +1,81 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 imp +import inspect +import os +from twisted.internet.defer import inlineCallbacks + +from iotronic_lightningrod.config import package_path +from iotronic_lightningrod.lightningrod import RPC_devices +from iotronic_lightningrod.lightningrod import SESSION +from iotronic_lightningrod.modules import Module + + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +def deviceWampRegister(dev_meth_list, board): + + LOG.info(" - " + str(board.type).capitalize() + + " device registering RPCs:") + + for meth in dev_meth_list: + + if (meth[0] != "__init__") & (meth[0] != "finalize"): + # LOG.info(" - " + str(meth[0])) + rpc_addr = u'iotronic.' + board.uuid + '.' + meth[0] + # LOG.debug(" --> " + str(rpc_addr)) + SESSION.register(inlineCallbacks(meth[1]), rpc_addr) + + LOG.info(" --> " + str(meth[0]) + " registered!") + + +class DeviceManager(Module.Module): + + def __init__(self, board, session): + + # Module declaration + super(DeviceManager, self).__init__("DeviceManager", board) + + device_type = board.type + + path = package_path + "/devices/" + device_type + ".py" + + if os.path.exists(path): + + device_module = imp.load_source("device", path) + + LOG.info(" - Device " + device_type + " module imported!") + + device = device_module.System() + + dev_meth_list = inspect.getmembers( + device, + predicate=inspect.ismethod + ) + + RPC_devices[device_type] = dev_meth_list + + deviceWampRegister(dev_meth_list, board) + + board.device = device + + else: + LOG.warning("Device " + device_type + " not supported!") + + def finalize(self): + pass diff --git a/iotronic_lightningrod/modules/plugin_manager.py b/iotronic_lightningrod/modules/plugin_manager.py new file mode 100644 index 0000000..e037593 --- /dev/null +++ b/iotronic_lightningrod/modules/plugin_manager.py @@ -0,0 +1,817 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 __future__ import absolute_import + +from datetime import datetime +import imp +import inspect +import json +import os +from Queue import Queue +import shutil +import time +from twisted.internet.defer import returnValue + +from iotronic_lightningrod.config import iotronic_home +from iotronic_lightningrod.modules import Module +from iotronic_lightningrod.plugins import PluginSerializer +import iotronic_lightningrod.wampmessage as WM + + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +PLUGINS_THRS = {} +PLUGINS_CONF_FILE = iotronic_home + "/plugins.json" + + +def getFuncName(): + return inspect.stack()[1][3] + + +def createPluginsConf(): + """Create plugins.json file if it does not exist. + + """ + if not os.path.exists(PLUGINS_CONF_FILE): + LOG.debug("plugins.json does not exist: creating...") + plugins_conf = {'plugins': {}} + with open(PLUGINS_CONF_FILE, 'w') as f: + json.dump(plugins_conf, f, indent=4) + + +def loadPluginsConf(): + """Load plugins.json JSON configuration. + + :return: JSON Plugins configuration + + """ + + try: + + with open(PLUGINS_CONF_FILE) as settings: + plugins_conf = json.load(settings) + + except Exception as err: + LOG.error("Parsing error in " + PLUGINS_CONF_FILE + ": " + str(err)) + plugins_conf = None + + return plugins_conf + + +def getEnabledPlugins(): + """This function gets the list of all asynchronous plugins. + + We considered only those plugins with 'callable' flag set to False + and 'onboot' flag set to True. + + :return: enabledPlugins List + + """ + enabledPlugins = [] + plugins_conf = loadPluginsConf() + + for plugin in plugins_conf['plugins']: + + if plugins_conf['plugins'][plugin]['callable'] is False: + + if plugins_conf['plugins'][plugin]['onboot'] is True: + + if plugins_conf['plugins'][plugin]['status'] == "operative": + enabledPlugins.append(plugin) + + if len(enabledPlugins) != 0: + LOG.info(" - Enabled plugins list: " + str(enabledPlugins)) + + return enabledPlugins + + +def makeNothing(): + """Sandbox function. + + """ + pass + + +def RebootOnBootPlugins(): + """Reboot at boot each enabled asynchronous plugin + + :return: + + """ + + rpc_name = getFuncName() + LOG.info("Rebooting enabled plugins:") + + enabledPlugins = getEnabledPlugins() + + if enabledPlugins.__len__() == 0: + + message = "No plugin to reboot!" + LOG.info(" - " + message) + + else: + + for plugin_uuid in enabledPlugins: + + plugins_conf = loadPluginsConf() + plugin_name = plugins_conf['plugins'][plugin_uuid]['name'] + # plugin_status = plugins_conf['plugins'][plugin_uuid]['status'] + + try: + + if (plugin_uuid in PLUGINS_THRS) and ( + PLUGINS_THRS[plugin_uuid].isAlive() + ): + + LOG.warning(" - Plugin " + + plugin_uuid + " already started!") + + else: + + LOG.info(" - Rebooting plugin " + plugin_uuid) + + plugin_home = iotronic_home + "/plugins/" + plugin_uuid + plugin_filename = plugin_home + "/" + plugin_uuid + ".py" + plugin_params_file = \ + plugin_home + "/" + plugin_uuid + ".json" + + if os.path.exists(plugin_filename): + + task = imp.load_source("plugin", plugin_filename) + + if os.path.exists(plugin_params_file): + + with open(plugin_params_file) as conf: + plugin_params = json.load(conf) + + worker = task.Worker( + plugin_uuid, + plugin_name, + q_result=None, + params=plugin_params + ) + + PLUGINS_THRS[plugin_uuid] = worker + LOG.info(" - Starting plugin " + str(worker)) + + worker.start() + + else: + message = "ERROR " \ + + plugin_params_file + " does not exist!" + + LOG.error(" - " + + worker.complete(rpc_name, message)) + + else: + message = "ERROR " \ + + plugin_filename + " does not exist!" + + LOG.error(" - " + worker.complete(rpc_name, message)) + + message = "rebooted!" + + LOG.info(" - " + worker.complete(rpc_name, message)) + + except Exception as err: + message = "Error rebooting plugin " \ + + plugin_uuid + ": " + str(err) + LOG.error(" - " + message) + + +class PluginManager(Module.Module): + + """Plugin module to manage board plugins. + + """ + + def __init__(self, board, session): + """Init function for PluginManager module. + + :param board: + :param session: + + """ + + # Module declaration + super(PluginManager, self).__init__("PluginManager", board) + + # Creation of plugins.json configuration file + createPluginsConf() + + def finalize(self): + """Function called at the end of module loading. + + This function in this module reloads + the enabled (asynchronous) plugins at boot. + + """ + + # Reboot boot enabled plugins + RebootOnBootPlugins() + + def PluginInject(self, plugin, onboot): + """Plugin injection procedure into the board: + + 1. get Plugin files + 2. deserialize files + 3. store files + + :param plugin: + :param onboot: + :return: + + """ + + rpc_name = getFuncName() + + try: + + plugin_uuid = plugin['uuid'] + plugin_name = plugin['name'] + code = plugin['code'] + callable = plugin['callable'] + + LOG.info("RPC " + rpc_name + " for plugin '" + + plugin_name + "' (" + plugin_uuid + ")") + + # Deserialize the plugin code received + ser = PluginSerializer.ObjectSerializer() + loaded = ser.deserialize_entity(code) + # LOG.debug("- plugin loaded code:\n" + loaded) + + plugin_path = iotronic_home + "/plugins/" + plugin_uuid + "/" + plugin_filename = plugin_path + plugin_uuid + ".py" + + # Plugin folder creation if does not exist + if not os.path.exists(plugin_path): + os.makedirs(plugin_path) + + # Plugin code file creation + with open(plugin_filename, "w") as pluginfile: + pluginfile.write(loaded) + + # Load plugins.json configuration file + plugins_conf = loadPluginsConf() + + # LOG.debug("Plugin setup:\n" + # + json.dumps(plugin, indent=4, sort_keys=True)) + + # Save plugin settings in plugins.json + if plugin_uuid not in plugins_conf['plugins']: + + # It is a new plugin + plugins_conf['plugins'][plugin_uuid] = {} + plugins_conf['plugins'][plugin_uuid]['name'] = plugin_name + plugins_conf['plugins'][plugin_uuid]['onboot'] = onboot + plugins_conf['plugins'][plugin_uuid]['callable'] = callable + plugins_conf['plugins'][plugin_uuid]['injected_at'] = \ + datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f') + plugins_conf['plugins'][plugin_uuid]['updated_at'] = "" + plugins_conf['plugins'][plugin_uuid]['status'] = "injected" + + LOG.info("Plugin " + plugin_name + " created!") + message = rpc_name + " result: INJECTED" + + else: + # The plugin was already injected and we are updating it + plugins_conf['plugins'][plugin_uuid]['name'] = plugin_name + plugins_conf['plugins'][plugin_uuid]['onboot'] = onboot + plugins_conf['plugins'][plugin_uuid]['callable'] = callable + plugins_conf['plugins'][plugin_uuid]['updated_at'] = \ + datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f') + plugins_conf['plugins'][plugin_uuid]['status'] = "updated" + + LOG.info("Plugin " + plugin_name + + " (" + str(plugin_uuid) + ") updated!") + message = rpc_name + " result: UPDATED" + + LOG.info("Plugin setup:\n" + json.dumps( + plugins_conf['plugins'][plugin_uuid], + indent=4, + sort_keys=True + )) + + # Apply the changes to plugins.json + with open(PLUGINS_CONF_FILE, 'w') as f: + json.dump(plugins_conf, f, indent=4) + + LOG.info(" - " + message) + w_msg = yield WM.WampSuccess(message) + + except Exception as err: + message = "Plugin injection error: " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + returnValue(w_msg.serialize()) + + def PluginStart(self, plugin_uuid, parameters=None): + """To start an asynchronous plugin; + + the plugin will run until the PluginStop is called. + + :param plugin_uuid: + :param parameters: + :return: return a response to RPC request + + """ + + try: + + rpc_name = getFuncName() + LOG.info("RPC " + rpc_name + " called for '" + + plugin_uuid + "' plugin:") + + plugins_conf = loadPluginsConf() + + if plugin_uuid in plugins_conf['plugins']: + + plugin_name = plugins_conf['plugins'][plugin_uuid]['name'] + + # Check if the plugin is already running + if (plugin_uuid in PLUGINS_THRS) and ( + PLUGINS_THRS[plugin_uuid].isAlive() + ): + + message = "ALREADY STARTED!" + LOG.warning(" - Plugin " + + plugin_uuid + " already started!") + w_msg = yield WM.WampError(message) + + else: + + plugin_home = \ + iotronic_home + "/plugins/" + plugin_uuid + plugin_filename = \ + plugin_home + "/" + plugin_uuid + ".py" + plugin_params_file = \ + plugin_home + "/" + plugin_uuid + ".json" + + # Import plugin (as python module) + if os.path.exists(plugin_filename): + + task = imp.load_source("plugin", plugin_filename) + + LOG.info(" - Plugin '" + plugin_uuid + "' imported!") + + # Store input parameters of the plugin + if parameters is not None: + + with open(plugin_params_file, 'w') as f: + json.dump(parameters, f, indent=4) + + with open(plugin_params_file) as conf: + plugin_params = json.load(conf) + + LOG.info(" - plugin with parameters:") + LOG.info(" " + str(plugin_params)) + + else: + plugin_params = None + + worker = task.Worker( + plugin_uuid, + plugin_name, + params=plugin_params + ) + + PLUGINS_THRS[plugin_uuid] = worker + LOG.debug(" - Starting plugin " + str(worker)) + + worker.start() + + # Apply the changes to plugins.json + with open(PLUGINS_CONF_FILE, 'w') as f: + plugins_conf['plugins'][plugin_uuid]['status'] = \ + 'operative' + json.dump(plugins_conf, f, indent=4) + + response = "STARTED" + LOG.info(" - " + worker.complete(rpc_name, response)) + w_msg = yield WM.WampSuccess(response) + + else: + message = \ + rpc_name + " - ERROR " \ + + plugin_filename + " does not exist!" + LOG.error(" - " + message) + w_msg = yield WM.WampError(message) + + else: + message = "Plugin " + plugin_uuid \ + + " does not exist in this board!" + LOG.warning(" - " + message) + w_msg = yield WM.WampError(message) + + except Exception as err: + message = \ + rpc_name + " - ERROR - plugin (" + plugin_uuid + ") - " \ + + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + returnValue(w_msg.serialize()) + + def PluginStop(self, plugin_uuid, parameters=None): + """To stop an asynchronous plugin + + :param plugin_uuid: ID of plufin to stop + :param parameters: JSON OPTIONAL stop parameters; 'delay' in seconds + :return: return a response to RPC request + + """ + rpc_name = getFuncName() + LOG.info("RPC " + rpc_name + " CALLED for '" + + plugin_uuid + "' plugin:") + + if parameters is not None: + LOG.info(" - " + rpc_name + " parameters: " + str(parameters)) + if 'delay' in parameters: + delay = parameters['delay'] + LOG.info(" --> stop delay: " + str(delay)) + + try: + + if plugin_uuid in PLUGINS_THRS: + + worker = PLUGINS_THRS[plugin_uuid] + LOG.debug(" - Stopping plugin " + str(worker)) + + if worker.isAlive(): + + if 'delay' in parameters: + time.sleep(delay) + + yield worker.stop() + + del PLUGINS_THRS[plugin_uuid] + + message = "STOPPED" + LOG.info(" - " + worker.complete(rpc_name, message)) + w_msg = yield WM.WampSuccess(message) + + else: + message = \ + rpc_name \ + + " - ERROR - plugin (" + plugin_uuid \ + + ") is instantiated but is not running anymore!" + LOG.error(" - " + message) + w_msg = yield WM.WampError(message) + + else: + message = \ + rpc_name + " - WARNING " \ + + plugin_uuid + " is not running!" + LOG.warning(" - " + message) + w_msg = yield WM.WampWarning(message) + + except Exception as err: + message = \ + rpc_name \ + + " - ERROR - plugin (" + plugin_uuid + ") - " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + returnValue(w_msg.serialize()) + + def PluginCall(self, plugin_uuid, parameters=None): + """To execute a synchronous plugin into the board + + :param plugin_uuid: + :param parameters: + :return: return a response to RPC request + + """ + + rpc_name = getFuncName() + LOG.info("RPC " + rpc_name + " CALLED for " + plugin_uuid + " plugin:") + + try: + + if (plugin_uuid in PLUGINS_THRS) and ( + PLUGINS_THRS[plugin_uuid].isAlive() + ): + + message = "Plugin " + plugin_uuid + " already started!" + LOG.warning(" - " + message) + w_msg = yield WM.WampWarning(message) + + else: + + plugin_home = iotronic_home + "/plugins/" + plugin_uuid + plugin_filename = plugin_home + "/" + plugin_uuid + ".py" + plugin_params_file = plugin_home + "/" + plugin_uuid + ".json" + + plugins_conf = loadPluginsConf() + plugin_name = plugins_conf['plugins'][plugin_uuid]['name'] + + # Import plugin (as python module) + if os.path.exists(plugin_filename): + + try: + + task = imp.load_source("plugin", plugin_filename) + + LOG.info(" - Plugin " + plugin_uuid + " imported!") + + q_result = Queue() + + except Exception as err: + message = "Error importing plugin " \ + + plugin_filename + ": " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + try: + + # Store input parameters of the plugin + if parameters is not None: + with open(plugin_params_file, 'w') as f: + json.dump(parameters, f, indent=4) + + with open(plugin_params_file) as conf: + plugin_params = json.load(conf) + + LOG.info(" - Plugin configuration:\n" + + str(plugin_params)) + + else: + plugin_params = None + + worker = task.Worker( + plugin_uuid, + plugin_name, + q_result=q_result, + params=plugin_params + ) + + PLUGINS_THRS[plugin_uuid] = worker + LOG.debug(" - Executing plugin " + str(worker)) + + worker.start() + + while q_result.empty(): + pass + + response = q_result.get() + + LOG.info(" - " + worker.complete(rpc_name, response)) + w_msg = yield WM.WampSuccess(response) + + except Exception as err: + message = "Error spawning plugin " \ + + plugin_filename + ": " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + else: + message = \ + rpc_name \ + + " - ERROR " + plugin_filename + " does not exist!" + LOG.error(" - " + message) + w_msg = yield WM.WampError(message) + + except Exception as err: + message = \ + rpc_name \ + + " - ERROR - plugin (" + plugin_uuid + ") - " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + returnValue(w_msg.serialize()) + + def PluginRemove(self, plugin_uuid): + """To remove a plugin from the board + + :param plugin_uuid: + :return: return a response to RPC request + + """ + + rpc_name = getFuncName() + + LOG.info("RPC " + rpc_name + " for plugin " + plugin_uuid) + + plugin_path = iotronic_home + "/plugins/" + plugin_uuid + "/" + + if os.path.exists(plugin_path) is False \ + or os.path.exists(PLUGINS_CONF_FILE) is False: + + message = "Plugin paths or files do not exist!" + LOG.error(message) + w_msg = yield WM.WampError(message) + returnValue(w_msg.serialize()) + + else: + + LOG.info(" - Removing plugin...") + + try: + + try: + + shutil.rmtree( + plugin_path, + ignore_errors=False, + onerror=None + ) + + except Exception as err: + message = "Removing plugin's files error in " \ + + plugin_path + ": " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + # Remove from plugins.json file its configuration + try: + + plugins_conf = loadPluginsConf() + + if plugin_uuid in plugins_conf['plugins']: + + plugin_name = \ + plugins_conf['plugins'][plugin_uuid]['name'] + + del plugins_conf['plugins'][plugin_uuid] + + with open(PLUGINS_CONF_FILE, 'w') as f: + json.dump(plugins_conf, f, indent=4) + + if plugin_uuid in PLUGINS_THRS: + worker = PLUGINS_THRS[plugin_uuid] + if worker.isAlive(): + LOG.info(" - Plugin " + + plugin_name + " is running...") + worker.stop() + LOG.info(" ...stopped!") + + del PLUGINS_THRS[plugin_uuid] + + message = "PluginRemove result: " \ + + plugin_uuid + " removed!" + LOG.info(" - " + message) + + else: + message = "PluginRemove result: " \ + + plugin_uuid + " already removed!" + LOG.warning(" - " + message) + + w_msg = yield WM.WampSuccess(message) + returnValue(w_msg.serialize()) + + except Exception as err: + message = "Updating plugins.json error: " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + except Exception as err: + message = "Plugin removing error: {0}".format(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + def PluginReboot(self, plugin_uuid): + """To reboot an asynchronous plugin (callable = false) into the board. + + :return: return a response to RPC request + + """ + + rpc_name = getFuncName() + + LOG.info("RPC " + rpc_name + " CALLED for '" + + plugin_uuid + "' plugin:") + + try: + + plugin_home = iotronic_home + "/plugins/" + plugin_uuid + plugin_filename = plugin_home + "/" + plugin_uuid + ".py" + plugin_params_file = plugin_home + "/" + plugin_uuid + ".json" + + plugins_conf = loadPluginsConf() + plugin_name = plugins_conf['plugins'][plugin_uuid]['name'] + callable = plugins_conf['plugins'][plugin_uuid]['callable'] + + if callable is False: + + if plugin_uuid in PLUGINS_THRS: + + worker = PLUGINS_THRS[plugin_uuid] + + if worker.isAlive(): + # STOP PLUGIN------------------------------------------ + LOG.info(" - Thread " + + plugin_uuid + " is running, stopping...") + LOG.debug(" - Stopping plugin " + str(worker)) + worker.stop() + + # Remove from plugin thread list + del PLUGINS_THRS[plugin_uuid] + + # START PLUGIN------------------------------------------------- + if os.path.exists(plugin_filename): + + # Import plugin python module + task = imp.load_source("plugin", plugin_filename) + + if os.path.exists(plugin_params_file): + + with open(plugin_params_file) as conf: + plugin_params = json.load(conf) + + else: + plugin_params = None + + worker = task.Worker( + plugin_uuid, + plugin_name, + params=plugin_params + ) + + PLUGINS_THRS[plugin_uuid] = worker + LOG.info(" - Starting plugin " + str(worker)) + + worker.start() + + message = "REBOOTED" + LOG.info(" - " + worker.complete(rpc_name, message)) + w_msg = yield WM.WampSuccess(message) + + else: + message = "ERROR '" + plugin_filename + "' does not exist!" + LOG.error(" - " + message) + w_msg = yield WM.WampError(message) + + except Exception as err: + message = "Error rebooting plugin '" \ + + plugin_uuid + "': " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + returnValue(w_msg.serialize()) + + def PluginStatus(self, plugin_uuid): + """Check status thread plugin + + :param plugin_uuid: + :return: + + """ + + rpc_name = getFuncName() + LOG.info("RPC " + rpc_name + " CALLED for '" + + plugin_uuid + "' plugin:") + + try: + + if plugin_uuid in PLUGINS_THRS: + + worker = PLUGINS_THRS[plugin_uuid] + + if worker.isAlive(): + result = "ALIVE" + else: + result = "DEAD" + + LOG.info(" - " + worker.complete(rpc_name, result)) + w_msg = yield WM.WampSuccess(result) + + else: + result = "DEAD" + LOG.info(" - " + rpc_name + " result for " + + plugin_uuid + ": " + result) + w_msg = yield WM.WampSuccess(result) + + except Exception as err: + message = \ + rpc_name \ + + " - ERROR - plugin (" + plugin_uuid + ") - " + str(err) + LOG.error(" - " + message) + w_msg = yield WM.WampError(str(err)) + returnValue(w_msg.serialize()) + + returnValue(w_msg.serialize()) diff --git a/iotronic_lightningrod/modules/test.py b/iotronic_lightningrod/modules/test.py new file mode 100644 index 0000000..4986ab7 --- /dev/null +++ b/iotronic_lightningrod/modules/test.py @@ -0,0 +1,42 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 autobahn.twisted.util import sleep +from iotronic_lightningrod.modules import Module +from twisted.internet.defer import returnValue + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +class Test(Module.Module): + + def __init__(self, board): + + super(Test, self).__init__("Test", board) + + def test_function(self): + import random + s = random.uniform(0.5, 1.5) + yield sleep(s) + result = "DEVICE test result: TEST!" + LOG.info(result) + returnValue(result) + + def add(self, x, y): + c = yield x + y + LOG.info("DEVICE add result: " + str(c)) + returnValue(c) diff --git a/iotronic_lightningrod/modules/utils.py b/iotronic_lightningrod/modules/utils.py new file mode 100644 index 0000000..3448ebc --- /dev/null +++ b/iotronic_lightningrod/modules/utils.py @@ -0,0 +1,119 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 autobahn.twisted.util import sleep +from iotronic_lightningrod.config import entry_points_name +from iotronic_lightningrod.modules import Module +import pkg_resources +from six import moves +from stevedore import extension +import sys +from twisted.internet.defer import returnValue + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + +from iotronic_lightningrod.lightningrod import SESSION + + +def refresh_stevedore(namespace=None): + """Trigger reload of entry points. + + Useful to have dynamic loading/unloading of stevedore modules. + """ + # NOTE(sheeprine): pkg_resources doesn't support reload on python3 due to + # defining basestring which is still there on reload hence executing + # python2 related code. + try: + del sys.modules['pkg_resources'].basestring + except AttributeError: + # python2, do nothing + pass + # Force working_set reload + moves.reload_module(sys.modules['pkg_resources']) + # Clear stevedore cache + cache = extension.ExtensionManager.ENTRY_POINT_CACHE + if namespace: + if namespace in cache: + del cache[namespace] + else: + cache.clear() + + +class Utility(Module.Module): + + def __init__(self, board, session): + super(Utility, self).__init__("Utility", board) + + def finalize(self): + pass + + def hello(self, client_name, message): + import random + s = random.uniform(0.5, 3.0) + yield sleep(s) + result = "Hello by board to Conductor " + client_name + \ + " that said me " + message + " - Time: " + '%.2f' % s + LOG.info("DEVICE hello result: " + str(result)) + + returnValue(result) + + def plug_and_play(self, new_module, new_class): + LOG.info("LR modules loaded:\n\t" + new_module) + + # Updating entry_points + with open(entry_points_name, 'a') as entry_points: + entry_points.write( + new_module + + '= iotronic_lightningrod.modules.' + new_module + ':' + + new_class + ) + + # Reload entry_points + refresh_stevedore('s4t.modules') + LOG.info("New entry_points loaded!") + + # Reading updated entry_points + named_objects = {} + for ep in pkg_resources.iter_entry_points(group='s4t.modules'): + named_objects.update({ep.name: ep.load()}) + + yield named_objects + + SESSION.disconnect() + + returnValue(str(named_objects)) + + def changeConf(self, conf): + + yield self.board.getConf(conf) + + self.board.setUpdateTime() + + result = "Board configuration changed!" + LOG.info("PROVISIONING RESULT: " + str(result)) + + returnValue(result) + + def destroyNode(self, conf): + + yield self.board.setConf(conf) + + result = "Board configuration cleaned!" + LOG.info("DESTROY RESULT: " + str(result)) + + returnValue(result) diff --git a/iotronic_lightningrod/modules/vfs_library.py b/iotronic_lightningrod/modules/vfs_library.py new file mode 100644 index 0000000..a74c711 --- /dev/null +++ b/iotronic_lightningrod/modules/vfs_library.py @@ -0,0 +1,162 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 errno +from fuse import FuseOSError +import os + + +# Logging conf +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +class FuseLib(object): + def __init__(self, mountSource): + self.mountSource = mountSource + + def _full_path(self, partial): + if partial.startswith("/"): + partial = partial[1:] + path = os.path.join(self.mountSource, partial) + print(path) + return path + + # Filesystem methods + # ================== + + def access(self, path, mode): + full_path = self._full_path(path) + if not os.access(full_path, mode): + raise FuseOSError(errno.EACCES) + + def chmod(self, path, mode): + full_path = self._full_path(path) + return os.chmod(full_path, mode) + + def chown(self, path, uid, gid): + full_path = self._full_path(path) + return os.chown(full_path, uid, gid) + + def getattr(self, path, fh=None): + full_path = self._full_path(path) + st = os.lstat(full_path) + attr = dict((key, getattr(st, key)) + for key in ( + 'st_atime', + 'st_ctime', + 'st_gid', + 'st_mode', + 'st_mtime', + 'st_nlink', + 'st_size', + 'st_uid' + ) + ) + + return attr + + def readdir(self, path, fh): + full_path = self._full_path(path) + + dirents = ['.', '..'] + if os.path.isdir(full_path): + dirents.extend(os.listdir(full_path)) + for r in dirents: + yield r + + def readlink(self, path): + pathname = os.readlink(self._full_path(path)) + if pathname.startswith("/"): + # Path name is absolute, sanitize it. + return os.path.relpath(pathname, self.mountSource) + else: + return pathname + + def mknod(self, path, mode, dev): + return os.mknod(self._full_path(path), mode, dev) + + def rmdir(self, path): + full_path = self._full_path(path) + return os.rmdir(full_path) + + def mkdir(self, path, mode): + return os.mkdir(self._full_path(path), mode) + + def statfs(self, path): + full_path = self._full_path(path) + stv = os.statvfs(full_path) + stat = dict((key, getattr(stv, key)) + for key in ('f_bavail', + 'f_bfree', + 'f_blocks', + 'f_bsize', + 'f_favail', + 'f_ffree', + 'f_files', + 'f_flag', + 'f_frsize', + 'f_namemax' + ) + ) + return stat + + def unlink(self, path): + return os.unlink(self._full_path(path)) + + def symlink(self, name, target): + return os.symlink(name, self._full_path(target)) + + def rename(self, old, new): + return os.rename(self._full_path(old), self._full_path(new)) + + def link(self, target, name): + return os.link(self._full_path(target), self._full_path(name)) + + def utimens(self, path, times=None): + return os.utime(self._full_path(path), times) + + # File methods + # ============ + + def open(self, path, flags): + full_path = self._full_path(path) + return os.open(full_path, flags) + + def create(self, path, mode, fi=None): + full_path = self._full_path(path) + return os.open(full_path, os.O_WRONLY | os.O_CREAT, mode) + + def read(self, path, length, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.read(fh, length) + + def write(self, path, buf, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.write(fh, buf) + + def truncate(self, path, length, fh=None): + full_path = self._full_path(path) + with open(full_path, 'r+') as f: + f.truncate(length) + + def flush(self, path, fh): + return os.fsync(fh) + + def release(self, path, fh): + return os.close(fh) + + def fsync(self, path, fdatasync, fh): + return self.flush(path, fh) diff --git a/iotronic_lightningrod/modules/vfs_manager.py b/iotronic_lightningrod/modules/vfs_manager.py new file mode 100644 index 0000000..edf6c99 --- /dev/null +++ b/iotronic_lightningrod/modules/vfs_manager.py @@ -0,0 +1,508 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 __future__ import with_statement + +import errno +import os +from subprocess import call +import threading +from twisted.internet.defer import inlineCallbacks +from twisted.internet.defer import returnValue + +# Iotronic imports +from iotronic_lightningrod.modules import Module + +# Fuse imports +import ctypes +import ctypes.util +from fuse import FUSE +from fuse import FuseOSError +from fuse import Operations + +# Logging conf +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +class VfsManager(Module.Module): + + def __init__(self, board, session): + super(VfsManager, self).__init__("VFS", board) + + self.session = session + self.board = board + + """ + #print session + from iotronic_lightningrod.modules import vfs_library + fuse=vfs_library.FuseLib("/opt/AAA") + print fuse.getattr("/aaa.txt") + """ + + libcPath = ctypes.util.find_library("c") + self.libc = ctypes.CDLL(libcPath) + + def finalize(self): + pass + + def mountLocal(self, mountSource, mountPoint): + + try: + + mounter = MounterLocal(mountSource, mountPoint) + mounter.start() + + result = "Mounted " + mountSource + " in " + mountPoint + + except Exception as msg: + result = "Mounting error:", msg + + print(result) + yield returnValue(result) + + def unmountLocal(self, mountPoint): + + print("Unmounting...") + + try: + + # errorCode = self.libc.umount(mountPoint, None) + errorCode = call(["umount", "-l", mountPoint]) + + result = "Unmount " + mountPoint + " result: " + str(errorCode) + + except Exception as msg: + result = "Unmounting error:", msg + + print(result) + yield returnValue(result) + + def mountRemote(self, + mountSource, + mountPoint, + boardRemote=None, + agentRemote=None + ): + + try: + + mounter = MounterRemote( + mountSource, + mountPoint, + self.board, + self.session, + boardRemote, + agentRemote + ) + + mounter.start() + + result = "Mounted " + mountSource + " in " + mountPoint + + except Exception as msg: + result = "Mounting error:", msg + + print(result) + yield returnValue(result) + + def unmountRemote(self, mountPoint): + + print("Unmounting...") + + try: + + # errorCode = self.libc.umount(mountPoint, None) + errorCode = call(["umount", "-l", mountPoint]) + + result = "Unmount " + mountPoint + " result: " + str(errorCode) + + except Exception as msg: + result = "Unmounting error:", msg + + print(result) + yield returnValue(result) + + +class MounterLocal(threading.Thread): + + def __init__(self, mountSource, mountPoint): + threading.Thread.__init__(self) + # self.setDaemon(1) + self.setName("VFS-Mounter") # Set thread name + + self.mountSource = mountSource + self.mountPoint = mountPoint + + def run(self): + """Mount FUSE FS + + """ + try: + + FUSE( + FuseManager(self.mountSource), + self.mountPoint, + nothreads=False, + foreground=True + ) + + except Exception as msg: + LOG.error("Mounting FUSE error: " + str(msg)) + + +class MounterRemote(threading.Thread): + + def __init__( + self, + mountSource, + mountPoint, + board, + session, + boardRemote, + agentRemote + ): + + threading.Thread.__init__(self) + # self.setDaemon(1) + self.setName("VFS-Mounter") # Set thread name + + self.mountSource = mountSource + self.mountPoint = mountPoint + self.session = session + self.board = board + self.boardRemote = boardRemote + self.agentRemote = agentRemote + + def run(self): + """Mount FUSE FS. + + """ + try: + + FUSE( + FuseRemoteManager( + self.mountSource, + self.board.agent, + self.session, + self.boardRemote, + self.agentRemote + ), + self.mountPoint, + nothreads=False, + foreground=True + ) + + except Exception as msg: + LOG.error("Mounting FUSE error: " + str(msg)) + + +@inlineCallbacks +def makeCall(msg=None, agent=None, session=None): + rpc_addr = str(agent) + '.stack4things.echo' + LOG.debug("VFS - I'm calling " + rpc_addr) + try: + res = yield session.call(rpc_addr, msg) + LOG.info("NOTIFICATION " + str(res)) + except Exception as e: + LOG.warning("NOTIFICATION error: {0}".format(e)) + + +class FuseRemoteManager(Operations): + + def __init__(self, mountSource, agent, session, boardRemote, agentRemote): + + self.mountSource = mountSource + self.session = session + self.agent = agent + self.boardRemote = boardRemote + self.agentRemote = agentRemote + + # makeCall("UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU", + # self.agent, self.session) # TEMPORARY + + def join_path(self, partial): + if partial.startswith("/"): + partial = partial[1:] + path = os.path.join(self.mountSource, partial) + print(path) + return path + + # Filesystem methods + # ================== + + def access(self, path, mode): + full_path = self.join_path(path) + if not os.access(full_path, mode): + raise FuseOSError(errno.EACCES) + + def chmod(self, path, mode): + full_path = self.join_path(path) + return os.chmod(full_path, mode) + + def chown(self, path, uid, gid): + full_path = self.join_path(path) + return os.chown(full_path, uid, gid) + + def getattr(self, path, fh=None): + full_path = self.join_path(path) + st = os.lstat(full_path) + attr = dict((key, getattr(st, key)) + for key in ( + 'st_atime', + 'st_ctime', + 'st_gid', + 'st_mode', + 'st_mtime', + 'st_nlink', + 'st_size', + 'st_uid' + ) + ) + + return attr + + def readdir(self, path, fh): + full_path = self.join_path(path) + + dirents = ['.', '..'] + if os.path.isdir(full_path): + dirents.extend(os.listdir(full_path)) + for r in dirents: + yield r + + def readlink(self, path): + pathname = os.readlink(self.join_path(path)) + if pathname.startswith("/"): + # Path name is absolute, sanitize it. + return os.path.relpath(pathname, self.mountSource) + else: + return pathname + + def mknod(self, path, mode, dev): + return os.mknod(self.join_path(path), mode, dev) + + def rmdir(self, path): + full_path = self.join_path(path) + return os.rmdir(full_path) + + def mkdir(self, path, mode): + return os.mkdir(self.join_path(path), mode) + + def statfs(self, path): + full_path = self.join_path(path) + stv = os.statvfs(full_path) + stat = dict((key, getattr(stv, key)) + for key in ('f_bavail', + 'f_bfree', + 'f_blocks', + 'f_bsize', + 'f_favail', + 'f_ffree', + 'f_files', + 'f_flag', + 'f_frsize', + 'f_namemax' + ) + ) + return stat + + def unlink(self, path): + return os.unlink(self.join_path(path)) + + def symlink(self, name, target): + return os.symlink(name, self.join_path(target)) + + def rename(self, old, new): + return os.rename(self.join_path(old), self.join_path(new)) + + def link(self, target, name): + return os.link(self.join_path(target), self.join_path(name)) + + def utimens(self, path, times=None): + return os.utime(self.join_path(path), times) + + # File methods + # ============ + + def open(self, path, flags): + full_path = self.join_path(path) + return os.open(full_path, flags) + + def create(self, path, mode, fi=None): + full_path = self.join_path(path) + return os.open(full_path, os.O_WRONLY | os.O_CREAT, mode) + + def read(self, path, length, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.read(fh, length) + + def write(self, path, buf, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.write(fh, buf) + + def truncate(self, path, length, fh=None): + full_path = self.join_path(path) + with open(full_path, 'r+') as f: + f.truncate(length) + + def flush(self, path, fh): + return os.fsync(fh) + + def release(self, path, fh): + return os.close(fh) + + def fsync(self, path, fdatasync, fh): + return self.flush(path, fh) + + +class FuseManager(Operations): + + def __init__(self, mountSource): + self.mountSource = mountSource + + def join_path(self, partial): + if partial.startswith("/"): + partial = partial[1:] + path = os.path.join(self.mountSource, partial) + print(path) + return path + + # Filesystem methods + # ================== + + def access(self, path, mode): + full_path = self.join_path(path) + if not os.access(full_path, mode): + raise FuseOSError(errno.EACCES) + + def chmod(self, path, mode): + full_path = self.join_path(path) + return os.chmod(full_path, mode) + + def chown(self, path, uid, gid): + full_path = self.join_path(path) + return os.chown(full_path, uid, gid) + + def getattr(self, path, fh=None): + full_path = self.join_path(path) + st = os.lstat(full_path) + attr = dict((key, getattr(st, key)) + for key in ( + 'st_atime', + 'st_ctime', + 'st_gid', + 'st_mode', + 'st_mtime', + 'st_nlink', + 'st_size', + 'st_uid' + ) + ) + + return attr + + def readdir(self, path, fh): + full_path = self.join_path(path) + + dirents = ['.', '..'] + if os.path.isdir(full_path): + dirents.extend(os.listdir(full_path)) + for r in dirents: + yield r + + def readlink(self, path): + pathname = os.readlink(self.join_path(path)) + if pathname.startswith("/"): + # Path name is absolute, sanitize it. + return os.path.relpath(pathname, self.mountSource) + else: + return pathname + + def mknod(self, path, mode, dev): + return os.mknod(self.join_path(path), mode, dev) + + def rmdir(self, path): + full_path = self.join_path(path) + return os.rmdir(full_path) + + def mkdir(self, path, mode): + return os.mkdir(self.join_path(path), mode) + + def statfs(self, path): + full_path = self.join_path(path) + stv = os.statvfs(full_path) + stat = dict((key, getattr(stv, key)) + for key in ('f_bavail', + 'f_bfree', + 'f_blocks', + 'f_bsize', + 'f_favail', + 'f_ffree', + 'f_files', + 'f_flag', + 'f_frsize', + 'f_namemax' + ) + ) + return stat + + def unlink(self, path): + return os.unlink(self.join_path(path)) + + def symlink(self, name, target): + return os.symlink(name, self.join_path(target)) + + def rename(self, old, new): + return os.rename(self.join_path(old), self.join_path(new)) + + def link(self, target, name): + return os.link(self.join_path(target), self.join_path(name)) + + def utimens(self, path, times=None): + return os.utime(self.join_path(path), times) + + # File methods + # ============ + + def open(self, path, flags): + full_path = self.join_path(path) + return os.open(full_path, flags) + + def create(self, path, mode, fi=None): + full_path = self.join_path(path) + return os.open(full_path, os.O_WRONLY | os.O_CREAT, mode) + + def read(self, path, length, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.read(fh, length) + + def write(self, path, buf, offset, fh): + os.lseek(fh, offset, os.SEEK_SET) + return os.write(fh, buf) + + def truncate(self, path, length, fh=None): + full_path = self.join_path(path) + with open(full_path, 'r+') as f: + f.truncate(length) + + def flush(self, path, fh): + return os.fsync(fh) + + def release(self, path, fh): + return os.close(fh) + + def fsync(self, path, fdatasync, fh): + return self.flush(path, fh) diff --git a/iotronic_lightningrod/plugins/Plugin.py b/iotronic_lightningrod/plugins/Plugin.py new file mode 100644 index 0000000..c06b411 --- /dev/null +++ b/iotronic_lightningrod/plugins/Plugin.py @@ -0,0 +1,83 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 abc +import six +import threading + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +""" +from twisted.internet.defer import inlineCallbacks + +@inlineCallbacks +def sendNotification(msg=None): + try: + res = yield SESSION.call(u'agent.stack4things.echo', msg) + LOG.info("NOTIFICATION " + str(res)) + except Exception as e: + LOG.warning("NOTIFICATION error: {0}".format(e)) +""" + + +@six.add_metaclass(abc.ABCMeta) +class Plugin(threading.Thread): + + def __init__(self, uuid, name, q_result=None, params=None): + + threading.Thread.__init__(self) + # self.setDaemon(1) + self.setName("Plugin " + str(self.name)) # Set thread name + + self.uuid = uuid + self.name = name + self.status = "INITED" + self.setStatus(self.status) + self._is_running = True + self.params = params + self.q_result = q_result + self.type = type + + @abc.abstractmethod + def run(self): + """RUN method where to implement the user's plugin logic + + """ + def stop(self): + self._is_running = False + + """ + def Done(self): + self.setStatus("COMPLETED") + sendNotification(msg="hello!") + self.checkStatus() + """ + + def checkStatus(self): + # LOG.debug("Plugin " + self.name + " check status: " + self.status) + return self.status + + def setStatus(self, status): + self.status = status + # LOG.debug("Plugin " + self.name + " changed status: " + self.status) + + def complete(self, rpc_name, result): + self.setStatus(result) + result = rpc_name + " result: " + self.checkStatus() + + return result diff --git a/iotronic_lightningrod/plugins/PluginSerializer.py b/iotronic_lightningrod/plugins/PluginSerializer.py new file mode 100644 index 0000000..5175a9f --- /dev/null +++ b/iotronic_lightningrod/plugins/PluginSerializer.py @@ -0,0 +1,50 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 cPickle as pickle +# import oslo_messaging + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +# class ObjectSerializer(oslo_messaging.NoOpSerializer): +class ObjectSerializer(object): + """A PluginObject-aware Serializer. + + This implements the Oslo Serializer interface and provides the + ability to serialize and deserialize PluginObject entities. + Any service that needs to accept or return PluginObject as + arguments or result values should pass this to its RpcProxy + and RpcDispatcher objects. + """ + + # def serialize_entity(self, context, entity): + def serialize_entity(self, entity): + + dumped = pickle.dumps(entity, 0) + + # LOG.debug(" - plugin serialized") + + return dumped + + # def deserialize_entity(self, context, entity): + def deserialize_entity(self, entity): + + loaded = pickle.loads(str(entity)) + + # LOG.debug(" - plugin deserialized") + + return loaded diff --git a/iotronic_lightningrod/plugins/__init__.py b/iotronic_lightningrod/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_lightningrod/plugins/pluginApis.py b/iotronic_lightningrod/plugins/pluginApis.py new file mode 100644 index 0000000..1139558 --- /dev/null +++ b/iotronic_lightningrod/plugins/pluginApis.py @@ -0,0 +1,63 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 httplib2 +import json + +from iotronic_lightningrod.lightningrod import board + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + + +def getBoardID(): + return board.uuid + + +def getLocation(): + return board.location + + +def getBoardGpio(): + return board.device.gpio + + +def sendRequest(url, action, headers=None, body=None, verbose=False): + """Generic REST client for plugin users. + + :param url: resource URI + :param action: POST, GET, PUT, etc + :param headers: request header + :param data: request body + :param verbose: flag to enable/disable verbose output + :return: + + """ + try: + + http = httplib2.Http() + headers = headers + response, send = http.request(url, action, headers=headers, body=body) + + if verbose: + req = json.loads(send) + LOG.info("\nREST REQUEST: HTTP " + str(response['status']) + + " - success = " + str(req['success']) + + " - " + str(req['result']['records'])) + + except Exception as err: + LOG.error("sendRequest error: " + str(err)) + + return send diff --git a/iotronic_lightningrod/plugins/plugins_examples/demo.py b/iotronic_lightningrod/plugins/plugins_examples/demo.py new file mode 100644 index 0000000..8d610d1 --- /dev/null +++ b/iotronic_lightningrod/plugins/plugins_examples/demo.py @@ -0,0 +1,70 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 iotronic_lightningrod.devices.gpio import yun +from iotronic_lightningrod.plugins import Plugin + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +# User imports +import datetime +import math +import time + +ADCres = 1023.0 +Beta = 3950 +Kelvin = 273.15 +Rb = 10000 +Ginf = 120.6685 + +# User global variables +resource_id = "" # temperature resource id +action_URL = "http://smartme-data.unime.it/api/3/action/datastore_upsert" +api_key = '' +headers = { + "Content-Type": "application/json", + 'Authorization': "" + api_key + "" +} +polling_time = 10 + + +class Worker(Plugin.Plugin): + def __init__(self, name, params=None): + super(Worker, self).__init__(name, params) + + def run(self): + + device = yun.YunGpio() + + while (self._is_running): + + voltage = device._readVoltage("A0") + + Rthermistor = float(Rb) * (float(ADCres) / float(voltage) - 1) + + rel_temp = float(Beta) / (math.log( + float(Rthermistor) * float(Ginf)) + ) + temp = rel_temp - Kelvin + + m_value = str(temp) + m_timestamp = datetime.datetime.now().strftime( + '%Y-%m-%dT%H:%M:%S.%f' + ) + + LOG.info(m_value + " - " + m_timestamp) + + time.sleep(polling_time) diff --git a/iotronic_lightningrod/plugins/plugins_examples/runner.json b/iotronic_lightningrod/plugins/plugins_examples/runner.json new file mode 100644 index 0000000..9b0d969 --- /dev/null +++ b/iotronic_lightningrod/plugins/plugins_examples/runner.json @@ -0,0 +1,3 @@ +{ + "message": "Hello!" +} diff --git a/iotronic_lightningrod/plugins/plugins_examples/runner.py b/iotronic_lightningrod/plugins/plugins_examples/runner.py new file mode 100644 index 0000000..21c0328 --- /dev/null +++ b/iotronic_lightningrod/plugins/plugins_examples/runner.py @@ -0,0 +1,35 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 iotronic_lightningrod.plugins import Plugin + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +# User imports +import time + + +class Worker(Plugin.Plugin): + def __init__(self, uuid, name, q_result=None, params=None): + super(Worker, self).__init__(uuid, name, q_result, params) + + def run(self): + LOG.info("Plugin " + self.name + " starting...") + LOG.info(self.params) + + while(self._is_running): + LOG.info(self.params['message']) + time.sleep(1) diff --git a/iotronic_lightningrod/plugins/plugins_examples/smartme.json b/iotronic_lightningrod/plugins/plugins_examples/smartme.json new file mode 100644 index 0000000..67e8d2c --- /dev/null +++ b/iotronic_lightningrod/plugins/plugins_examples/smartme.json @@ -0,0 +1,13 @@ +{ + "polling" : "600", + "ckan_enabled" : false, + "temperature": { "pin" : "A0", "enabled":true }, + "brightness": { "pin" : "A1", "enabled":true }, + "humidity": { "pin" : "A2", "enabled":true }, + "gas": { "pin" : "A3", "enabled":true }, + "noise": { "pin" : "A4", "enabled":true }, + "pressure": { "pin" : "i2c", "enabled":true } +} + + +{"delay" : 10} \ No newline at end of file diff --git a/iotronic_lightningrod/plugins/plugins_examples/smartme.py b/iotronic_lightningrod/plugins/plugins_examples/smartme.py new file mode 100644 index 0000000..8c7105f --- /dev/null +++ b/iotronic_lightningrod/plugins/plugins_examples/smartme.py @@ -0,0 +1,409 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 iotronic_lightningrod.plugins import Plugin +from iotronic_lightningrod.plugins import pluginApis as API + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +# User imports +import datetime +import json +import math +import threading +import time + +# User global variables +ckan_addr = 'smartme-data.unime.it' +action_URL = "http://" + ckan_addr + "/api/3/action/datastore_upsert" +api_key = '22c5cfa7-9dea-4dd9-9f9d-eedf296852ae' +headers = { + "Content-Type": "application/json", + 'Authorization': "" + api_key + "" +} + +sensors_list = [ + 'temperature', + 'brightness', + 'humidity', + 'pressure', + 'noise' + # , 'gas' +] +position = None + +SENSORS = {} + +location = {} + +device = API.getBoardGpio() + +THR_KILL = None + + +# Sensors gloabl parameters + +# Temperature Parameters +ADCres = 1023.0 +Beta = 3950 +Kelvin = 273.15 +Rb = 10000 +Ginf = 120.6685 +latest_temp = None + +# Noise Parameters +samples_number = 1000 +amplitudes_sum = 0 +amplitudes_count = 0 + + +def Temperature(): + """To get Temperature value. + + :return: Temperature value (float) + + """ + try: + voltage = device._readVoltage(SENSORS['temperature']['pin']) + + Rthermistor = float(Rb) * (float(ADCres) / float(voltage) - 1) + rel_temp = float(Beta) / (math.log(float(Rthermistor) * float(Ginf))) + temp = rel_temp - Kelvin + + # LOG.info("Temperature " + str(temp) + u" \u2103") + + except Exception as err: + LOG.error("Error getting temperature: " + str(err)) + + return temp + + +def Brightness(): + """To get Brightness value. + + :return: Brightness value (float) + + """ + try: + voltage = float(device._readVoltage(SENSORS['brightness']['pin'])) + + ldr = (2500 / (5 - voltage * float(0.004887)) - 500) / float(3.3) + + LOG.info("Brightness: " + str(ldr) + " (lux)") + + except Exception as err: + LOG.error("Error getting brightness: " + str(err)) + + return ldr + + +def Humidity(): + """To get Humidity value: this function uses the Temperature sensor too. + + :return: Humidity value (float) + + """ + try: + + degCelsius = Temperature() + supplyVolt = float(4.64) + HIH4030_Value = float(device._readVoltage(SENSORS['humidity']['pin'])) + voltage = HIH4030_Value / float(1023.) * supplyVolt + sensorRH = float(161.0) * float(voltage) / supplyVolt - float(25.8) + relHum = sensorRH / (float(1.0546) - float(0.0026) * degCelsius) + + LOG.info("Humidity " + str(relHum) + " percent") + + except Exception as err: + LOG.error("Error getting humidity: " + str(err)) + + return relHum + + +def Pressure(): + """To get Pressure value. + + :return: Pressure value (float) + + """ + try: + + in_pressure_raw = device.i2cRead('pressure') + pressure = float(in_pressure_raw) * float(0.00025) * 10 + + LOG.info("Pressure: " + str(pressure) + " hPa") + + except Exception as err: + LOG.error("Error getting pressure: " + str(err)) + + return pressure + + +def Noise(): + """To get Noise value. + + Elaborate a noise avarange value from noise listener. + + :return: Noise value (float) + + """ + + try: + + global amplitudes_sum, amplitudes_count + + if amplitudes_count == float(0): + amplitude = float(0) + + else: + amplitude = float(amplitudes_sum / amplitudes_count) + + amplitudes_sum = 0 + amplitudes_count = 0 + + except Exception as err: + LOG.error("Error getting noise: " + str(err)) + + return amplitude + + +def noise_listner(): + """Each two seconds collect a Noise sample. + + """ + + global THR_KILL + + vect = [] + + if THR_KILL: + + # LOG.info("listening noise..." + str(THR_KILL)) + + for x in range(samples_number): + + read = float(device._readVoltage(SENSORS['noise']['pin'])) + vect.append(read) + + sorted_vect = sorted(vect) + + minimum = float(sorted_vect[50]) + maximum = float(sorted_vect[samples_number - 51]) + tmp_amplitude = float(maximum - minimum) + + global amplitudes_sum, amplitudes_count + amplitudes_sum = float(amplitudes_sum + tmp_amplitude) + amplitudes_count = float(amplitudes_count + 1) + # LOG.info("amplitudes_sum = " + str(amplitudes_sum)) + # LOG.info("amplitudes_count = " + str(amplitudes_count)) + + threading.Timer(2.0, noise_listner).start() + + else: + LOG.debug("Cancelled SmartME noise listening: " + str(THR_KILL)) + + +def getMetric(metric, ckan): + """Function to get metric values. + + This function call the function relative to the 'metric' + specified and if the 'ckan' flag is True we create the body for the + REST request to send to CKAN database to store the sample there; + + :param metric: name of the metric analized: 'Temperature', etc + :param ckan: flag True --> create JSON body for the CKAN request + :return: ckan_data --> JSON data to send as request body to CKAN + + """ + + # Call Sensors Metrics: Temperature(), etc... + m_value = str(globals()[metric.capitalize()]()) + + m_timestamp = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f') + + if metric == 'noise': + LOG.info("Noise: " + str(m_value) + " amplitude") + + elif metric == 'temperature': + LOG.info("Temperature " + str(m_value) + u" \u2103") + + if ckan: + + ckan_data = {} + ckan_data["resource_id"] = str(SENSORS[metric]['ckanID']) + ckan_data["method"] = "insert" + ckan_data["records"] = [] + sample = {} + sample["Latitude"] = location['latitude'] + sample["Longitude"] = location['longitude'] + sample["Altitude"] = location['altitude'] + metric_func_name = metric.capitalize() + sample[metric_func_name] = m_value + sample["Date"] = m_timestamp + ckan_data["records"].append(sample) + + ckan_data = json.dumps(ckan_data) + + else: + ckan_data = None + + return ckan_data + + +def getCKANdataset(board_uuid): + """To get CKAN resource IDs for each metric type managed by SmartME boards. + + :param board_uuid: + :return: + + """ + + datasets_url = "http://" + ckan_addr + "/api/rest/dataset/" + board_uuid + datasets = API.sendRequest(url=datasets_url, action='GET') + ckan_data = json.loads(datasets) + + for resource in ckan_data['resources']: + + # LOG.info(resource['name'].capitalize()) + + if resource['name'] in sensors_list: + # LOG.debug(resource['name']) + SENSORS[resource['name']]['ckanID'] = resource['id'] + # LOG.info(resource['name'] + " - " + resource['id']) + + +def setSensorsLayout(params): + for sensor in sensors_list: + SENSORS[sensor] = {} + SENSORS[sensor]['pin'] = params[sensor]['pin'] + SENSORS[sensor]['enabled'] = params[sensor]['enabled'] + + +def InitSmartMeBoard(params): + """This function init the SmartME board. + + In the SmartME Arduino YUN board this function enables the needed + devices and set the needed parameters about sensors and location. + + :param params: plugin parameters to configure the board. + + """ + + # get location + global location + location = API.getLocation() + LOG.info( + "Board location: \n" + + json.dumps(location, indent=4, separators=(',', ': ')) + ) + + # set devices + try: + + device.EnableI2c() + device.EnableGPIO() + + except Exception as err: + LOG.error("Error configuring devices: " + str(err)) + global THR_KILL + THR_KILL = False + + # set up sensors + setSensorsLayout(params) + + +class Worker(Plugin.Plugin): + + def __init__(self, uuid, name, q_result=None, params=None): + super(Worker, self).__init__( + uuid, name, + q_result=q_result, + params=params + ) + + def run(self): + + LOG.info("SmartME plugin starting...") + + global THR_KILL + THR_KILL = self._is_running + + # Board initialization + LOG.info("PARAMS list: " + str(self.params.keys())) + + if len(self.params.keys()) != 0: + + InitSmartMeBoard(self.params) + + # Get polling time + polling_time = float(self.params['polling']) + LOG.info("Polling time: " + str(polling_time)) + + # GET CKAN SENSORS UUID + getCKANdataset(API.getBoardID()) + + LOG.info( + "SENSORS: \n" + + json.dumps(SENSORS, indent=4, separators=(',', ': ')) + ) + + # START NOISE LISTENER if sensor enabled + if SENSORS['noise']['enabled']: + LOG.info("Starting noise listening...") + noise_listner() + + LOG.info("CKAN enabled: " + str(self.params['ckan_enabled'])) + + counter = 0 + + while (self._is_running and THR_KILL): + + if sensors_list.__len__() != 0: + + LOG.info("\n\n") + + for sensor in sensors_list: + + if SENSORS[sensor]['enabled']: + + if self.params['ckan_enabled']: + + API.sendRequest( + url=action_URL, + action='POST', + headers=headers, + body=getMetric(sensor, ckan=True), + verbose=False + ) + + else: + getMetric(sensor, ckan=False) + + counter = counter + 1 + LOG.info("Samples sent: " + str(counter)) + + time.sleep(polling_time) + + else: + LOG.warning("No sensors!") + self._is_running = False + THR_KILL = self._is_running + + # Update the thread status: at this stage THR_KILL will be False + THR_KILL = self._is_running + + else: + LOG.error("No parameters provided!") diff --git a/iotronic_lightningrod/plugins/plugins_examples/zero.py b/iotronic_lightningrod/plugins/plugins_examples/zero.py new file mode 100644 index 0000000..2f29f92 --- /dev/null +++ b/iotronic_lightningrod/plugins/plugins_examples/zero.py @@ -0,0 +1,32 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 iotronic_lightningrod.plugins import Plugin + +from oslo_log import log as logging +LOG = logging.getLogger(__name__) + +# User imports + + +class Worker(Plugin.Plugin): + + def __init__(self, uuid, name, q_result, params=None): + super(Worker, self).__init__(uuid, name, q_result, params) + + def run(self): + LOG.info("Input parameters: " + str(self.params)) + LOG.info("Plugin " + self.name + " process completed!") + self.q_result.put("ZERO RESULT") diff --git a/iotronic_lightningrod/wampmessage.py b/iotronic_lightningrod/wampmessage.py new file mode 100644 index 0000000..07c0878 --- /dev/null +++ b/iotronic_lightningrod/wampmessage.py @@ -0,0 +1,54 @@ +# Copyright 2017 MDSLAB - University of Messina +# All Rights Reserved. +# +# 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 + +SUCCESS = 'SUCCESS' +ERROR = 'ERROR' +WARNING = 'WARNING' + + +def deserialize(received): + m = json.loads(received) + return WampMessage(**m) + + +class WampMessage(object): + def __init__(self, message=None, result=None): + self.message = message + self.result = result + + def serialize(self): + return json.dumps(self, default=lambda o: o.__dict__) + """ + def deserialize(self, received): + self.__dict__ = json.loads(received) + return self + """ + + +class WampSuccess(WampMessage): + def __init__(self, msg=None): + super(WampSuccess, self).__init__(msg, SUCCESS) + + +class WampError(WampMessage): + def __init__(self, msg=None): + super(WampError, self).__init__(msg, ERROR) + + +class WampWarning(WampMessage): + def __init__(self, msg=None): + super(WampWarning, self).__init__(msg, WARNING) diff --git a/opt/stack4things/plugins.example.json b/opt/stack4things/plugins.example.json new file mode 100644 index 0000000..e7d8241 --- /dev/null +++ b/opt/stack4things/plugins.example.json @@ -0,0 +1,8 @@ +{ + "plugins": { + "zero": { + "onboot": "false", + "callable": "true" + } + } +} \ No newline at end of file diff --git a/opt/stack4things/settings.example.json b/opt/stack4things/settings.example.json new file mode 100644 index 0000000..fe1a463 --- /dev/null +++ b/opt/stack4things/settings.example.json @@ -0,0 +1,13 @@ +{ + "iotronic": { + "board": { + "code": "" + }, + "wamp": { + "registration-agent": { + "url": "ws://:/", + "realm": "" + } + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f8d0829 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +pbr>=2.0.0 # Apache-2.0 + +# Openstack modules +oslo.config>=3.22.0 # Apache-2.0 +oslo.log>=3.22.0 # Apache-2.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f5388ab --- /dev/null +++ b/setup.cfg @@ -0,0 +1,60 @@ +[metadata] +name = iotronic_lightningrod +summary = Implementation of the Lightning-rod, the Stack4Things board-side probe +description-file = + README.rst +author = Nicola Peditto, Fabio Verboso +author-email = unime.mdslab@gmail.com +home-page = http://stack4things.unime.it/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + iotronic_lightningrod + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = iotronic_lightningrod/locale +domain = iotronic_lightningrod + +[update_catalog] +domain = iotronic_lightningrod +output_dir = iotronic_lightningrod/locale +input_file = iotronic_lightningrod/locale/iotronic_lightningrod.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = iotronic_lightningrod/locale/iotronic_lightningrod.pot + +[build_releasenotes] +all_files = 1 +build-dir = releasenotes/build +source-dir = releasenotes/source + +[entry_points] +console_scripts = + lightning-rod = iotronic_lightningrod.lightningrod:main + +s4t.modules = + utility = iotronic_lightningrod.modules.utils:Utility + plugin = iotronic_lightningrod.modules.plugin_manager:PluginManager + device = iotronic_lightningrod.modules.device_manager:DeviceManager diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dcce8e6 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr>=1.8'], + pbr=True, + +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..2c95d63 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,18 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 + +coverage>=4.0 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx>=1.5.1 # BSD +oslosphinx>=4.7.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT + + +# releasenotes +reno>=1.8.0 # Apache-2.0 \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1238925 --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +minversion = 2.0 +envlist = py35,py27,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning +deps = -r{toxinidir}/test-requirements.txt +commands = find . -type f -name "*.pyc" -delete + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:releasenotes] +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build \ No newline at end of file