Release Lightning-rod v0.4.5:

- Added REST server for local LR management
 - Modules loader updated:
   - Single module loader added
 - Service Manager improved:
   - Add checks status for WSTUN server
   - Cleaning procedures added for WSTUN monitor threads
   - services.json backup management fixed
 - Webservice Manager updated:
   - NGINX listening ports changed
 - Docker procedures updated
 - Installation procedures updated
 - Requirements fixed: Flask

Change-Id: I73fdb55a2ea58d0f7a76eab78734d067133438c5
This commit is contained in:
Nicola Peditto 2019-01-09 17:45:52 +01:00
parent db3994293f
commit c2d6ecf0f5
39 changed files with 22410 additions and 213 deletions

View File

@ -8,6 +8,9 @@ include scripts/lr_install
include scripts/lr_configure
include scripts/device_bkp_rest
include iotronic_lightningrod/proxies/configs/*
include iotronic_lightningrod/modules/web/static/css/*
include iotronic_lightningrod/modules/web/static/js/*
include iotronic_lightningrod/modules/web/templates/*
include AUTHORS
include ChangeLog

View File

@ -3,34 +3,10 @@
GitHub repo:
- https://github.com/openstack/iotronic-lightning-rod
# Configure Lightning-rod environment
* Create the folder in your system to store Lightning-rod settings <LR_CONF_PATH> (e.g. "/etc/iotronic/"):
```
sudo mkdir <LR_CONF_PATH>
```
* Get Lightning-rod configuration template files:
```
cd <LR_CONF_PATH>
sudo wget https://raw.githubusercontent.com/openstack/iotronic-lightning-rod/master/templates/settings.example.json -O settings.json
sudo wget https://raw.githubusercontent.com/openstack/iotronic-lightning-rod/master/etc/iotronic/iotronic.conf
```
* Configure Lightning-rod identity:
```
cd <LR_CONF_PATH>
wget https://raw.githubusercontent.com/openstack/iotronic-lightning-rod/master/scripts/lr_configure
chmod +x lr_configure
./lr_configure -c <REGISTRATION-TOKEN> <WAMP-REG-AGENT-URL> <LR_CONF_PATH>
```
# Create container:
```
docker run -d --privileged \
-v lr_var:/var/lib/iotronic -v lr_le:/etc/letsencrypt/ \
-v <LR_CONF_PATH>/settings.json:/etc/iotronic/settings.json \
-v <LR_CONF_PATH>/iotronic.conf:/etc/iotronic/iotronic.conf \
--net=host --restart unless-stopped \
--name=lightning-rod mdslab/rpi-openstack-iotronic-lightning-rod
```

View File

@ -3,34 +3,10 @@
GitHub repo:
- https://github.com/openstack/iotronic-lightning-rod
# Configure Lightning-rod environment
* Create the folder in your system to store Lightning-rod settings <LR_CONF_PATH> (e.g. "/etc/iotronic/"):
```
sudo mkdir <LR_CONF_PATH>
```
* Get Lightning-rod configuration template files:
```
cd <LR_CONF_PATH>
sudo wget https://raw.githubusercontent.com/openstack/iotronic-lightning-rod/master/templates/settings.example.json -O settings.json
sudo wget https://raw.githubusercontent.com/openstack/iotronic-lightning-rod/master/etc/iotronic/iotronic.conf
```
* Configure Lightning-rod identity:
```
cd <LR_CONF_PATH>
wget https://raw.githubusercontent.com/openstack/iotronic-lightning-rod/master/scripts/lr_configure
chmod +x lr_configure
./lr_configure -c <REGISTRATION-TOKEN> <WAMP-REG-AGENT-URL> <LR_CONF_PATH>
```
# Create container:
```
docker run -d --privileged \
-v lr_var:/var/lib/iotronic -v lr_le:/etc/letsencrypt/ \
-v <LR_CONF_PATH>/settings.json:/etc/iotronic/settings.json \
-v <LR_CONF_PATH>/iotronic.conf:/etc/iotronic/iotronic.conf \
--net=host --restart unless-stopped \
--name=lightning-rod mdslab/openstack-iotronic-lightning-rod
```

View File

@ -1,8 +1,47 @@
IoTronic Lightning-rod installation guide for Raspberry Pi 2/3
============================================================
==============================================================
We tested this procedure on a Raspberry Pi 2/3 board (Raspbian).
Requirements
~~~~~~~~~~~~
* OS requirement
::
apt install python3 python3-setuptools python3-pip gdb lsof
* NodeJS
::
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
apt-get install -y nodejs
npm install -g npm
echo "NODE_PATH=/usr/lib/node_modules" | tee -a /etc/environment
source /etc/environment > /dev/null
* WSTUN:
::
npm install -g --unsafe @mdslab/wstun
* NGINX:
::
apt install -y nginx
sed -i 's/# server_names_hash_bucket_size 64;/server_names_hash_bucket_size 64;/g' /etc/nginx/nginx.conf
* Certbot
::
apt-get install python-certbot-nginx
Install Lightning-rod
~~~~~~~~~~~~~~~~~~~~~
@ -26,6 +65,7 @@ Iotronic setup
Arguments required:
* <REGISTRATION-TOKEN> , token released by IoTronic registration procedure
* <WAMP-REG-AGENT-URL> , IoTronic Crossbar server WAMP URL:
ws(s)://<IOTRONIC-CROSSBAR-IP>:<IOTRONIC-CROSSBAR-PORT>/
e.g.

View File

@ -4,6 +4,46 @@ IoTronic Lightning-rod installation guide for Ubuntu 16.04
We tested this procedure on a Ubuntu 16.04 (also within a LXD
container). Everything needs to be run as root.
Requirements
~~~~~~~~~~~~
* OS requirement
::
apt install python3 python3-setuptools python3-pip gdb lsof
* NodeJS
::
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
apt-get install -y nodejs
npm install -g npm
echo "NODE_PATH=/usr/lib/node_modules" | tee -a /etc/environment
source /etc/environment > /dev/null
* WSTUN:
::
npm install -g --unsafe @mdslab/wstun
* NGINX:
::
apt install -y nginx
sed -i 's/# server_names_hash_bucket_size 64;/server_names_hash_bucket_size 64;/g' /etc/nginx/nginx.conf
* Certbot
::
apt-get install python-certbot-nginx
Install Lightning-rod
~~~~~~~~~~~~~~~~~~~~~
::
@ -26,6 +66,7 @@ Iotronic setup
Arguments required:
* <REGISTRATION-TOKEN> , token released by IoTronic registration procedure
* <WAMP-REG-AGENT-URL> , IoTronic Crossbar server WAMP URL:
ws(s)://<IOTRONIC-CROSSBAR-IP>:<IOTRONIC-CROSSBAR-PORT>/
e.g.

View File

@ -27,6 +27,9 @@ CONF = cfg.CONF
SETTINGS = '/etc/iotronic/settings.json'
# global FIRST_BOOT
FIRST_BOOT = False
class Board(object):
@ -107,14 +110,26 @@ class Board(object):
except Exception as err:
if str(err) != 'uuid':
LOG.warning("settings.json file exception: " + str(err))
if self.status == None:
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)
if self.code == "<REGISTRATION-TOKEN>":
# LR start to waiting for first configuration
global FIRST_BOOT
if FIRST_BOOT == False:
FIRST_BOOT = True
LOG.info("FIRST BOOT procedure started")
self.status = "first_boot"
else:
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)

View File

@ -32,12 +32,15 @@ import signal
import ssl
from stevedore import extension
import sys
import time
import txaio
from pip._vendor import pkg_resources
# IoTronic imports
from iotronic_lightningrod.Board import Board
from iotronic_lightningrod.Board import FIRST_BOOT
from iotronic_lightningrod.common.exception import timeoutALIVE
from iotronic_lightningrod.common.exception import timeoutRPC
from iotronic_lightningrod.common import utils
@ -71,6 +74,7 @@ lr_opts = [
CONF = cfg.CONF
CONF.register_opts(lr_opts)
global SESSION
SESSION = None
global board
board = None
@ -81,6 +85,7 @@ RPC_proxies = {}
zombie_alert = True
# ASYNCIO
global loop
loop = None
component = None
txaio.start_logging(level="info")
@ -110,6 +115,8 @@ class LightningRod(object):
CONF(project='iotronic')
logging.setup(CONF, DOMAIN)
self.w = None
if (utils.checkIotronicConf(CONF)):
if CONF.debug:
@ -133,8 +140,20 @@ class LightningRod(object):
global board
board = Board()
self.w = WampManager(board.wamp_config)
# Start REST server
singleModuleLoader("rest", session=None)
if(board.status == "first_boot"):
LOG.info("LR FIRST BOOT: waiting for first configuration...")
while (board.status == "first_boot"):
time.sleep(5)
# LR was configured and we have to load its new configuration
board.loadSettings()
# Start Wamp Manager
self.w = WampManager(board.wamp_config)
self.w.start()
else:
@ -146,7 +165,8 @@ class LightningRod(object):
# No zombie alert activation
zombie_alert = False
LOG.info("LR is shutting down...")
self.w.stop()
if self.w != None:
self.w.stop()
Bye()
except Exception as e:
LOG.error("Error closing LR")
@ -177,6 +197,46 @@ class WampManager(object):
LOG.info("WAMP server stopped!")
def iotronic_status(board_status):
if board_status != "first_boot":
# WS ALIVE
try:
alive = asyncio.run_coroutine_threadsafe(
wamp_singleCheck(SESSION),
loop
)
alive = alive.result()
except Exception as e:
LOG.error(" - Iotronic check: " + str(e))
alive = e
else:
alive = "Not connected!"
return alive
async def wamp_singleCheck(session):
try:
# LOG.debug("ALIVE sending...")
with timeoutALIVE(seconds=CONF.rpc_alive_timer, action="ws_alive"):
res = await session.call(
str(board.agent) + u'.stack4things.wamp_alive',
board_uuid=board.uuid,
board_name=board.name
)
LOG.debug("WampCheck attempt " + str(res))
except exception.ApplicationError as e:
LOG.error(" - Iotronic Connection RPC error: " + str(e))
return res
async def wamp_checks(session):
while (True):
@ -594,6 +654,8 @@ def wampConnect(wamp_conf):
"\n- connected = " + str(connected)
)
board.session_id = "N/A"
if board.status == "operative" and reconnection is False:
#################
@ -696,6 +758,58 @@ def moduleWampRegister(session, meth_list):
LOG.info(" --> " + str(meth[0]))
def singleModuleLoader(module_name, session=None):
ep = []
for ep in pkg_resources.iter_entry_points(group='s4t.modules'):
# LOG.info(" - " + str(ep))
pass
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('Module "' + module_name + '" loading:')
for ext in modules.extensions:
if (ext.name == 'rest'):
mod = ext.plugin(board, session)
global MODULES
MODULES[mod.name] = mod
# Methods list for each module
meth_list = inspect.getmembers(mod, predicate=inspect.ismethod)
global RPC
RPC[mod.name] = meth_list
if len(meth_list) == 3:
# there are at least two methods for each module:
# "__init__" and "finalize"
LOG.info(" - No RPC to register for "
+ str(ext.name) + " module!")
else:
if(session != None):
LOG.info(" - RPC list of " + str(mod.name) + ":")
moduleWampRegister(SESSION, meth_list)
# Call the finalize procedure for each module
mod.finalize()
def modulesLoader(session):
"""Modules loader method thorugh stevedore libraries.
@ -727,36 +841,41 @@ def modulesLoader(session):
for ext in modules.extensions:
# LOG.debug(ext.name)
LOG.debug(ext.name)
if (ext.name == 'gpio') & (board.type == 'server'):
LOG.info("- GPIO module disabled for 'server' devices")
else:
mod = ext.plugin(board, session)
global MODULES
MODULES[mod.name] = mod
if ext.name != "rest":
# Methods list for each module
meth_list = inspect.getmembers(mod, predicate=inspect.ismethod)
mod = ext.plugin(board, session)
global RPC
RPC[mod.name] = meth_list
global MODULES
MODULES[mod.name] = mod
if len(meth_list) == 3:
# there are at least two methods for each module:
# "__init__" and "finalize"
# Methods list for each module
meth_list = inspect.getmembers(
mod, predicate=inspect.ismethod
)
LOG.info(" - No RPC to register for "
+ str(ext.name) + " module!")
global RPC
RPC[mod.name] = meth_list
else:
LOG.info(" - RPC list of " + str(mod.name) + ":")
moduleWampRegister(SESSION, meth_list)
if len(meth_list) == 3:
# there are at least two methods for each module:
# "__init__" and "finalize"
# Call the finalize procedure for each module
mod.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...")

View File

@ -27,7 +27,6 @@ from datetime import datetime
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 iotronic_lightningrod.modules import utils
import iotronic_lightningrod.wampmessage as WM
@ -88,13 +87,13 @@ class DeviceManager(Module.Module):
for meth in dev_meth_list:
if (meth[0] != "__init__") & (meth[0] != "finalize"):
# LOG.info(" - " + str(meth[0]))
LOG.info(" - " + str(meth[0]))
# rpc_addr = u'iotronic.' + board.uuid + '.' + meth[0]
rpc_addr = u'iotronic.' + str(board.session_id) + '.' + \
board.uuid + '.' + meth[0]
# LOG.debug(" --> " + str(rpc_addr))
SESSION.register(meth[1], rpc_addr)
self.device_session.register(meth[1], rpc_addr)
LOG.info(" --> " + str(meth[0]) + " registered!")
@ -164,17 +163,22 @@ class DeviceManager(Module.Module):
rpc_name = utils.getFuncName()
LOG.info("RPC " + rpc_name + " CALLED")
command = "ifconfig"
out = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE
)
output = out.communicate()[0].decode('utf-8').strip()
message = str(output)
message = getIfconfig()
w_msg = WM.WampSuccess(message)
return w_msg.serialize()
def getIfconfig():
command = "ifconfig"
out = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE
)
output = str(out.communicate()[0].decode('utf-8').strip())
return output

View File

@ -0,0 +1,133 @@
# 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__ = "Nicola Peditto <n.peditto@gmail.com>"
from iotronic_lightningrod.lightningrod import board
from iotronic_lightningrod.lightningrod import iotronic_status
from iotronic_lightningrod.modules import device_manager
from iotronic_lightningrod.modules import Module
from iotronic_lightningrod.modules import service_manager
from datetime import datetime
from flask import Flask
from flask import redirect
from flask import render_template
from flask import request
import os
import subprocess
import threading
from oslo_config import cfg
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class RestManager(Module.Module):
def __init__(self, board, session=None):
super(RestManager, self).__init__("RestManager", board)
def finalize(self):
threading.Thread(target=self._runRestServer, args=()).start()
def restore(self):
pass
def _runRestServer(self):
APP_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATE_PATH = os.path.join(APP_PATH, 'modules/web/templates/')
STATIC_PATH = os.path.join(APP_PATH, 'modules/web/static/')
app = Flask(
__name__,
template_folder=TEMPLATE_PATH,
static_folder=STATIC_PATH,
static_url_path="/static"
)
@app.route('/')
def home():
return render_template('home.html')
@app.route('/status')
def status():
wstun_status = service_manager.wstun_status()
if wstun_status == 0:
wstun_status = "Online"
else:
wstun_status = "Offline"
info = {
'board_id': board.uuid,
'board_name': board.name,
'wagent': board.agent,
'session_id': board.session_id,
'timestamp': str(
datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')),
'wstun_status': wstun_status,
'board_reg_status': str(board.status),
'iotronic_status': str(iotronic_status(board.status)),
'service_list': str(service_manager.services_list())
}
return render_template('status.html', **info)
@app.route('/network')
def network():
info = {
'ifconfig': device_manager.getIfconfig().replace('\n', '<br>')
}
return render_template('network.html', **info)
def lr_config(ragent, code):
bashCommand = "lr_configure %s %s " % (code, ragent)
process = subprocess.Popen(bashCommand.split(),
stdout=subprocess.PIPE)
output, error = process.communicate()
# print(output)
return
@app.route('/config', methods=['GET', 'POST'])
def config():
if request.method == 'POST':
ragent = request.form['urlwagent']
code = request.form['code']
lr_config(ragent, code)
return redirect("/status", code=302)
else:
if board.status == "first_boot":
urlwagent = request.args.get('urlwagent') or ""
code = request.args.get('code') or ""
info = {
'urlwagent': urlwagent,
'code': code
}
return render_template('config.html', **info)
else:
return redirect("/status", code=302)
app.run(host='0.0.0.0', port=1474, debug=False, use_reloader=False)

View File

@ -20,10 +20,14 @@ import json
import os
import psutil
import pyinotify
import queue
import signal
import socket
import subprocess
import time
import threading
from datetime import datetime
from threading import Thread
from urllib.parse import urlparse
@ -35,7 +39,7 @@ from iotronic_lightningrod.modules import utils
import iotronic_lightningrod.wampmessage as WM
from iotronic_lightningrod import lightningrod
from random import randint
from oslo_config import cfg
from oslo_log import log as logging
@ -62,6 +66,17 @@ CONF.register_opts(wstun_opts, group=service_group)
s_conf_FILE = CONF.lightningrod_home + "/services.json"
ws_server_alive = 0
WS_MON_LIST = {}
global wstun_ip
wstun_ip = None
global wstun_port
wstun_port = None
class ServiceManager(Module.Module):
def __init__(self, board, session):
@ -72,6 +87,11 @@ class ServiceManager(Module.Module):
self.wstun_ip = urlparse(board.wamp_config["url"])[1].split(':')[0]
self.wstun_port = "8080"
global wstun_port
wstun_port = self.wstun_port
global wstun_ip
wstun_ip = self.wstun_ip
is_wss = False
wurl_list = board.wamp_config["url"].split(':')
if wurl_list[0] == "wss":
@ -112,101 +132,128 @@ class ServiceManager(Module.Module):
else:
if len(s_conf['services']) != 0:
print("WSTUN server checks:")
wstun_process_list = []
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(4)
global ws_server_alive
ws_server_alive = sock.connect_ex(
(self.wstun_ip, int(self.wstun_port)))
for p in psutil.process_iter():
if len(p.cmdline()) != 0:
if (p.name() == "node" and "wstun" in p.cmdline()[1]):
wstun_process_list.append(p)
if ws_server_alive == 0:
if len(s_conf) != 0:
print("\nWSTUN processes:")
print(" - WSTUN server is online!")
for s_uuid in s_conf['services']:
sock.close() # close check socket
service_name = \
s_conf['services'][s_uuid]['name']
service_pid = \
s_conf['services'][s_uuid]['pid']
LOG.info(" - " + service_name)
if len(s_conf['services']) != 0:
if len(wstun_process_list) != 0:
wstun_process_list = []
for wp in wstun_process_list:
try:
for p in psutil.process_iter():
if len(p.cmdline()) != 0:
if ((p.name() == "node") and (
"wstun" in p.cmdline()[1]
)):
wstun_process_list.append(p)
except Exception as e:
LOG.error(
" --> PSUTIL [finalize]: " +
"error getting wstun processes info: " + str(e)
)
if service_pid == wp.pid:
LOG.info(
" --> the tunnel for '" + service_name
+ "' already exists; killing..."
)
if len(s_conf) != 0:
print("\nWSTUN processes:")
# 1. Kill wstun process (if exists)
for s_uuid in s_conf['services']:
# No zombie alert activation
lightningrod.zombie_alert = False
LOG.debug(
"[WSTUN-RESTORE] - "
"on-finalize zombie_alert: " +
str(lightningrod.zombie_alert)
)
service_name = \
s_conf['services'][s_uuid]['name']
service_pid = \
s_conf['services'][s_uuid]['pid']
LOG.info(" - " + service_name)
try:
os.kill(service_pid, signal.SIGINT)
print("OLD WSTUN KILLED: " + str(wp))
LOG.info(" --> service '" + service_name
+ "' with PID " + str(service_pid)
+ " was killed; "
+ "creating new one...")
if len(wstun_process_list) != 0:
except OSError:
LOG.warning(
" - WSTUN process already killed, "
"creating new one...")
for wp in wstun_process_list:
break
if service_pid == wp.pid:
LOG.info(
" --> the tunnel for '" + service_name
+ "' already exists; killing..."
)
# 2. Create the reverse tunnel
public_port = \
s_conf['services'][s_uuid]['public_port']
local_port = \
s_conf['services'][s_uuid]['local_port']
# 1. Kill wstun process (if exists)
wstun = self._startWstun(
public_port, local_port, event="boot"
)
# No zombie alert activation
lightningrod.zombie_alert = False
LOG.debug(
"[WSTUN-RESTORE] - "
"on-finalize zombie_alert: " +
str(lightningrod.zombie_alert)
)
if wstun != None:
try:
os.kill(service_pid, signal.SIGINT)
print("OLD WSTUN KILLED: " + str(wp))
LOG.info(
" --> service '" + service_name
+ "' with PID " + str(service_pid)
+ " was killed; "
+ "creating new one...")
service_pid = wstun.pid
except OSError:
LOG.warning(
" - WSTUN process already killed, "
"creating new one...")
# 3. Update services.json file
s_conf['services'][s_uuid]['pid'] = \
service_pid
s_conf['services'][s_uuid]['updated_at'] = \
datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')
break
self._updateServiceConf(s_conf, s_uuid,
output=True)
# 2. Create the reverse tunnel
public_port = \
s_conf['services'][s_uuid]['public_port']
local_port = \
s_conf['services'][s_uuid]['local_port']
LOG.info(" --> Cloud service '" + service_name
+ "' tunnel established.")
else:
message = "Error spawning " + str(service_name) \
+ " service tunnel!"
LOG.error(" - " + message)
wstun = self._startWstunOnBoot(
public_port, local_port, event="boot")
if wstun != None:
service_pid = wstun.pid
# 3. Update services.json file
s_conf['services'][s_uuid]['pid'] = \
service_pid
s_conf['services'][s_uuid]['updated_at'] = \
datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')
self._updateServiceConf(s_conf, s_uuid,
output=True)
LOG.info(" --> Cloud service '" + service_name
+ "' tunnel established.")
else:
message = "Error spawning " + str(service_name) \
+ " service tunnel!"
LOG.error(" - " + message)
signal.signal(signal.SIGCHLD, self._zombie_hunter)
# Reactivate zombies monitoring
if not lightningrod.zombie_alert:
lightningrod.zombie_alert = True
else:
LOG.info(" --> No service tunnels to establish.")
signal.signal(signal.SIGCHLD, self._zombie_hunter)
# Reactivate zombies monitoring
if not lightningrod.zombie_alert:
lightningrod.zombie_alert = True
else:
LOG.info(" --> No service tunnels to establish.")
signal.signal(signal.SIGCHLD, self._zombie_hunter)
sock.close() # close check socket
print(" - WSTUN server is offline!")
LOG.error("WSTUN server is offline!")
def restore(self):
LOG.info("Cloud service tunnels to restore:")
@ -227,21 +274,30 @@ class ServiceManager(Module.Module):
# No zombie alert activation
lightningrod.zombie_alert = False
LOG.debug("[WSTUN-RESTORE] - Restore zombie_alert: " + str(
lightningrod.zombie_alert))
LOG.debug(
"[WSTUN-RESTORE] - Restore zombie_alert: "
+ str(lightningrod.zombie_alert)
)
# Collect all alive WSTUN proccesses
for p in psutil.process_iter():
if (p.name() == "node"):
if (p.status() == psutil.STATUS_ZOMBIE):
LOG.warning("WSTUN ZOMBIE: " + str(p))
wstun_process_list.append(p)
elif ("wstun" in p.cmdline()[1]):
LOG.warning("WSTUN ALIVE: " + str(p))
wstun_process_list.append(p)
try:
for p in psutil.process_iter():
if (p.name() == "node"):
if (p.status() == psutil.STATUS_ZOMBIE):
LOG.warning("WSTUN ZOMBIE: " + str(p))
wstun_process_list.append(p)
elif ("wstun" in p.cmdline()[1]):
LOG.warning("WSTUN ALIVE: " + str(p))
wstun_process_list.append(p)
psutil.Process(p.pid).kill()
LOG.warning(" --> PID " + str(p.pid) + " killed!")
psutil.Process(p.pid).kill()
LOG.warning(" --> PID " + str(p.pid)
+ " killed!")
except Exception as e:
LOG.error(
" --> PSUTIL [restore]: " +
"error getting wstun processes info: " + str(e)
)
LOG.debug("[WSTUN-RESTORE] - WSTUN processes to restore:\n"
+ str(wstun_process_list))
@ -270,12 +326,21 @@ class ServiceManager(Module.Module):
zombie_list = []
for p in psutil.process_iter():
if len(p.cmdline()) == 0:
if ((p.name() == "node") and
(p.status() == psutil.STATUS_ZOMBIE)):
print(" - process: " + str(p))
zombie_list.append(p.pid)
try:
for p in psutil.process_iter():
if len(p.cmdline()) == 0:
if ((p.name() == "node") and
(p.status() == psutil.STATUS_ZOMBIE)):
print(" - process: " + str(p))
zombie_list.append(p.pid)
except Exception as e:
LOG.error(
" --> PSUTIL [_zombie_hunter]: " +
"error getting wstun processes info. " +
"Please restore manually your services: " + str(e)
)
return
if len(zombie_list) == 0:
# print(" - no action required.")
@ -404,15 +469,13 @@ class ServiceManager(Module.Module):
:return: JSON Services configuration
"""
try:
with open(s_conf_FILE) as settings:
s_conf = json.load(settings)
except Exception as err:
LOG.error(" --> Parsing error in "
+ s_conf_FILE + ": " + str(err))
LOG.error(" --> Parsing error in " + s_conf_FILE + ": " + str(err))
if os.path.isfile(s_conf_FILE):
@ -437,7 +500,7 @@ class ServiceManager(Module.Module):
return s_conf
def _wstunMon(self, wstun):
def _wstunMon(self, wstun, local_port):
wfd_check = True
@ -503,6 +566,8 @@ class ServiceManager(Module.Module):
event_notifier = pyinotify.ThreadedNotifier(
watch_manager, EventProcessor()
)
event_notifier.setName("TN-" + str(local_port))
WS_MON_LIST[str(local_port)] = event_notifier
watch_this = os.path.abspath(
"/proc/" + str(wstun.pid) + "/fd/" + str(wstun_fd)
@ -512,13 +577,27 @@ class ServiceManager(Module.Module):
def _startWstun(self, public_port, local_port, event="no-set"):
import socket
count_ws = 0
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(4)
result = sock.connect_ex((self.wstun_ip, int(self.wstun_port)))
if result == 0:
global ws_server_alive
ws_server_alive = sock.connect_ex(
(self.wstun_ip, int(self.wstun_port))
)
while(ws_server_alive != 0 and count_ws < 5):
count_ws = count_ws + 1
LOG.warning(
"WSTUN server is offline! Retry " + str(count_ws) + "/5..."
)
time.sleep(randint(3, 6))
global ws_server_alive
ws_server_alive = sock.connect_ex(
(self.wstun_ip, int(self.wstun_port))
)
if ws_server_alive == 0:
sock.close() # close check socket
@ -542,12 +621,22 @@ class ServiceManager(Module.Module):
# WSTUN MON
# #############################################################
Thread(
target=self._wstunMon,
args=(wstun,)
).start()
try:
if event != "enable":
WS_MON_LIST[str(local_port)].stop()
except Exception as err:
LOG.error("Error stopping WSTUN monitor: " + str(err))
# self._wstunMon(wstun)
wsmon = Thread(
target=self._wstunMon,
name="THR-" + str(local_port),
args=(wstun, local_port, )
)
wsmon.start()
# print(threading.enumerate())
print(WS_MON_LIST)
# #############################################################
@ -561,25 +650,89 @@ class ServiceManager(Module.Module):
return wstun
def _startWstunOnBoot(self, public_port, local_port, event="no-set"):
opt_reverse = "-r" + str(public_port) + ":127.0.0.1:" + str(
local_port)
try:
wstun = subprocess.Popen(
[CONF.services.wstun_bin, opt_reverse, self.wstun_url],
stdout=subprocess.PIPE
)
if (event != "boot"):
print("WSTUN start event:")
cmd_print = 'WSTUN exec: ' + str(CONF.services.wstun_bin) \
+ opt_reverse + ' ' + self.wstun_url
print(" - " + str(cmd_print))
LOG.debug(cmd_print)
# WSTUN MON
# #############################################################
wsmon = Thread(
target=self._wstunMon,
name="THR-" + str(local_port),
args=(wstun, local_port,)
)
wsmon.start()
# #############################################################
except Exception as err:
LOG.error("Error spawning WSTUN process: " + str(err))
wstun = None
return wstun
async def ServicesStatus(self):
rpc_name = utils.getFuncName()
LOG.info("RPC " + rpc_name + " CALLED")
thr_list = str(threading.enumerate())
# print(WS_MON_LIST)
print(thr_list + "\n" + str(WS_MON_LIST))
w_msg = WM.WampSuccess(thr_list)
return w_msg.serialize()
def _updateServiceConf(self, s_conf, s_uuid, output=True):
# Apply the changes to services.json
with open(s_conf_FILE, 'w') as f:
json.dump(s_conf, f, indent=4)
if s_conf == "":
if output:
LOG.info(" - service updated:\n" + json.dumps(
s_conf['services'][s_uuid],
indent=4,
sort_keys=True
))
else:
LOG.info(" - services.json file updated!")
LOG.error(" - ERROR new services.json content is empty: " +
"Restoring backup.")
# Backup json file before update
os.system(
'cp ' + s_conf_FILE + ' ' + s_conf_FILE + '.bkp'
)
# Restore backup json file on error
os.system(
'cp ' + s_conf_FILE + '.bkp ' + s_conf_FILE
)
else:
# Apply the changes to services.json
with open(s_conf_FILE, 'w') as f:
json.dump(s_conf, f, indent=4)
print(s_conf)
if output:
LOG.info(" - service updated:\n" + json.dumps(
s_conf['services'][s_uuid],
indent=4,
sort_keys=True
))
else:
LOG.info(" - services.json file updated!")
# Backup json file before update
os.system(
'cp ' + s_conf_FILE + ' ' + s_conf_FILE + '.bkp'
)
async def ServiceEnable(self, service, public_port):
@ -647,8 +800,8 @@ class ServiceManager(Module.Module):
w_msg = WM.WampSuccess(message)
else:
message = "Error spawning " + str(service_name) \
+ " service tunnel!"
message = "Error spawning '" + str(service_name) \
+ "' service tunnel!"
LOG.error(" - " + message)
w_msg = WM.WampError(message)
@ -822,8 +975,7 @@ class ServiceManager(Module.Module):
s_conf['services'][s_uuid]['updated_at'] = \
datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')
self._updateServiceConf(s_conf, s_uuid,
output=True)
self._updateServiceConf(s_conf, s_uuid, output=True)
message = "service " + str(service_name) \
+ " restored on port " \
@ -898,3 +1050,41 @@ class ServiceManager(Module.Module):
w_msg = WM.WampError(message)
return w_msg.serialize()
def services_list():
try:
s_list = ""
with open(s_conf_FILE) as settings:
s_conf = json.load(settings)
for s_uuid in s_conf['services']:
s_service = str(s_conf['services'][s_uuid]['name']) \
+ " - " + str(s_conf['services'][s_uuid]['public_port']) \
+ " - " + str(s_conf['services'][s_uuid]['local_port'])
s_list = s_list + "<li>" + s_service + "</li>"
except Exception as err:
LOG.error("Error getting services list: " + str(err))
s_list = str(err)
return s_list
def wstun_status():
if(wstun_ip != None) and (wstun_port != None):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(4)
global ws_server_alive
ws_server_alive = sock.connect_ex((wstun_ip, int(wstun_port)))
sock.close() # close check socket
else:
ws_server_alive = "N/A"
return ws_server_alive

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,330 @@
/*!
* Bootstrap Reboot v4.1.0 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v4.1.0 (https://getbootstrap.com/)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,24 @@
<!doctype html>
<title>{% block title %}{% endblock %} - LR</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<div class="container">
<nav class="navbar navbar-expand-sm bg-dark navbar-dark justify-content-center">
<ul class="navbar-nav">
<li class="nav-item">
<a class="navbar-brand" href="/">Home</a>
<a class="navbar-brand" href="/status">Status</a>
<a class="navbar-brand" href="/network">Network</a>
</li>
</ul>
</nav>
<header class="text-center" style="margin-top: 20px;">
{% block header %}{% endblock %}
</header>
{% block content %}{% endblock %}
</div>

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block header %}
<h3>{% block title %}First configuration{% endblock %}</h3>
{% endblock %}
{% block content %}
<div class="form-group">
<form method="post">
<div class="form-group">
<label for="urlwagent">Registration Agent: </label>
<input class="form-control" name="urlwagent" id="urlwagent" value="{{ urlwagent }}" required></div>
<div class="form-group"></div>
<label for="code">Registration Code:</label>
<input class="form-control" name="code" id="code" value="{{ code }}" required></div>
<input class="btn btn-success" type="submit" value="CONFIGURE">
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Welcome in Lightning-rod{% endblock %}</h1>
{% endblock %}
{% block content %}
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Networking{% endblock %}</h1>
{% endblock %}
{% block content %}
<div class="jumbotron">
<h4> Ifconfig </h4>
<pre>
{% autoescape false %}
{{ ifconfig }}
{% endautoescape %}
</pre>
</div>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Status{% endblock %}</h1>
{% endblock %}
{% block content %}
<div class="jumbotron">
<h2>Board info @ {{ timestamp }}</h2>
<br>
<h4> Iotronic </h4>
<li> Iotronic connection status: {{iotronic_status}}</li>
<li>Name: {{ board_name }} </li>
<li>UUID: {{ board_id }} </li>
<li>Registartion status: {{ board_reg_status }}</li>
<li>WAMP Agent: {{ wagent }} </li>
<li>Session ID: {{ session_id }} </li>
<br>
<h4> WSTUN </h4>
<li>Status: {{ wstun_status }} </li>
<li>Services: <br>
<ul>
{% autoescape false %}
{{service_list}}
{% endautoescape %}
</ul>
</li>
</div>
{% endblock %}

View File

@ -17,7 +17,6 @@ __author__ = "Nicola Peditto <n.peditto@gmail.com>"
from iotronic_lightningrod.config import package_path
from iotronic_lightningrod.lightningrod import RPC_proxies
from iotronic_lightningrod.lightningrod import SESSION
from iotronic_lightningrod.modules import Module
from iotronic_lightningrod.modules import utils
import iotronic_lightningrod.wampmessage as WM
@ -58,6 +57,8 @@ class WebServiceManager(Module.Module):
LOG.info(" - Proxy used: " + CONF.webservices.proxy.upper())
self.session = session
try:
proxy_type = CONF.webservices.proxy
path = package_path + "/modules/proxies/" + proxy_type + ".py"
@ -168,11 +169,11 @@ class WebServiceManager(Module.Module):
# LOG.debug(" --> " + str(rpc_addr))
if not meth[0].startswith('_'):
SESSION.register(meth[1], rpc_addr)
self.session.register(meth[1], rpc_addr)
LOG.info(" --> " + str(meth[0]))
async def ExposeWebservice(self, board_dns, service_dns,
local_port, dns_list):
async def ExposeWebservice(self,
board_dns, service_dns, local_port, dns_list):
rpc_name = utils.getFuncName()
LOG.info("RPC " + rpc_name + " CALLED")

View File

@ -11,3 +11,4 @@ oslo.config>=5.1.0 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
pyinotify>=0.9.6;sys_platform!='win32' and sys_platform!='darwin' and sys_platform!='sunos5' # MIT
pyOpenSSL>=16.2.0 # Apache-2.0
Flask!=0.11,>=1.0.2 # BSD

View File

@ -67,6 +67,7 @@ s4t.modules =
service = iotronic_lightningrod.modules.service_manager:ServiceManager
network = iotronic_lightningrod.modules.network_manager:NetworkManager
webservice = iotronic_lightningrod.modules.webservice_manager:WebServiceManager
rest = iotronic_lightningrod.modules.rest_manager:RestManager
[options]
build_scripts =