Quellcode für modules.admin

#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
#  Copyright 2018-      Martin Sinn                         m.sinn@gmx.de
#########################################################################
#  This file is part of SmartHomeNG.
#
#  SmartHomeNG is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  SmartHomeNG is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with SmartHomeNG.  If not, see <http://www.gnu.org/licenses/>.
#########################################################################


import os
import logging
import cherrypy

from lib.utils import Utils

from lib.model.module import Module
from lib.module import Modules
from lib.shtime import Shtime

from .systemdata import SystemData
from .itemdata import ItemData
from .plugindata import PluginData

from .rest import RESTResource

from .api_server import *
from .api_auth import *

from .api_config import *
from .api_files import *
from .api_items import *
from .api_functions import *
from .api_loggers import *
from .api_logs import *
from .api_scenes import *
from .api_sched import *
from .api_services import *
from .api_system import *
from .api_threads import *

from .api_logics import *
from .api_plugins import *
from .api_plugin import *
#from .api_plginst import *

suburl = 'admin'


class Admin(Module):
    version = '1.9.0'
    longname = 'Admin module for SmartHomeNG'
    _port = 0

    _stop_methods = []      # list of stop methods defined by the various controllers of the admin api

    def __init__(self, sh, testparam=''):
        """
        Initialization Routine for the module
        """
        # TO DO: Shortname anders setzen (oder warten bis der Plugin Loader es beim Laden setzt
        self._shortname = self.__class__.__name__
        self._shortname = self._shortname.lower()

        self.logger = logging.getLogger(__name__)
        self._sh = sh
        self.shtime = Shtime.get_instance()
        self.logger.debug("Module '{}': Initializing".format(self._shortname))

        self.logger.debug("Module '{}': Parameters = '{}'".format(self._shortname, str(self._parameters)))

        # for authentication
        self.send_hash = 'shNG0160$'
        self.jwt_secret = 'SmartHomeNG$0815'

        try:
            self.mod_http = Modules.get_instance().get_module('http')  # try/except to handle running in a core version that does not support modules
        except Exception:
            self.mod_http = None
        if self.mod_http is None:
            self.logger.error(
                "Module '{}': Not initializing - Module 'http' has to be loaded BEFORE this module".format(
                    self._shortname))
            self._init_complete = False
            return

        self._showtraceback = self.mod_http._showtraceback

        try:
            self.login_expiration = self._parameters['login_expiration']
            self.login_autorenew = self._parameters['login_autorenew']
            self.pypi_timeout = self._parameters['pypi_timeout']
            self.itemtree_fullpath = self._parameters['itemtree_fullpath']
            self.itemtree_searchstart = self._parameters['itemtree_searchstart']
            self.log_chunksize = self._parameters['log_chunksize']
            self.developer_mode = self._parameters['developer_mode']
            self.rest_dispatch_force_exception = self._parameters['rest_dispatch_force_exception']
            self.click_dropdown_header = self._parameters['click_dropdown_header']
        except Exception:
            self.logger.critical(
                "Module '{}': Inconsistent module (invalid metadata definition)".format(self._shortname))
            self._init_complete = False
            return

        # Deprecation: websocket_host/websocket_port belong to the websocket module,
        # not to the admin module.  Warn users who still have them in admin config.
        _dep_host = self._parameters.get('websocket_host')
        _dep_port = self._parameters.get('websocket_port')
        _default_ws_port = 2424
        if _dep_host:
            self.logger.warning(
                "Module '{}': Parameter 'websocket_host' is DEPRECATED in the admin module. "
                "Configure it in the websocket module instead. The websocket module's value will be used.".format(self._shortname))
        if _dep_port is not None and _dep_port != _default_ws_port:
            self.logger.warning(
                "Module '{}': Parameter 'websocket_port' is DEPRECATED in the admin module. "
                "Configure it in the websocket module instead. The websocket module's value will be used.".format(self._shortname))

        # Stash deprecated fallback values so start() can use them if the
        # websocket module is genuinely absent.  The authoritative lookup is
        # deferred to start() because module __init__ methods run before any
        # module's start() is called, so get_module('websocket') always returns
        # None here even when the websocket module is properly configured.
        self._ws_dep_host = _dep_host or None
        self._ws_dep_port = str(_dep_port) if _dep_port else str(_default_ws_port)
        self._ws_default_port = str(_default_ws_port)
        self.websocket_host = None
        self.websocket_port = str(_default_ws_port)

        mysuburl = ''
        if suburl != '':
            mysuburl = '/' + suburl
        ip = Utils.get_local_ipv4_address()
        self._port = self.mod_http._port
        # self.logger.warning('port = {}'.format(self._port))
        self.shng_url_root = 'http://' + ip + ':' + str(self._port)         # for links mto plugin webinterfaces
        self.url_root = self.shng_url_root + mysuburl
        self.api_url_root = self.shng_url_root + 'api'

    def start(self):
        """
        Start the admin module

        Initialization and startup code of the module
        """
        self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'start()'}))

        # Resolve websocket host/port now that all modules are started.
        try:
            mod_ws = Modules.get_instance().get_module('websocket')
            if mod_ws is not None:
                actual_port = mod_ws.get_port()
                if actual_port:
                    self.websocket_port = str(actual_port)
                # Use the websocket module's bind IP only when it is a specific
                # address; 0.0.0.0 / :: are wildcard bind addresses that the
                # browser cannot connect to.
                ws_ip = getattr(mod_ws, 'ip', None)
                if ws_ip and ws_ip not in ('0.0.0.0', '::', ''):
                    self.websocket_host = ws_ip
            else:
                self.logger.warning(
                    "Module '{}': Websocket module not found; falling back to admin module parameters.".format(self._shortname))
                self.websocket_host = self._ws_dep_host
                self.websocket_port = self._ws_dep_port
        except Exception as e:
            self.logger.warning(
                "Module '{}': Could not read websocket module config: {}".format(self._shortname, e))

        self.webif_dir = os.path.dirname(os.path.abspath(__file__)) + '/webif'

        self.logger.info("Module '{}': webif_dir = webif_dir = {}".format(self._shortname, self.webif_dir))
        # config for Angular app (special: error page)
        config = {
            '/': {
                'tools.staticdir.root': self.webif_dir,
                'tools.staticdir.on': True,
                'tools.staticdir.dir': 'static/browser',
                'tools.staticdir.index': 'index.html',
                'tools.chaching.on': False,
                'tools.caching.force': False,
                'tools.caching.delay': 6,
                'tools.expires.on': True,
                'tools.expires.secs': 0,
                # fix for path error
                'error_page.404': self._spa_index,
            }
        }
        # API config (special: request.dispatch)
        config_api = {
            '/': {
                'tools.chaching.on': False,
                'tools.caching.force': False,
                'tools.caching.delay': 6,
                'tools.expires.on': True,
                'tools.expires.secs': 6,
                'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
                'error_page.404': self._error_page,
                'error_page.400': self._error_page,
                'error_page.401': self._error_page,
                'error_page.405': self._error_page,
                'error_page.411': self._error_page,
                'error_page.500': self._error_page,
            }
        }

        # Register the web interface as a cherrypy app
        self.mod_http.register_webif(WebInterface(self.webif_dir, self, self.shng_url_root, self.url_root),
                                     suburl,
                                     config,
                                     'admin', '',
                                     description='Administrationsoberfläche für SmartHomeNG',
                                     webifname='',
                                     use_global_basic_auth=False,
                                     useprefix=False)

        # Register the web interface as a cherrypy app
        self.mod_http.register_webif(WebApi(self.webif_dir, self, self.shng_url_root, self.api_url_root),
                                     'api',
                                     config_api,
                                     'api', '',
                                     description='API der Administrationsoberfläche für SmartHomeNG',
                                     webifname='',
                                     use_global_basic_auth=False,
                                     useprefix=False)

        # Angular's polyfills bundle requests /3rdpartylicenses.txt at the server
        # root (not relative to base-href), so serve it from the root app.
        license_file = os.path.join(self.webif_dir, 'static', '3rdpartylicenses.txt')
        if os.path.isfile(license_file) and '' in cherrypy.tree.apps:
            cherrypy.tree.apps[''].config['/3rdpartylicenses.txt'] = {
                'tools.staticfile.on': True,
                'tools.staticfile.filename': license_file,
            }

    def stop(self):
        """

        """
        self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'stop()'}))

        self.logger.info(f"Shutting down {self._shortname}")
        for stop_method in self._stop_methods:
            stop_method()
        self.logger.info(f"{self._shortname} shut down ")

    def add_stop_method(self, method, classname=''):
        """
        Class instances that implement their own stop() method should add those methods through this
        Method, so the stop() methods of the admin module can stop those instances too when stopping the module.

        :param method: stop-method to be added
        :param classname: Name of the class (optional)
        :type method: object
        :type classname: str
        """
        self.logger.info("Adding stop method of class {}".format(classname))
        self._stop_methods.append(method)


    def error_page(self, status, message, traceback, version):
        """
        Error 404 page, that redirects to index.html of Angular application

        :param status:
        :param message:
        :param traceback:
        :param version:

        :return: page to display (a redirect)
        :rtype: str
        """
        # ip = Utils.get_local_ipv4_address()
        # mysuburl = ''
        # if suburl != '':
        #     mysuburl = '/' + suburl

        # page = '<meta http-equiv="refresh" content="0; url=http://' + ip + ':' + str(self._port) + mysuburl + '/" />'
        # page = '<meta http-equiv="refresh" content="0; url=' + self.url_root + '/" />'
        page = '404: Page not found!<br>' + message
        self.logger.warning(
            "error_page: status = {}, message = {}".format(status, message))
        return page

    def _error_page(self, status, message, traceback, version):
        """
        Generate html page for errors

        :param status: error number and description
        :param message: detailed error description
        :param traceback: traceback that lead to the error
        :param version: CherryPy version
        :type status: str
        :type message: str
        :type traceback: str
        :type version: str

        :return: html error page
        :rtype: str

        """
        # show_traceback = True
        errno = status.split()[0]
        result = '<link rel="stylesheet" href="/gstatic/bootstrap/css/bootstrap.min.css" type="text/css"/>'
        result += '<link rel="stylesheet" href="/gstatic/css/smarthomeng.css" type="text/css"/>'
        result += '<div class="container mt-4 ml-0">' \
                  '<h1 class="margin-base-vertical">' \
                  '<img src="/gstatic/img/logo_small_120x120.png" width="40" height="40" style="vertical-align:top">'
        result += ' Oops, Error ' + errno + ':'
        result += '</h1><br/>'
        result += '<h3>' + message + '</h3><br/>'

        if not self._showtraceback or (errno == '404'):
            traceback = ''
        else:
            traceback = traceback.replace('\n', '<br>&nbsp;&nbsp;')
            traceback = traceback.replace(' ', '&nbsp;&nbsp;')
            traceback = '&nbsp;&nbsp;' + traceback

            result += '<div class="card">' \
                      '<div class="card-header"><strong>Traceback</strong></div>' \
                      '<div class="card-body text-shng">'
            result += traceback
            result += '</div>' \
                      '</div>'

        result += '</div>'

        return result

    def _spa_index(self, status, message, traceback, version):
        cherrypy.response.status = 200
        cherrypy.response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate'
        cherrypy.response.headers['Pragma'] = 'no-cache'
        cherrypy.response.headers['Expires'] = '0'
        index_path = os.path.join(self.webif_dir, 'static', 'browser', 'index.html')
        with open(index_path, 'r', encoding='utf-8') as f:
            return f.read()

def translate(s):
    # needed for Admin UI
    return s


class WebInterface(SystemData, ItemData, PluginData):

    def __init__(self, webif_dir, module, shng_url_root, url_root):
        self._sh = module._sh
        self.logger = logging.getLogger(__name__)
        self.module = module
        self.pypi_timeout = module.pypi_timeout
        self.shng_url_root = shng_url_root
        self.url_root = url_root

        SystemData.__init__(self, self._sh)
        ItemData.__init__(self)
        PluginData.__init__(self)

        return


[Doku]class WebApi(RESTResource): """ :param webif_dir: Directory where the files of the web interface (shngadmin) are stored :param module: Instance of the webif object :param shng_url_root: ... :param url_root: ... :type webif_dir: str :type module: object :type shng_url_root: str :type url_root: str """ exposed = True def __init__(self, webif_dir, module, shng_url_root, url_root): self._sh = module._sh self.logger = logging.getLogger(__name__) self.module = module self.shng_url_root = shng_url_root self.url_root = url_root # ------------------------------ # --- Add REST controllers --- # ------------------------------ self.authenticate = AuthController(self.module) self.config = ConfigController(self.module) self.files = FilesController(self.module) self.items = ItemsController(self.module) self.items.list = ItemsListController(self.module) self.functions = FunctionsController(self.module) self.functions.reload = FunctionsReloadController(self.module) self.logics = LogicsController(self.module) self.loggers = LoggersController(self.module) self.logs = LogsController(self.module) self.plugin = PluginController(self.module, self.jwt_secret) self.plugins = PluginsController(self.module) self.plugins.api = PluginsAPIController(self.module) self.plugins.installed = PluginsInstalledController(self.module) self.plugins.config = PluginsConfigController(self.module) self.plugins.info = PluginsInfoController(self.module, self.shng_url_root) self.plugins.logicparams = PluginsLogicParametersController(self.module) self.scenes = ScenesController(self.module) self.scenes.reload = ScenesReloadController(self.module) self.schedulers = SchedulersController(self.module) self.server = ServerController(self.module) self.services = ServicesController(self.module) self.system = SystemController(self.module) self.threads = ThreadsController(self.module) return @cherrypy.expose(['home', '']) def index(self): return "Give SmartHomeNG a REST."