From fa37f77bcbe66e4d56d46ed849fd40ab972f5d66 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 17 Nov 2019 20:01:03 +0100 Subject: [PATCH 1/3] [IMP] set fields and decorators --- oot/__init__.py | 2 ++ oot/api.py | 15 +++++++++ oot/connection/consumer.py | 4 ++- oot/fields.py | 16 +++++++++ oot/oot.py | 13 ++++++++ oot/oot_amqp.py | 66 ++++++++++++++++++++++++-------------- oot/oot_multiprocess.py | 19 ++++++++++- 7 files changed, 109 insertions(+), 26 deletions(-) create mode 100644 oot/api.py create mode 100644 oot/fields.py diff --git a/oot/__init__.py b/oot/__init__.py index cd4aed2..090c879 100644 --- a/oot/__init__.py +++ b/oot/__init__.py @@ -4,3 +4,5 @@ from .oot_amqp import OotAmqp from . import device from . import upgrade +from . import api +from .fields import Field diff --git a/oot/api.py b/oot/api.py new file mode 100644 index 0000000..13e4111 --- /dev/null +++ b/oot/api.py @@ -0,0 +1,15 @@ +def attrsettermethod(method, attr, value): + return setattr(method, attr, value) or method + + +def attrsetter(attr, value): + """ Return a function that sets ``attr`` on its argument and returns it. """ + return lambda method: attrsettermethod(method, attr, value) + + +def oot(method): + return attrsettermethod(method, "_oot_process", True) + + +def amqp(command): + return attrsetter("_amqp_command", command) diff --git a/oot/connection/consumer.py b/oot/connection/consumer.py index 36af167..5baa552 100644 --- a/oot/connection/consumer.py +++ b/oot/connection/consumer.py @@ -44,7 +44,9 @@ def run(self): channel.basic_qos(prefetch_count=1) channel.queue_declare(self.queue, **self.queue_options) channel.exchange_declare( - exchange=self.exchange_name, exchange_type=self.exchange_type, passive=True, + exchange=self.exchange_name, + exchange_type=self.exchange_type, + passive=True, ) for routing_key in self.options: channel.queue_bind( diff --git a/oot/fields.py b/oot/fields.py new file mode 100644 index 0000000..644f70f --- /dev/null +++ b/oot/fields.py @@ -0,0 +1,16 @@ +class Field: + def __init__(self, name, description=False, placeholder=False, required=True): + self.name = name + self.description = description + self.placeholder = placeholder + self.required = required + + def generate(self): + result = {"name": self.name} + if self.description: + result["description"] = self.description + if self.placeholder: + result["placeHolder"] = self.placeholder + if self.required: + result["required"] = self.required + return result diff --git a/oot/oot.py b/oot/oot.py index 4bac479..4383a49 100644 --- a/oot/oot.py +++ b/oot/oot.py @@ -3,9 +3,11 @@ import os import subprocess import traceback +from inspect import getmembers from io import StringIO from .connection import OdooConnectionIot +from .fields import Field from .server.server import initialize _logger = logging.getLogger(__name__) @@ -22,6 +24,7 @@ class Oot: extra_tools_template = "extra_tools.html" result_template = "result.html" ssid = "OotDevice" + fields = {} connection = False connection_data = {} @@ -43,6 +46,13 @@ def __init__(self, connection): for key in clss.fields: if key not in self._fields: self._fields[key] = clss.fields[key] + + def is_field(item): + return not callable(item) and isinstance(item, Field) + + cls = type(self) + for attr, item in getmembers(cls, is_field): + self._fields[attr] = item.generate() if isinstance(connection, dict): self.connection_data = connection self.generate_connection() @@ -86,6 +96,9 @@ def check_key(self, key, **kwargs): def generate_connection(self): self.connection = self.connection_class(self.connection_data) + for field in self._fields: + setattr(self, field, self.connection_data.get(field)) + self.name = self.connection_data.get("name") def run(self, **kwargs): if not self.connection and self.connection_path: diff --git a/oot/oot_amqp.py b/oot/oot_amqp.py index 3a8412a..4a8a02a 100644 --- a/oot/oot_amqp.py +++ b/oot/oot_amqp.py @@ -1,12 +1,15 @@ import json import logging import uuid +from inspect import getmembers from multiprocessing import Process import pika import psutil +from . import api from .connection.consumer import Consumer +from .fields import Field from .oot_multiprocess import OotMultiProcessing _logger = logging.getLogger(__name__) @@ -14,19 +17,17 @@ class OotAmqp(OotMultiProcessing): consumer = False - fields = { - "amqp_host": { - "name": "Host for AMQP", - "placeHolder": "amqp://user:password@hostname:port", - "required": False, - }, - "amqp_name": { - "name": "Unique name for this device on AMQP, if " - "blank, name of initial generated name on odoo will be used", - "required": False, - }, - "amqp_check_key": {"name": "Key For System AMQP Calls", "required": False}, - } + amqp_host = Field( + name="Host for AMQP", + placeholder="amqp://user:password@hostname:port", + required=False, + ) + amqp_name = Field( + name="Unique name for this device on AMQP, if " + "blank, name of initial generated name on odoo will be used", + required=False, + ) + amqp_check_key = Field(name="Key For System AMQP Calls", required=False) def amqp_machine_stats(self, **kwargs): return { @@ -35,12 +36,31 @@ def amqp_machine_stats(self, **kwargs): "temp": psutil.sensors_temperatures()["cpu-thermal"][0].current, } + @api.amqp("reboot") + def amqp_reboot(self, channel, basic_deliver, properties, body): + if body.decode("utf-8") == self.amqp_check_key: + return self.reboot() + + @api.amqp("ssh") + def amq_ssh(self, channel, basic_deliver, properties, body): + if body.decode("utf-8") == self.amqp_check_key: + return self.toggle_service_function("ssh")() + + @api.amqp("stats") + def amqp_stats(self, channel, basic_deliver, properties, body): + if body.decode("utf-8") == self.amqp_check_key: + return self.amqp_machine_stats() + def get_default_amqp_options(self): - return { - "reboot": self.amqp_key_check(self.reboot), - "ssh": self.amqp_key_check(self.toggle_service_function("ssh")), - "stats": self.amqp_key_check(self.amqp_machine_stats), - } + result = {} + + def is_command(func): + return callable(func) and hasattr(func, "_amqp_command") + + cls = type(self) + for _attr, func in getmembers(cls, is_command): + result[func._amqp_command] = self.get_callback_function(func) + return result def amqp_key_check(self, funct, key=False): if not key: @@ -60,14 +80,12 @@ def new_func(channel, basic_deliver, properties, body): def generate_connection(self): super().generate_connection() - amqp_host = self.connection_data.get("amqp_host", False) + amqp_host = self.amqp_host if amqp_host: - amqp_options = self.connection_data.get("amqp_options", []) - self.amqp_name = self.connection_data.get( - "amqp_name", self.connection_data.get("name") - ) + amqp_options = [] + self.amqp_name = self.amqp_name or self.name self.routing_base = "oot.%s" % self.amqp_name - self.amqp_check_key = self.connection_data.get("amqp_check_key") + self.amqp_check_key = self.amqp_check_key amqp_options += self.get_default_amqp_options() self.consumer = Consumer( amqp_host, diff --git a/oot/oot_multiprocess.py b/oot/oot_multiprocess.py index 2a8f558..de4bba9 100644 --- a/oot/oot_multiprocess.py +++ b/oot/oot_multiprocess.py @@ -1,4 +1,5 @@ import time +from inspect import getmembers from multiprocessing import Process, Queue from multiprocessing.queues import Queue as QueueClass @@ -29,8 +30,24 @@ def execute_function(self, function, *args, queue=False, **kwargs): if value: queue.put(value) + def get_callback_function(self, function): + def callback(*args, **kwargs): + return function(self, *args, **kwargs) + + return callback + + def _get_functions(self): + def is_command(func): + return callable(func) and hasattr(func, "_oot_process") + + functions = [] + cls = type(self) + for _attr, func in getmembers(cls, is_command): + functions.append([self.get_callback_function(func)]) + return self.functions + functions + def _run(self, **kwargs): - for function in self.functions: + for function in self._get_functions(): process = Process( target=self.execute_function, args=function, From bd18467099a063b54768c3be449f74935d6d4561 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 29 Dec 2020 23:46:55 +0100 Subject: [PATCH 2/3] [IMP] oot: Define some documentation --- README.rst | 87 +++++++++++++++++++++++++++++++++++++++++ oot/__init__.py | 1 - oot/api.py | 2 + oot/fields.py | 2 + oot/oot.py | 44 ++++++++++++++++----- oot/oot_amqp.py | 8 +++- oot/oot_multiprocess.py | 6 ++- oot/server/server.py | 46 ++++++++++++---------- 8 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..0c821cc --- /dev/null +++ b/README.rst @@ -0,0 +1,87 @@ +==================== +Oot - Odoo of things +==================== + +The main goal of this package is to provide the necessary tools to configure a device +that will send information to odoo. + +Device +------ + +A device is a processing unit that capable to send information to odoo. +All devices must be registered on odoo. +In order to send the data, it will use and HTTP Protocol. + +Create your first device +------------------------ + +Oot class +~~~~~~~~~ + +First we define a class for the Oot to manage it configuration. +For example: + +.. code-block:: python + + from oot import OotAmqp, api, Field + from oot.device import CardReader + import time + + + class DemoOot(OotAmqp): + """We are using AMQP as it allows to define some extra configuration""" + template = "demo_template" + # Template to be used on odoo + oot_input = "demo_input" + # Input to be used on odoo + + # Now we define the configuration fields + admin_id = Field(name="Admin key", required=True) + reader = CardReader(spd=10000) + + @api.oot + def get_data_mfrc522(self, **kwargs): + """We will return the card if a card is readed. Otherwise, we will wait""" + time.sleep(5.0) + while True: + uid = self.reader.scan_card() + if uid: + return uid + +Launcher +~~~~~~~~ + +The second part will be defining a launcher. +The launcher is usually a folder that will contain: + +* main file to execute +* a config for the logging +* a `log` folder that will store logs +* a `data` folder that will contain the configuration file + +We will try to make an example: + +.. code-block:: python + + import os + import logging.config + from ootdemo import DemoOot + + path = os.path.dirname(os.path.realpath(__file__)) + + log_folder = path + "/log" + + if not os.path.isdir(log_folder): + os.mkdir(log_folder) + + logging.config.fileConfig(path + "/ras.logging.conf") + + data_folder = path + "/data" + + if not os.path.isdir(data_folder): + os.mkdir(data_folder) + + DemoOot(data_folder + "/data.json").run() + + +The example provided on this file will only work on a Raspberry PI with an MFRC522. diff --git a/oot/__init__.py b/oot/__init__.py index 090c879..da4c3ae 100644 --- a/oot/__init__.py +++ b/oot/__init__.py @@ -2,7 +2,6 @@ from .oot import Oot from .oot_multiprocess import OotMultiProcessing from .oot_amqp import OotAmqp -from . import device from . import upgrade from . import api from .fields import Field diff --git a/oot/api.py b/oot/api.py index 13e4111..04fb3bb 100644 --- a/oot/api.py +++ b/oot/api.py @@ -8,8 +8,10 @@ def attrsetter(attr, value): def oot(method): + """Marks the method as a process that will send data to Odoo""" return attrsettermethod(method, "_oot_process", True) def amqp(command): + """Marks the method as and amqp command""" return attrsetter("_amqp_command", command) diff --git a/oot/fields.py b/oot/fields.py index 644f70f..e4788ea 100644 --- a/oot/fields.py +++ b/oot/fields.py @@ -1,4 +1,6 @@ class Field: + """This class is intended to be used to define fields for the configuration + """ def __init__(self, name, description=False, placeholder=False, required=True): self.name = name self.description = description diff --git a/oot/oot.py b/oot/oot.py index 4383a49..c448645 100644 --- a/oot/oot.py +++ b/oot/oot.py @@ -16,6 +16,13 @@ class Oot: + """Base class for Oot definition + + It will instantiate an Access Point if it does not find a configuration. + In it we will be able to define all the necessary information and configure it. + + Otherwise, it will be used to send information to a Odoo System + """ connection_class = OdooConnectionIot oot_input = False template = False @@ -24,6 +31,7 @@ class Oot: extra_tools_template = "extra_tools.html" result_template = "result.html" ssid = "OotDevice" + _ignore_access_point = False fields = {} connection = False @@ -60,18 +68,31 @@ def is_field(item): self.connection_path = connection def initialize(self): - initialize(self) + initialize(self, no_access_point=self._ignore_access_point) def get_data(self, **kwargs): + """This is intended to be overridden by each configuration. + It must return the data for odoo. + If a tuple is returned, it will send the first value and use the second one + as extra arguments, so it must be a dictionary + """ pass def process_result(self, key, result, **kwargs): + """To be executed after sending the information to odoo. + This is the function that checks that the response is valid. + + :param key: The sent value to odoo + :param result: Result from odoo. Usually a dict + """ pass def exit(self, **kwargs): + """Executed when exiting the loop""" pass def no_key(self, **kwargs): + """Executed if `get_data` returns no value""" pass def checking_connection(self): @@ -90,17 +111,20 @@ def start_connection(self, server, access_point): pass def check_key(self, key, **kwargs): + """Checks the result from the Oot to Odoo""" return self.connection.execute_action( key, oot_input=kwargs.get("oot_input", self.oot_input) ) def generate_connection(self): + """Initializes the connection configuraton to odoo""" self.connection = self.connection_class(self.connection_data) for field in self._fields: setattr(self, field, self.connection_data.get(field)) self.name = self.connection_data.get("name") def run(self, **kwargs): + """Loop to execute""" if not self.connection and self.connection_path: if not os.path.exists(self.connection_path): self.initialize() @@ -136,7 +160,8 @@ def _run(self, **kwargs): self.exit(**kwargs) raise - def check_service(self, service, **kwargs): + def _check_service(self, service, **kwargs): + """Checks if a service is active. Only works on debian""" stat = subprocess.Popen( "systemctl is-active --quiet %s; echo $?" % service, stdout=subprocess.PIPE, @@ -146,23 +171,24 @@ def check_service(self, service, **kwargs): return stat[0] == b"0\n" def toggle_service_function(self, service, **kwargs): + """Returns the function that changes the state of a service""" def toggle(**kwtargs): - return self.toggle_service(service, **kwtargs) + return self._toggle_service(service, **kwtargs) return toggle - def toggle_service(self, service, **kwargs): - if self.check_service(service): - self.stop_service(service) + def _toggle_service(self, service, **kwargs): + if self._check_service(service): + self._stop_service(service) else: - self.start_service(service) + self._start_service(service) - def stop_service(self, service, disable=True, **kwargs): + def _stop_service(self, service, disable=True, **kwargs): subprocess.Popen(["systemctl", "stop", service]).communicate() if disable: subprocess.Popen(["systemctl", "disable", service]).communicate() - def start_service(self, service, enable=True, **kwargs): + def _start_service(self, service, enable=True, **kwargs): subprocess.Popen(["systemctl", "enable", service]).communicate() if enable: subprocess.Popen(["systemctl", "start", service]).communicate() diff --git a/oot/oot_amqp.py b/oot/oot_amqp.py index 4a8a02a..d4c6558 100644 --- a/oot/oot_amqp.py +++ b/oot/oot_amqp.py @@ -28,12 +28,18 @@ class OotAmqp(OotMultiProcessing): required=False, ) amqp_check_key = Field(name="Key For System AMQP Calls", required=False) + _temperature_field = "cpu-thermal" def amqp_machine_stats(self, **kwargs): + temperatures = psutil.sensors_temperatures() + if self._temperature_field in temperatures: + temperature = temperatures[self._temperature_field][0].current + else: + temperature = False return { "cpu_percentage": psutil.cpu_percent(), "virtual_memory_percentage": psutil.virtual_memory().percent, - "temp": psutil.sensors_temperatures()["cpu-thermal"][0].current, + "temp": temperature, } @api.amqp("reboot") diff --git a/oot/oot_multiprocess.py b/oot/oot_multiprocess.py index de4bba9..ce6336f 100644 --- a/oot/oot_multiprocess.py +++ b/oot/oot_multiprocess.py @@ -7,6 +7,10 @@ class OotMultiProcessing(Oot): + """This allows to use multiple functions that can send data to odoo. + In order to add a function we can add it on the functions or add a decorator + the decorator `api.oot` on the class function + """ functions = [] queue = Queue() jobs = [] @@ -24,7 +28,7 @@ def start_execute_function(self, function, *args, queue=False, **kwargs): def execute_function(self, function, *args, queue=False, **kwargs): if not isinstance(queue, QueueClass): raise Exception("A queue is required") - self.start_execute_function(function, *args, queue=False, **kwargs) + self.start_execute_function(function, *args, queue=queue, **kwargs) while True: value = function(*args, **kwargs) if value: diff --git a/oot/server/server.py b/oot/server/server.py index a2452f3..8f99ec6 100644 --- a/oot/server/server.py +++ b/oot/server/server.py @@ -50,7 +50,7 @@ def shutdown(self): self.srv.shutdown() -def initialize(oot): +def initialize(oot, no_access_point=False): app = Flask( __name__, template_folder="%s/templates" % oot.folder, @@ -61,7 +61,7 @@ def initialize(oot): connected_eth = is_interface_up("eth0") parameters = {"interfaces": []} - if not connected_eth: + if not connected_eth and not no_access_point: parameters["interfaces"] = [cell.ssid for cell in Cell.all("wlan0")] @app.route("/") @@ -73,7 +73,7 @@ def form(): fields=oot._fields or {}, interfaces=parameters["interfaces"], countries=COUNTRIES, - connected=connected_eth, + connected=connected_eth or no_access_point, ) @app.route("/extra_tools", methods=["POST", "GET"]) @@ -97,7 +97,7 @@ def result(): if value: value = value[0] result[key] = value - if connected_eth: + if connected_eth or no_access_point: check_configuration(result, result_dic["odoo_link"][0], oot) with open(oot.connection_path, "w+") as outfile: json.dump(result, outfile) @@ -118,8 +118,11 @@ def result(): oot.result_template, result=result, fields=oot.fields ) - _logger.info("Configuring Access Point") - access_point = OotAccessPoint(ssid=oot.ssid, ip=DEFAULT_IP) + if no_access_point: + access_point = False + else: + _logger.info("Configuring Access Point") + access_point = OotAccessPoint(ssid=oot.ssid, ip=DEFAULT_IP) interfaces = is_interface_up("wlan0") or [] first_start = not any( interface.get("addr", False) == DEFAULT_IP for interface in interfaces @@ -130,33 +133,36 @@ def result(): def process(oot, access_point, app, parameters, connected_eth, first_start=False): server = ServerThread(app) try: - access_point.start() - if first_start: - _logger.info( - "On first start we must create twice the access point for configuration" - ) - access_point.stop() + if access_point: access_point.start() - _logger.info("Access Point configured") - if not access_point.is_running(): - raise Exception("Access point could not be raised") + if first_start: + _logger.info( + "On first start we must create twice the access point for configuration" + ) + access_point.stop() + access_point.start() + _logger.info("Access Point configured") + if not access_point.is_running(): + raise Exception("Access point could not be raised") server.start() oot.start_connection(server, access_point) - while not parameters.get("processed") and access_point.is_running(): + while not parameters.get("processed") and (not access_point or access_point.is_running()): pass _logger.info("Configuration has been launched. Waiting for execution") time.sleep(10) except KeyboardInterrupt: server.shutdown() - access_point.stop() + if access_point: + access_point.stop() raise server.shutdown() _logger.info("Server closed") - access_point.stop() - _logger.info("Access Point closed") + if access_point: + access_point.stop() + _logger.info("Access Point closed") if connected_eth: return - if not connected_eth and parameters.get("result_data", False): + if not connected_eth and parameters.get("result_data", False) and access_point: with open("/etc/wpa_supplicant/wpa_supplicant.conf", "w") as f: f.write("ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\n") f.write("update_config=1\n") From c69b7270f26192df0e0e9f461d6593aafe55a72f Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 30 Dec 2020 15:12:25 +0100 Subject: [PATCH 3/3] [IMP] Add a demo example --- demo/file_reader/launcher.py | 19 ++++++++++ demo/file_reader/logging.conf | 35 ++++++++++++++++++ demo/file_reader/ootdemo/__init__.py | 1 + demo/file_reader/ootdemo/ootdemo.py | 55 ++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 demo/file_reader/launcher.py create mode 100644 demo/file_reader/logging.conf create mode 100644 demo/file_reader/ootdemo/__init__.py create mode 100644 demo/file_reader/ootdemo/ootdemo.py diff --git a/demo/file_reader/launcher.py b/demo/file_reader/launcher.py new file mode 100644 index 0000000..e5a7dea --- /dev/null +++ b/demo/file_reader/launcher.py @@ -0,0 +1,19 @@ +import os +import logging.config +from ootdemo import DemoOot + +path = os.path.dirname(os.path.realpath(__file__)) + +log_folder = path + "/log" + +if not os.path.isdir(log_folder): + os.mkdir(log_folder) + +logging.config.fileConfig(path + "/logging.conf") + +data_folder = path + "/data" + +if not os.path.isdir(data_folder): + os.mkdir(data_folder) + +DemoOot(data_folder + "/data.json", data_folder + "/read").run() diff --git a/demo/file_reader/logging.conf b/demo/file_reader/logging.conf new file mode 100644 index 0000000..f8ec9d6 --- /dev/null +++ b/demo/file_reader/logging.conf @@ -0,0 +1,35 @@ +[loggers] +keys=root,oot,ootdemo + +[handlers] +keys=consoleHandler + +[formatters] +keys=consoleFormatter + +[logger_root] +level=INFO +handlers=consoleHandler + +[logger_oot] +level=DEBUG +handlers=consoleHandler +qualname=oot +propagate=0 + +[logger_ootdemo] +level=DEBUG +handlers=consoleHandler +qualname=ootdemo +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=consoleFormatter +args=(sys.stdout,) + +[formatter_consoleFormatter] +format=%(levelname)s: %(message)s +datefmt= + diff --git a/demo/file_reader/ootdemo/__init__.py b/demo/file_reader/ootdemo/__init__.py new file mode 100644 index 0000000..4470780 --- /dev/null +++ b/demo/file_reader/ootdemo/__init__.py @@ -0,0 +1 @@ +from .ootdemo import DemoOot \ No newline at end of file diff --git a/demo/file_reader/ootdemo/ootdemo.py b/demo/file_reader/ootdemo/ootdemo.py new file mode 100644 index 0000000..079a624 --- /dev/null +++ b/demo/file_reader/ootdemo/ootdemo.py @@ -0,0 +1,55 @@ +from oot import OotAmqp, api, Field +import os +import logging +import time + +_logger = logging.getLogger(__name__) + + +class FileReader: + def __init__(self, file_path, delay=15): + self.file_path = file_path + self.delay = delay + + def scan(self): + while True: + if not os.path.exists(self.file_path): + time.sleep(0.1) + continue + if os.path.getmtime(self.file_path) > time.time() - self.delay: + time.sleep(0.1) + continue + with open(self.file_path, "r") as f: + data = f.read() + os.remove(self.file_path) + return data + + +class DemoOot(OotAmqp): + """We are using AMQP as it allows to define some extra configuration""" + template = "demo_template" + # Template to be used on odoo + oot_input = "demo_input" + _ignore_access_point = True + # Input to be used on odoo + + # Now we define the configuration fields + admin_id = Field(name="Admin key", required=True) + + def __init__(self, connection, file_path): + super(DemoOot, self).__init__(connection) + self.reader = FileReader(file_path) + + @api.oot + def get_data_mfrc522(self, **kwargs): + """We will return the card if a card is readed. Otherwise, we will wait""" + time.sleep(5.0) + while True: + uid = self.reader.scan() + if uid: + _logger.info("Sending %s" % uid) + return uid + + def process_result(self, key, result, **kwargs): + _logger.info("For %s, we received the following result: %s" % (key, result)) + return super(DemoOot, self).process_result(key, result, **kwargs)