527 lines
25 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import jinja2
import platform
import logging
import os
from pathlib import Path
import socket
import subprocess
import sys
import threading
from odoo import http, service, tools
from odoo.http import Response, request
from odoo.addons.hw_drivers.connection_manager import connection_manager
from odoo.addons.hw_drivers.main import iot_devices
from odoo.addons.hw_drivers.tools import helpers
from odoo.addons.web.controllers.home import Home
from odoo.tools.misc import file_path
_logger = logging.getLogger(__name__)
#----------------------------------------------------------
# Controllers
#----------------------------------------------------------
if hasattr(sys, 'frozen'):
# When running on compiled windows binary, we don't have access to package loader.
path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views'))
loader = jinja2.FileSystemLoader(path)
else:
loader = jinja2.PackageLoader('odoo.addons.hw_posbox_homepage', "views")
jinja_env = jinja2.Environment(loader=loader, autoescape=True)
jinja_env.filters["json"] = json.dumps
homepage_template = jinja_env.get_template('homepage.html')
server_config_template = jinja_env.get_template('server_config.html')
wifi_config_template = jinja_env.get_template('wifi_config.html')
handler_list_template = jinja_env.get_template('handler_list.html')
remote_connect_template = jinja_env.get_template('remote_connect.html')
configure_wizard_template = jinja_env.get_template('configure_wizard.html')
six_payment_terminal_template = jinja_env.get_template('six_payment_terminal.html')
list_credential_template = jinja_env.get_template('list_credential.html')
upgrade_page_template = jinja_env.get_template('upgrade_page.html')
class IoTboxHomepage(Home):
def __init__(self):
super(IoTboxHomepage,self).__init__()
self.updating = threading.Lock()
def clean_partition(self):
subprocess.check_call(['sudo', 'bash', '-c', '. /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/upgrade.sh; cleanup'])
def get_six_terminal(self):
terminal_id = helpers.read_file_first_line('odoo-six-payment-terminal.conf')
return terminal_id or 'Not Configured'
def get_homepage_data(self):
hostname = str(socket.gethostname())
if platform.system() == 'Linux':
ssid = helpers.get_ssid()
wired = helpers.read_file_first_line('/sys/class/net/eth0/operstate')
else:
wired = 'up'
if wired == 'up':
network = 'Ethernet'
elif ssid:
if helpers.access_point():
network = 'Wifi access point'
else:
network = 'Wifi : ' + ssid
else:
network = 'Not Connected'
is_certificate_ok, certificate_details = helpers.get_certificate_status()
iot_device = []
for device in iot_devices:
iot_device.append({
'name': iot_devices[device].device_name + ' : ' + str(iot_devices[device].data['value']),
'type': iot_devices[device].device_type.replace('_', ' '),
'identifier': iot_devices[device].device_identifier,
})
return {
'hostname': hostname,
'ip': helpers.get_ip(),
'mac': helpers.get_mac_address(),
'iot_device_status': iot_device,
'server_status': helpers.get_odoo_server_url() or 'Not Configured',
'pairing_code': connection_manager.pairing_code,
'six_terminal': self.get_six_terminal(),
'network_status': network,
'version': helpers.get_version(),
'system': platform.system(),
'is_certificate_ok': is_certificate_ok,
'certificate_details': certificate_details,
}
@http.route()
def index(self):
wifi = Path.home() / 'wifi_network.txt'
remote_server = Path.home() / 'odoo-remote-server.conf'
if (wifi.exists() == False or remote_server.exists() == False) and helpers.access_point():
return "<meta http-equiv='refresh' content='0; url=http://" + helpers.get_ip() + ":8069/steps'>"
else:
return homepage_template.render(self.get_homepage_data())
@http.route('/list_handlers', type='http', auth='none', website=True, csrf=False, save_session=False)
def list_handlers(self, **post):
AVAILABLE_LOG_LEVELS = ('debug', 'info', 'warning', 'error')
if request.httprequest.method == 'POST':
IOT_LOGGING_PREFIX = 'iot-logging-'
INTERFACE_PREFIX = 'interface-'
DRIVER_PREFIX = 'driver-'
AVAILABLE_LOG_LEVELS_WITH_PARENT = AVAILABLE_LOG_LEVELS + ('parent',)
need_config_save = False
for post_request_key, log_level_or_parent in post.items():
if not post_request_key.startswith(IOT_LOGGING_PREFIX):
# probably a new post request payload argument not related to logging
continue
post_request_key = post_request_key[len(IOT_LOGGING_PREFIX):]
if post_request_key == 'root':
need_config_save |= self._update_logger_level('', log_level_or_parent, AVAILABLE_LOG_LEVELS)
elif post_request_key == 'odoo':
need_config_save |= self._update_logger_level('odoo', log_level_or_parent, AVAILABLE_LOG_LEVELS)
need_config_save |= self._update_logger_level('werkzeug', log_level_or_parent if log_level_or_parent != 'debug' else 'info', AVAILABLE_LOG_LEVELS)
elif post_request_key.startswith(INTERFACE_PREFIX):
logger_name = post_request_key[len(INTERFACE_PREFIX):]
need_config_save |= self._update_logger_level(logger_name, log_level_or_parent, AVAILABLE_LOG_LEVELS_WITH_PARENT, 'interfaces')
elif post_request_key.startswith(DRIVER_PREFIX):
logger_name = post_request_key[len(DRIVER_PREFIX):]
need_config_save |= self._update_logger_level(logger_name, log_level_or_parent, AVAILABLE_LOG_LEVELS_WITH_PARENT, 'drivers')
else:
_logger.warning('Unhandled iot logger: %s', post_request_key)
# Update and save the config file (in case of IoT box reset)
if need_config_save:
with helpers.writable():
tools.config.save()
drivers_list = helpers.list_file_by_os(file_path('hw_drivers/iot_handlers/drivers'))
interfaces_list = helpers.list_file_by_os(file_path('hw_drivers/iot_handlers/interfaces'))
return handler_list_template.render({
'title': "Odoo's IoT Box - Handlers list",
'breadcrumb': 'Handlers list',
'drivers_list': drivers_list,
'interfaces_list': interfaces_list,
'server': helpers.get_odoo_server_url(),
'root_logger_log_level': self._get_logger_effective_level_str(logging.getLogger()),
'odoo_current_log_level': self._get_logger_effective_level_str(logging.getLogger('odoo')),
'recommended_log_level': 'warning',
'available_log_levels': AVAILABLE_LOG_LEVELS,
'drivers_logger_info': self._get_iot_handlers_logger(drivers_list, 'drivers'),
'interfaces_logger_info': self._get_iot_handlers_logger(interfaces_list, 'interfaces'),
})
@http.route('/load_iot_handlers', type='http', auth='none', website=True)
def load_iot_handlers(self):
helpers.download_iot_handlers(False)
helpers.odoo_restart(0)
return "<meta http-equiv='refresh' content='20; url=http://" + helpers.get_ip() + ":8069/list_handlers'>"
@http.route('/list_credential', type='http', auth='none', website=True)
def list_credential(self):
return list_credential_template.render({
'title': "Odoo's IoT Box - List credential",
'breadcrumb': 'List credential',
'db_uuid': helpers.read_file_first_line('odoo-db-uuid.conf'),
'enterprise_code': helpers.read_file_first_line('odoo-enterprise-code.conf'),
})
@http.route('/save_credential', type='http', auth='none', cors='*', csrf=False)
def save_credential(self, db_uuid, enterprise_code):
helpers.write_file('odoo-db-uuid.conf', db_uuid)
helpers.write_file('odoo-enterprise-code.conf', enterprise_code)
helpers.odoo_restart(0)
return "<meta http-equiv='refresh' content='20; url=http://" + helpers.get_ip() + ":8069'>"
@http.route('/clear_credential', type='http', auth='none', cors='*', csrf=False)
def clear_credential(self):
helpers.unlink_file('odoo-db-uuid.conf')
helpers.unlink_file('odoo-enterprise-code.conf')
helpers.odoo_restart(0)
return "<meta http-equiv='refresh' content='20; url=http://" + helpers.get_ip() + ":8069'>"
@http.route('/wifi', type='http', auth='none', website=True)
def wifi(self):
return wifi_config_template.render({
'title': 'Wifi configuration',
'breadcrumb': 'Configure Wifi',
'loading_message': 'Connecting to Wifi',
'ssid': helpers.get_wifi_essid(),
})
@http.route('/wifi_connect', type='http', auth='none', cors='*', csrf=False)
def connect_to_wifi(self, essid, password, persistent=False):
if persistent:
persistent = "1"
else:
persistent = ""
subprocess.check_call([file_path('point_of_sale/tools/posbox/configuration/connect_to_wifi.sh'), essid, password, persistent])
server = helpers.get_odoo_server_url()
res_payload = {
'message': 'Connecting to ' + essid,
}
if server:
res_payload['server'] = {
'url': server,
'message': 'Redirect to Odoo Server'
}
else:
res_payload['server'] = {
'url': 'http://' + helpers.get_ip() + ':8069',
'message': 'Redirect to IoT Box'
}
return json.dumps(res_payload)
@http.route('/wifi_clear', type='http', auth='none', cors='*', csrf=False)
def clear_wifi_configuration(self):
helpers.unlink_file('wifi_network.txt')
return "<meta http-equiv='refresh' content='0; url=http://" + helpers.get_ip() + ":8069'>"
@http.route('/server_clear', type='http', auth='none', cors='*', csrf=False)
def clear_server_configuration(self):
helpers.unlink_file('odoo-remote-server.conf')
return "<meta http-equiv='refresh' content='0; url=http://" + helpers.get_ip() + ":8069'>"
@http.route('/handlers_clear', type='http', auth='none', cors='*', csrf=False)
def clear_handlers_list(self):
for directory in ['drivers', 'interfaces']:
for file in list(Path(file_path(f'hw_drivers/iot_handlers/{directory}')).glob('*')):
if file.name != '__pycache__':
helpers.unlink_file(str(file.relative_to(*file.parts[:3])))
return "<meta http-equiv='refresh' content='0; url=http://" + helpers.get_ip() + ":8069/list_handlers'>"
@http.route('/server_connect', type='http', auth='none', cors='*', csrf=False)
def connect_to_server(self, token, iotname):
if token:
credential = token.split('|')
url = credential[0]
token = credential[1]
db_uuid = credential[2]
enterprise_code = credential[3]
helpers.save_conf_server(url, token, db_uuid, enterprise_code)
else:
url = helpers.get_odoo_server_url()
token = helpers.get_token()
if iotname and platform.system() == 'Linux':
subprocess.check_call([file_path('point_of_sale/tools/posbox/configuration/rename_iot.sh'), iotname])
helpers.odoo_restart(5)
return 'http://' + helpers.get_ip() + ':8069'
@http.route('/steps', type='http', auth='none', cors='*', csrf=False)
def step_by_step_configure_page(self):
return configure_wizard_template.render({
'title': 'Configure IoT Box',
'breadcrumb': 'Configure IoT Box',
'loading_message': 'Configuring your IoT Box',
'ssid': helpers.get_wifi_essid(),
'server': helpers.get_odoo_server_url() or '',
'hostname': subprocess.check_output('hostname').decode('utf-8').strip('\n'),
})
@http.route('/step_configure', type='http', auth='none', cors='*', csrf=False)
def step_by_step_configure(self, token, iotname, essid, password, persistent=False):
if token:
url = token.split('|')[0]
token = token.split('|')[1]
else:
url = ''
subprocess.check_call([file_path('point_of_sale/tools/posbox/configuration/connect_to_server_wifi.sh'), url, iotname, token, essid, password, persistent])
return url
# Set server address
@http.route('/server', type='http', auth='none', website=True)
def server(self):
return server_config_template.render({
'title': 'IoT -> Odoo server configuration',
'breadcrumb': 'Configure Odoo Server',
'hostname': subprocess.check_output('hostname').decode('utf-8').strip('\n'),
'server_status': helpers.get_odoo_server_url() or 'Not configured yet',
'loading_message': 'Configure Domain Server'
})
# Get password
@http.route('/hw_posbox_homepage/password', type='json', auth='none', methods=['POST'])
def view_password(self):
return helpers.generate_password()
@http.route('/remote_connect', type='http', auth='none', cors='*')
def remote_connect(self):
"""
Establish a link with a customer box trough internet with a ssh tunnel
1 - take a new auth_token on https://dashboard.ngrok.com/
2 - copy past this auth_token on the IoT Box : http://IoT_Box:8069/remote_connect
3 - check on ngrok the port and url to get access to the box
4 - you can connect to the box with this command : ssh -p port -v pi@url
"""
return remote_connect_template.render({
'title': 'Remote debugging',
'breadcrumb': 'Remote Debugging',
})
@http.route('/enable_ngrok', type='http', auth='none', cors='*', csrf=False)
def enable_ngrok(self, auth_token):
if subprocess.call(['pgrep', 'ngrok']) == 1:
subprocess.Popen(['ngrok', 'tcp', '--authtoken', auth_token, '--log', '/tmp/ngrok.log', '22'])
return 'starting with ' + auth_token
else:
return 'already running'
@http.route('/six_payment_terminal', type='http', auth='none', cors='*', csrf=False)
def six_payment_terminal(self):
return six_payment_terminal_template.render({
'title': 'Six Payment Terminal',
'breadcrumb': 'Six Payment Terminal',
'terminalId': self.get_six_terminal(),
})
@http.route('/six_payment_terminal_add', type='http', auth='none', cors='*', csrf=False)
def add_six_payment_terminal(self, terminal_id):
if terminal_id.isdigit():
helpers.write_file('odoo-six-payment-terminal.conf', terminal_id)
service.server.restart()
else:
_logger.warning('Ignoring invalid Six TID: "%s". Only digits are allowed', terminal_id)
self.clear_six_payment_terminal()
return 'http://' + helpers.get_ip() + ':8069'
@http.route('/six_payment_terminal_clear', type='http', auth='none', cors='*', csrf=False)
def clear_six_payment_terminal(self):
helpers.unlink_file('odoo-six-payment-terminal.conf')
service.server.restart()
return "<meta http-equiv='refresh' content='0; url=http://" + helpers.get_ip() + ":8069'>"
@http.route('/hw_proxy/upgrade', type='http', auth='none', )
def upgrade(self):
commit = subprocess.check_output(["git", "--work-tree=/home/pi/odoo/", "--git-dir=/home/pi/odoo/.git", "log", "-1"]).decode('utf-8').replace("\n", "<br/>")
flashToVersion = helpers.check_image()
actualVersion = helpers.get_version()
if flashToVersion:
flashToVersion = '%s.%s' % (flashToVersion.get('major', ''), flashToVersion.get('minor', ''))
return upgrade_page_template.render({
'title': "Odoo's IoTBox - Software Upgrade",
'breadcrumb': 'IoT Box Software Upgrade',
'loading_message': 'Updating IoT box',
'commit': commit,
'flashToVersion': flashToVersion,
'actualVersion': actualVersion,
})
@http.route('/hw_proxy/perform_upgrade', type='http', auth='none')
def perform_upgrade(self):
self.updating.acquire()
os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/posbox_update.sh')
self.updating.release()
return 'SUCCESS'
@http.route('/hw_proxy/get_version', type='http', auth='none')
def check_version(self):
return helpers.get_version()
@http.route('/hw_proxy/perform_flashing_create_partition', type='http', auth='none')
def perform_flashing_create_partition(self):
try:
response = subprocess.check_output(['sudo', 'bash', '-c', '. /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/upgrade.sh; create_partition']).decode().split('\n')[-2]
if response in ['Error_Card_Size', 'Error_Upgrade_Already_Started']:
raise Exception(response)
return Response('success', status=200)
except subprocess.CalledProcessError as e:
raise Exception(e.output)
except Exception as e:
_logger.error('A error encountered : %s ' % e)
return Response(str(e), status=500)
@http.route('/hw_proxy/perform_flashing_download_raspios', type='http', auth='none')
def perform_flashing_download_raspios(self):
try:
response = subprocess.check_output(['sudo', 'bash', '-c', '. /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/upgrade.sh; download_raspios']).decode().split('\n')[-2]
if response == 'Error_Raspios_Download':
raise Exception(response)
return Response('success', status=200)
except subprocess.CalledProcessError as e:
raise Exception(e.output)
except Exception as e:
self.clean_partition()
_logger.error('A error encountered : %s ' % e)
return Response(str(e), status=500)
@http.route('/hw_proxy/perform_flashing_copy_raspios', type='http', auth='none')
def perform_flashing_copy_raspios(self):
try:
response = subprocess.check_output(['sudo', 'bash', '-c', '. /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/upgrade.sh; copy_raspios']).decode().split('\n')[-2]
if response == 'Error_Iotbox_Download':
raise Exception(response)
return Response('success', status=200)
except subprocess.CalledProcessError as e:
raise Exception(e.output)
except Exception as e:
self.clean_partition()
_logger.error('A error encountered : %s ' % e)
return Response(str(e), status=500)
@http.route('/iot_restart_odoo_or_reboot', type='json', auth='none', cors='*', csrf=False)
def iot_restart_odoo_or_reboot(self, action):
""" Reboots the IoT Box / restarts Odoo on it depending on chosen 'action' argument"""
try:
if action == 'restart_odoo':
helpers.odoo_restart(3)
else:
subprocess.call(['sudo', 'reboot'])
return 'success'
except Exception as e:
_logger.error('An error encountered : %s ', e)
return str(e)
def _get_logger_effective_level_str(self, logger):
return logging.getLevelName(logger.getEffectiveLevel()).lower()
def _get_iot_handler_logger(self, handler_name, handler_folder_name):
"""
Get Odoo Iot logger given an IoT handler name
:param handler_name: name of the IoT handler
:param handler_folder_name: IoT handler folder name (interfaces or drivers)
:return: logger if any, False otherwise
"""
odoo_addon_handler_path = helpers.compute_iot_handlers_addon_name(handler_folder_name, handler_name)
return odoo_addon_handler_path in logging.Logger.manager.loggerDict.keys() and \
logging.getLogger(odoo_addon_handler_path)
def _update_logger_level(self, logger_name, new_level, available_log_levels, handler_folder=False):
"""
Update (if necessary) Odoo's configuration and logger to the given logger_name to the given level.
The responsibility of saving the config file is not managed here.
:param logger_name: name of the logging logger to change level
:param new_level: new log level to set for this logger
:param available_log_levels: iterable of logs levels allowed (for initial check)
:param handler_folder: optional string of the IoT handler folder name ('interfaces' or 'drivers')
:return: wherever some changes were performed or not on the config
"""
if new_level not in available_log_levels:
_logger.warning('Unknown level to set on logger %s: %s', logger_name, new_level)
return False
if handler_folder:
logger = self._get_iot_handler_logger(logger_name, handler_folder)
if not logger:
_logger.warning('Unable to change log level for logger %s as logger missing', logger_name)
return False
logger_name = logger.name
ODOO_TOOL_CONFIG_HANDLER_NAME = 'log_handler'
LOG_HANDLERS = tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME]
LOGGER_PREFIX = logger_name + ':'
IS_NEW_LEVEL_PARENT = new_level == 'parent'
if not IS_NEW_LEVEL_PARENT:
intended_to_find = LOGGER_PREFIX + new_level.upper()
if intended_to_find in LOG_HANDLERS:
# There is nothing to do, the entry is already inside
return False
# We remove every occurrence for the given logger
log_handlers_without_logger = [
log_handler for log_handler in LOG_HANDLERS if not log_handler.startswith(LOGGER_PREFIX)
]
if IS_NEW_LEVEL_PARENT:
# We must check that there is no existing entries using this logger (whatever the level)
if len(log_handlers_without_logger) == len(LOG_HANDLERS):
return False
# We add if necessary new logger entry
# If it is "parent" it means we want it to inherit from the parent logger.
# In order to do this we have to make sure that no entries for the logger exists in the
# `log_handler` (which is the case at this point as long as we don't re-add an entry)
tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] = log_handlers_without_logger
new_level_upper_case = new_level.upper()
if not IS_NEW_LEVEL_PARENT:
new_entry = [LOGGER_PREFIX + new_level_upper_case]
tools.config[ODOO_TOOL_CONFIG_HANDLER_NAME] += new_entry
_logger.debug('Adding to odoo config log_handler: %s', new_entry)
# Update the logger dynamically
real_new_level = logging.NOTSET if IS_NEW_LEVEL_PARENT else new_level_upper_case
_logger.debug('Change logger %s level to %s', logger_name, real_new_level)
logging.getLogger(logger_name).setLevel(real_new_level)
return True
def _get_iot_handlers_logger(self, handlers_name, iot_handler_folder_name):
"""
:param handlers_name: List of IoT handler string to search the loggers of
:param iot_handler_folder_name: name of the handler folder ('interfaces' or 'drivers')
:return:
{
<iot_handler_name_1> : {
'level': <logger_level_1>,
'is_using_parent_level': <logger_use_parent_level_or_not_1>,
'parent_name': <logger_parent_name_1>,
},
...
}
"""
handlers_loggers_level = dict()
for handler_name in handlers_name:
handler_logger = self._get_iot_handler_logger(handler_name, iot_handler_folder_name)
if not handler_logger:
# Might happen if the file didn't define a logger (or not init yet)
handlers_loggers_level[handler_name] = False
_logger.debug('Unable to find logger for handler %s', handler_name)
continue
logger_parent = handler_logger.parent
handlers_loggers_level[handler_name] = {
'level': self._get_logger_effective_level_str(handler_logger),
'is_using_parent_level': handler_logger.level == logging.NOTSET,
'parent_name': logger_parent.name,
'parent_level': self._get_logger_effective_level_str(logger_parent),
}
return handlers_loggers_level