Feature 7184 New Generation RO
[osm/RO.git] / NG-RO / osm_ng_ro / ro_main.py
diff --git a/NG-RO/osm_ng_ro/ro_main.py b/NG-RO/osm_ng_ro/ro_main.py
new file mode 100644 (file)
index 0000000..35a93fe
--- /dev/null
@@ -0,0 +1,740 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+##
+# Copyright 2020 Telefonica Investigacion y Desarrollo, S.A.U.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import cherrypy
+import time
+import json
+import yaml
+import osm_ng_ro.html_out as html
+import logging
+import logging.handlers
+import getopt
+import sys
+
+from osm_ng_ro.ns import Ns, NsException
+from osm_ng_ro.validation import ValidationError
+from osm_common.dbbase import DbException
+from osm_common.fsbase import FsException
+from osm_common.msgbase import MsgException
+from http import HTTPStatus
+from codecs import getreader
+from os import environ, path
+from osm_ng_ro import version as ro_version, version_date as ro_version_date
+
+__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
+
+__version__ = "0.1."    # file version, not NBI version
+version_date = "May 2020"
+
+database_version = '1.2'
+auth_database_version = '1.0'
+ro_server = None           # instance of Server class
+# vim_threads = None  # instance of VimThread class
+
+"""
+RO North Bound Interface
+URL: /ro                                                       GET     POST    PUT     DELETE  PATCH
+        /ns/v1/deploy                                           O
+            /<nsrs_id>                                          O       O               O
+                /<action_id>                                    O
+                    /cancel                                             O
+
+"""
+
+valid_query_string = ("ADMIN", "SET_PROJECT", "FORCE", "PUBLIC")
+# ^ Contains possible administrative query string words:
+#     ADMIN=True(by default)|Project|Project-list:  See all elements, or elements of a project
+#           (not owned by my session project).
+#     PUBLIC=True(by default)|False: See/hide public elements. Set/Unset a topic to be public
+#     FORCE=True(by default)|False: Force edition/deletion operations
+#     SET_PROJECT=Project|Project-list: Add/Delete the topic to the projects portfolio
+
+valid_url_methods = {
+    # contains allowed URL and methods, and the role_permission name
+    "admin": {
+        "v1": {
+            "tokens": {
+                "METHODS": ("POST",),
+                "ROLE_PERMISSION": "tokens:",
+                "<ID>": {
+                    "METHODS": ("DELETE",),
+                    "ROLE_PERMISSION": "tokens:id:"
+                }
+            },
+        }
+    },
+    "ns": {
+        "v1": {
+            "deploy": {
+                "METHODS": ("GET",),
+                "ROLE_PERMISSION": "deploy:",
+                "<ID>": {
+                    "METHODS": ("GET", "POST", "DELETE"),
+                    "ROLE_PERMISSION": "deploy:id:",
+                    "<ID>": {
+                        "METHODS": ("GET",),
+                        "ROLE_PERMISSION": "deploy:id:id:",
+                        "cancel": {
+                            "METHODS": ("POST",),
+                            "ROLE_PERMISSION": "deploy:id:id:cancel",
+                        }
+                    }
+                }
+            },
+        }
+    },
+}
+
+
+class RoException(Exception):
+
+    def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED):
+        Exception.__init__(self, message)
+        self.http_code = http_code
+
+
+class AuthException(RoException):
+    pass
+
+
+class Authenticator:
+    
+    def __init__(self, valid_url_methods, valid_query_string):
+        self.valid_url_methods = valid_url_methods
+        self.valid_query_string = valid_query_string
+
+    def authorize(self, *args, **kwargs):
+        return {"token": "ok", "id": "ok"}
+    
+    def new_token(self, token_info, indata, remote):
+        return {"token": "ok",
+                "id": "ok",
+                "remote": remote}
+
+    def del_token(self, token_id):
+        pass
+
+    def start(self, engine_config):
+        pass
+
+
+class Server(object):
+    instance = 0
+    # to decode bytes to str
+    reader = getreader("utf-8")
+
+    def __init__(self):
+        self.instance += 1
+        self.authenticator = Authenticator(valid_url_methods, valid_query_string)
+        self.ns = Ns()
+        self.map_operation = {
+            "token:post": self.new_token,
+            "token:id:delete": self.del_token,
+            "deploy:get": self.ns.get_deploy,
+            "deploy:id:get": self.ns.get_actions,
+            "deploy:id:post": self.ns.deploy,
+            "deploy:id:delete": self.ns.delete,
+            "deploy:id:id:get": self.ns.status,
+            "deploy:id:id:cancel:post": self.ns.cancel,
+        }
+
+    def _format_in(self, kwargs):
+        try:
+            indata = None
+            if cherrypy.request.body.length:
+                error_text = "Invalid input format "
+
+                if "Content-Type" in cherrypy.request.headers:
+                    if "application/json" in cherrypy.request.headers["Content-Type"]:
+                        error_text = "Invalid json format "
+                        indata = json.load(self.reader(cherrypy.request.body))
+                        cherrypy.request.headers.pop("Content-File-MD5", None)
+                    elif "application/yaml" in cherrypy.request.headers["Content-Type"]:
+                        error_text = "Invalid yaml format "
+                        indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
+                        cherrypy.request.headers.pop("Content-File-MD5", None)
+                    elif "application/binary" in cherrypy.request.headers["Content-Type"] or \
+                         "application/gzip" in cherrypy.request.headers["Content-Type"] or \
+                         "application/zip" in cherrypy.request.headers["Content-Type"] or \
+                         "text/plain" in cherrypy.request.headers["Content-Type"]:
+                        indata = cherrypy.request.body  # .read()
+                    elif "multipart/form-data" in cherrypy.request.headers["Content-Type"]:
+                        if "descriptor_file" in kwargs:
+                            filecontent = kwargs.pop("descriptor_file")
+                            if not filecontent.file:
+                                raise RoException("empty file or content", HTTPStatus.BAD_REQUEST)
+                            indata = filecontent.file  # .read()
+                            if filecontent.content_type.value:
+                                cherrypy.request.headers["Content-Type"] = filecontent.content_type.value
+                    else:
+                        # raise cherrypy.HTTPError(HTTPStatus.Not_Acceptable,
+                        #                          "Only 'Content-Type' of type 'application/json' or
+                        # 'application/yaml' for input format are available")
+                        error_text = "Invalid yaml format "
+                        indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
+                        cherrypy.request.headers.pop("Content-File-MD5", None)
+                else:
+                    error_text = "Invalid yaml format "
+                    indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
+                    cherrypy.request.headers.pop("Content-File-MD5", None)
+            if not indata:
+                indata = {}
+
+            format_yaml = False
+            if cherrypy.request.headers.get("Query-String-Format") == "yaml":
+                format_yaml = True
+
+            for k, v in kwargs.items():
+                if isinstance(v, str):
+                    if v == "":
+                        kwargs[k] = None
+                    elif format_yaml:
+                        try:
+                            kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader)
+                        except Exception:
+                            pass
+                    elif k.endswith(".gt") or k.endswith(".lt") or k.endswith(".gte") or k.endswith(".lte"):
+                        try:
+                            kwargs[k] = int(v)
+                        except Exception:
+                            try:
+                                kwargs[k] = float(v)
+                            except Exception:
+                                pass
+                    elif v.find(",") > 0:
+                        kwargs[k] = v.split(",")
+                elif isinstance(v, (list, tuple)):
+                    for index in range(0, len(v)):
+                        if v[index] == "":
+                            v[index] = None
+                        elif format_yaml:
+                            try:
+                                v[index] = yaml.load(v[index], Loader=yaml.SafeLoader)
+                            except Exception:
+                                pass
+
+            return indata
+        except (ValueError, yaml.YAMLError) as exc:
+            raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
+        except KeyError as exc:
+            raise RoException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST)
+        except Exception as exc:
+            raise RoException(error_text + str(exc), HTTPStatus.BAD_REQUEST)
+
+    @staticmethod
+    def _format_out(data, token_info=None, _format=None):
+        """
+        return string of dictionary data according to requested json, yaml, xml. By default json
+        :param data: response to be sent. Can be a dict, text or file
+        :param token_info: Contains among other username and project
+        :param _format: The format to be set as Content-Type if data is a file
+        :return: None
+        """
+        accept = cherrypy.request.headers.get("Accept")
+        if data is None:
+            if accept and "text/html" in accept:
+                return html.format(data, cherrypy.request, cherrypy.response, token_info)
+            # cherrypy.response.status = HTTPStatus.NO_CONTENT.value
+            return
+        elif hasattr(data, "read"):  # file object
+            if _format:
+                cherrypy.response.headers["Content-Type"] = _format
+            elif "b" in data.mode:  # binariy asssumig zip
+                cherrypy.response.headers["Content-Type"] = 'application/zip'
+            else:
+                cherrypy.response.headers["Content-Type"] = 'text/plain'
+            # TODO check that cherrypy close file. If not implement pending things to close  per thread next
+            return data
+        if accept:
+            if "application/json" in accept:
+                cherrypy.response.headers["Content-Type"] = 'application/json; charset=utf-8'
+                a = json.dumps(data, indent=4) + "\n"
+                return a.encode("utf8")
+            elif "text/html" in accept:
+                return html.format(data, cherrypy.request, cherrypy.response, token_info)
+
+            elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
+                pass
+            # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
+            elif cherrypy.response.status >= 400:
+                raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
+                                         "Only 'Accept' of type 'application/json' or 'application/yaml' "
+                                         "for output format are available")
+        cherrypy.response.headers["Content-Type"] = 'application/yaml'
+        return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False,
+                              encoding='utf-8', allow_unicode=True)  # , canonical=True, default_style='"'
+
+    @cherrypy.expose
+    def index(self, *args, **kwargs):
+        token_info = None
+        try:
+            if cherrypy.request.method == "GET":
+                token_info = self.authenticator.authorize()
+                outdata = token_info   # Home page
+            else:
+                raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
+                                         "Method {} not allowed for tokens".format(cherrypy.request.method))
+
+            return self._format_out(outdata, token_info)
+
+        except (NsException, AuthException) as e:
+            # cherrypy.log("index Exception {}".format(e))
+            cherrypy.response.status = e.http_code.value
+            return self._format_out("Welcome to OSM!", token_info)
+
+    @cherrypy.expose
+    def version(self, *args, **kwargs):
+        # TODO consider to remove and provide version using the static version file
+        try:
+            if cherrypy.request.method != "GET":
+                raise RoException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
+            elif args or kwargs:
+                raise RoException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
+            # TODO include version of other modules, pick up from some kafka admin message
+            osm_ng_ro_version = {"version": ro_version, "date": ro_version_date}
+            return self._format_out(osm_ng_ro_version)
+        except RoException as e:
+            cherrypy.response.status = e.http_code.value
+            problem_details = {
+                "code": e.http_code.name,
+                "status": e.http_code.value,
+                "detail": str(e),
+            }
+            return self._format_out(problem_details, None)
+
+    def new_token(self, engine_session, indata, *args, **kwargs):
+        token_info = None
+
+        try:
+            token_info = self.authenticator.authorize()
+        except Exception:
+            token_info = None
+        if kwargs:
+            indata.update(kwargs)
+        # This is needed to log the user when authentication fails
+        cherrypy.request.login = "{}".format(indata.get("username", "-"))
+        token_info = self.authenticator.new_token(token_info, indata, cherrypy.request.remote)
+        cherrypy.session['Authorization'] = token_info["id"]
+        self._set_location_header("admin", "v1", "tokens", token_info["id"])
+        # for logging
+
+        # cherrypy.response.cookie["Authorization"] = outdata["id"]
+        # cherrypy.response.cookie["Authorization"]['expires'] = 3600
+        return token_info, token_info["id"], True
+
+    def del_token(self, engine_session, indata, version, _id, *args, **kwargs):
+        token_id = _id
+        if not token_id and "id" in kwargs:
+            token_id = kwargs["id"]
+        elif not token_id:
+            token_info = self.authenticator.authorize()
+            # for logging
+            token_id = token_info["id"]
+        self.authenticator.del_token(token_id)
+        token_info = None
+        cherrypy.session['Authorization'] = "logout"
+        # cherrypy.response.cookie["Authorization"] = token_id
+        # cherrypy.response.cookie["Authorization"]['expires'] = 0
+        return None, None, True
+    
+    @cherrypy.expose
+    def test(self, *args, **kwargs):
+        if not cherrypy.config.get("server.enable_test") or (isinstance(cherrypy.config["server.enable_test"], str) and
+                                                             cherrypy.config["server.enable_test"].lower() == "false"):
+            cherrypy.response.status = HTTPStatus.METHOD_NOT_ALLOWED.value
+            return "test URL is disabled"
+        thread_info = None
+        if args and args[0] == "help":
+            return "<html><pre>\ninit\nfile/<name>  download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
+                   "sleep/<time>\nmessage/topic\n</pre></html>"
+
+        elif args and args[0] == "init":
+            try:
+                # self.ns.load_dbase(cherrypy.request.app.config)
+                self.ns.create_admin()
+                return "Done. User 'admin', password 'admin' created"
+            except Exception:
+                cherrypy.response.status = HTTPStatus.FORBIDDEN.value
+                return self._format_out("Database already initialized")
+        elif args and args[0] == "file":
+            return cherrypy.lib.static.serve_file(cherrypy.tree.apps['/ro'].config["storage"]["path"] + "/" + args[1],
+                                                  "text/plain", "attachment")
+        elif args and args[0] == "file2":
+            f_path = cherrypy.tree.apps['/ro'].config["storage"]["path"] + "/" + args[1]
+            f = open(f_path, "r")
+            cherrypy.response.headers["Content-type"] = "text/plain"
+            return f
+
+        elif len(args) == 2 and args[0] == "db-clear":
+            deleted_info = self.ns.db.del_list(args[1], kwargs)
+            return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
+        elif len(args) and args[0] == "fs-clear":
+            if len(args) >= 2:
+                folders = (args[1],)
+            else:
+                folders = self.ns.fs.dir_ls(".")
+            for folder in folders:
+                self.ns.fs.file_delete(folder)
+            return ",".join(folders) + " folders deleted\n"
+        elif args and args[0] == "login":
+            if not cherrypy.request.headers.get("Authorization"):
+                cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
+                cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
+        elif args and args[0] == "login2":
+            if not cherrypy.request.headers.get("Authorization"):
+                cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="Access to OSM site"'
+                cherrypy.response.status = HTTPStatus.UNAUTHORIZED.value
+        elif args and args[0] == "sleep":
+            sleep_time = 5
+            try:
+                sleep_time = int(args[1])
+            except Exception:
+                cherrypy.response.status = HTTPStatus.FORBIDDEN.value
+                return self._format_out("Database already initialized")
+            thread_info = cherrypy.thread_data
+            print(thread_info)
+            time.sleep(sleep_time)
+            # thread_info
+        elif len(args) >= 2 and args[0] == "message":
+            main_topic = args[1]
+            return_text = "<html><pre>{} ->\n".format(main_topic)
+            try:
+                if cherrypy.request.method == 'POST':
+                    to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
+                    for k, v in to_send.items():
+                        self.ns.msg.write(main_topic, k, v)
+                        return_text += "  {}: {}\n".format(k, v)
+                elif cherrypy.request.method == 'GET':
+                    for k, v in kwargs.items():
+                        self.ns.msg.write(main_topic, k, yaml.load(v, Loader=yaml.SafeLoader))
+                        return_text += "  {}: {}\n".format(k, yaml.load(v, Loader=yaml.SafeLoader))
+            except Exception as e:
+                return_text += "Error: " + str(e)
+            return_text += "</pre></html>\n"
+            return return_text
+
+        return_text = (
+            "<html><pre>\nheaders:\n  args: {}\n".format(args) +
+            "  kwargs: {}\n".format(kwargs) +
+            "  headers: {}\n".format(cherrypy.request.headers) +
+            "  path_info: {}\n".format(cherrypy.request.path_info) +
+            "  query_string: {}\n".format(cherrypy.request.query_string) +
+            "  session: {}\n".format(cherrypy.session) +
+            "  cookie: {}\n".format(cherrypy.request.cookie) +
+            "  method: {}\n".format(cherrypy.request.method) +
+            "  session: {}\n".format(cherrypy.session.get('fieldname')) +
+            "  body:\n")
+        return_text += "    length: {}\n".format(cherrypy.request.body.length)
+        if cherrypy.request.body.length:
+            return_text += "    content: {}\n".format(
+                str(cherrypy.request.body.read(int(cherrypy.request.headers.get('Content-Length', 0)))))
+        if thread_info:
+            return_text += "thread: {}\n".format(thread_info)
+        return_text += "</pre></html>"
+        return return_text
+
+    @staticmethod
+    def _check_valid_url_method(method, *args):
+        if len(args) < 3:
+            raise RoException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED)
+
+        reference = valid_url_methods
+        for arg in args:
+            if arg is None:
+                break
+            if not isinstance(reference, dict):
+                raise RoException("URL contains unexpected extra items '{}'".format(arg),
+                                  HTTPStatus.METHOD_NOT_ALLOWED)
+
+            if arg in reference:
+                reference = reference[arg]
+            elif "<ID>" in reference:
+                reference = reference["<ID>"]
+            elif "*" in reference:
+                # reference = reference["*"]
+                break
+            else:
+                raise RoException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED)
+        if "TODO" in reference and method in reference["TODO"]:
+            raise RoException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED)
+        elif "METHODS" not in reference or method not in reference["METHODS"]:
+            raise RoException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
+        return reference["ROLE_PERMISSION"] + method.lower()
+
+    @staticmethod
+    def _set_location_header(main_topic, version, topic, id):
+        """
+        Insert response header Location with the URL of created item base on URL params
+        :param main_topic:
+        :param version:
+        :param topic:
+        :param id:
+        :return: None
+        """
+        # Use cherrypy.request.base for absoluted path and make use of request.header HOST just in case behind aNAT
+        cherrypy.response.headers["Location"] = "/ro/{}/{}/{}/{}".format(main_topic, version, topic, id)
+        return
+
+    @cherrypy.expose
+    def default(self, main_topic=None, version=None, topic=None, _id=None, _id2=None, *args, **kwargs):
+        token_info = None
+        outdata = None
+        _format = None
+        method = "DONE"
+        rollback = []
+        engine_session = None
+        try:
+            if not main_topic or not version or not topic:
+                raise RoException("URL must contain at least 'main_topic/version/topic'",
+                                  HTTPStatus.METHOD_NOT_ALLOWED)
+            if main_topic not in ("admin", "ns",):
+                raise RoException("URL main_topic '{}' not supported".format(main_topic),
+                                  HTTPStatus.METHOD_NOT_ALLOWED)
+            if version != 'v1':
+                raise RoException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED)
+
+            if kwargs and "METHOD" in kwargs and kwargs["METHOD"] in ("PUT", "POST", "DELETE", "GET", "PATCH"):
+                method = kwargs.pop("METHOD")
+            else:
+                method = cherrypy.request.method
+
+            role_permission = self._check_valid_url_method(method, main_topic, version, topic, _id, _id2, *args,
+                                                           **kwargs)
+            # skip token validation if requesting a token
+            indata = self._format_in(kwargs)
+            if main_topic != "admin" or topic != "tokens":
+                token_info = self.authenticator.authorize(role_permission, _id)
+            outdata, created_id, done = self.map_operation[role_permission](
+                engine_session, indata, version, _id, _id2, *args, *kwargs)
+            if created_id:
+                self._set_location_header(main_topic, version, topic, _id)
+            cherrypy.response.status = HTTPStatus.ACCEPTED.value if not done else HTTPStatus.OK.value if \
+                outdata is not None else HTTPStatus.NO_CONTENT.value
+            return self._format_out(outdata, token_info, _format)
+        except Exception as e:
+            if isinstance(e, (RoException, NsException, DbException, FsException, MsgException, AuthException,
+                              ValidationError)):
+                http_code_value = cherrypy.response.status = e.http_code.value
+                http_code_name = e.http_code.name
+                cherrypy.log("Exception {}".format(e))
+            else:
+                http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value  # INTERNAL_SERVER_ERROR
+                cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
+                http_code_name = HTTPStatus.BAD_REQUEST.name
+            if hasattr(outdata, "close"):  # is an open file
+                outdata.close()
+            error_text = str(e)
+            rollback.reverse()
+            for rollback_item in rollback:
+                try:
+                    if rollback_item.get("operation") == "set":
+                        self.ns.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
+                                           rollback_item["content"], fail_on_empty=False)
+                    else:
+                        self.ns.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
+                                           fail_on_empty=False)
+                except Exception as e2:
+                    rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
+                    cherrypy.log(rollback_error_text)
+                    error_text += ". " + rollback_error_text
+            # if isinstance(e, MsgException):
+            #     error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format(
+            #         engine_topic[:-1], method, error_text)
+            problem_details = {
+                "code": http_code_name,
+                "status": http_code_value,
+                "detail": error_text,
+            }
+            return self._format_out(problem_details, token_info)
+            # raise cherrypy.HTTPError(e.http_code.value, str(e))
+        finally:
+            if token_info:
+                if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
+                    for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
+                        if outdata.get(logging_id):
+                            cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36])
+
+
+def _start_service():
+    """
+    Callback function called when cherrypy.engine starts
+    Override configuration with env variables
+    Set database, storage, message configuration
+    Init database with admin/admin user password
+    """
+    global ro_server
+    # global vim_threads
+    cherrypy.log.error("Starting osm_ng_ro")
+    # update general cherrypy configuration
+    update_dict = {}
+
+    engine_config = cherrypy.tree.apps['/ro'].config
+    for k, v in environ.items():
+        if not k.startswith("OSMRO_"):
+            continue
+        k1, _, k2 = k[6:].lower().partition("_")
+        if not k2:
+            continue
+        try:
+            if k1 in ("server", "test", "auth", "log"):
+                # update [global] configuration
+                update_dict[k1 + '.' + k2] = yaml.safe_load(v)
+            elif k1 == "static":
+                # update [/static] configuration
+                engine_config["/static"]["tools.staticdir." + k2] = yaml.safe_load(v)
+            elif k1 == "tools":
+                # update [/] configuration
+                engine_config["/"]["tools." + k2.replace('_', '.')] = yaml.safe_load(v)
+            elif k1 in ("message", "database", "storage", "authentication"):
+                # update [message], [database], ... configuration
+                if k2 in ("port", "db_port"):
+                    engine_config[k1][k2] = int(v)
+                else:
+                    engine_config[k1][k2] = v
+
+        except Exception as e:
+            raise RoException("Cannot load env '{}': {}".format(k, e))
+
+    if update_dict:
+        cherrypy.config.update(update_dict)
+        engine_config["global"].update(update_dict)
+
+    # logging cherrypy
+    log_format_simple = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
+    log_formatter_simple = logging.Formatter(log_format_simple, datefmt='%Y-%m-%dT%H:%M:%S')
+    logger_server = logging.getLogger("cherrypy.error")
+    logger_access = logging.getLogger("cherrypy.access")
+    logger_cherry = logging.getLogger("cherrypy")
+    logger_nbi = logging.getLogger("ro")
+
+    if "log.file" in engine_config["global"]:
+        file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["log.file"],
+                                                            maxBytes=100e6, backupCount=9, delay=0)
+        file_handler.setFormatter(log_formatter_simple)
+        logger_cherry.addHandler(file_handler)
+        logger_nbi.addHandler(file_handler)
+    # log always to standard output
+    for format_, logger in {"ro.server %(filename)s:%(lineno)s": logger_server,
+                            "ro.access %(filename)s:%(lineno)s": logger_access,
+                            "%(name)s %(filename)s:%(lineno)s": logger_nbi
+                            }.items():
+        log_format_cherry = "%(asctime)s %(levelname)s {} %(message)s".format(format_)
+        log_formatter_cherry = logging.Formatter(log_format_cherry, datefmt='%Y-%m-%dT%H:%M:%S')
+        str_handler = logging.StreamHandler()
+        str_handler.setFormatter(log_formatter_cherry)
+        logger.addHandler(str_handler)
+
+    if engine_config["global"].get("log.level"):
+        logger_cherry.setLevel(engine_config["global"]["log.level"])
+        logger_nbi.setLevel(engine_config["global"]["log.level"])
+
+    # logging other modules
+    for k1, logname in {"message": "ro.msg", "database": "ro.db", "storage": "ro.fs"}.items():
+        engine_config[k1]["logger_name"] = logname
+        logger_module = logging.getLogger(logname)
+        if "logfile" in engine_config[k1]:
+            file_handler = logging.handlers.RotatingFileHandler(engine_config[k1]["logfile"],
+                                                                maxBytes=100e6, backupCount=9, delay=0)
+            file_handler.setFormatter(log_formatter_simple)
+            logger_module.addHandler(file_handler)
+        if "loglevel" in engine_config[k1]:
+            logger_module.setLevel(engine_config[k1]["loglevel"])
+    # TODO add more entries, e.g.: storage
+
+    engine_config["assignment"] = {}
+    # ^ each VIM, SDNc will be assigned one worker id. Ns class will add items and VimThread will auto-assign
+    cherrypy.tree.apps['/ro'].root.ns.start(engine_config)
+    cherrypy.tree.apps['/ro'].root.authenticator.start(engine_config)
+    cherrypy.tree.apps['/ro'].root.ns.init_db(target_version=database_version)
+
+    # # start subscriptions thread:
+    # vim_threads = []
+    # for thread_id in range(engine_config["global"]["server.ns_threads"]):
+    #     vim_thread = VimThread(thread_id, config=engine_config, engine=ro_server.ns)
+    #     vim_thread.start()
+    #     vim_threads.append(vim_thread)
+    # # Do not capture except SubscriptionException
+
+    backend = engine_config["authentication"]["backend"]
+    cherrypy.log.error("Starting OSM NBI Version '{} {}' with '{}' authentication backend"
+                       .format(ro_version, ro_version_date, backend))
+
+
+def _stop_service():
+    """
+    Callback function called when cherrypy.engine stops
+    TODO: Ending database connections.
+    """
+    # global vim_threads
+    # if vim_threads:
+    #     for vim_thread in vim_threads:
+    #         vim_thread.terminate()
+    # vim_threads = None
+    cherrypy.tree.apps['/ro'].root.ns.stop()
+    cherrypy.log.error("Stopping osm_ng_ro")
+
+
+def ro_main(config_file):
+    global ro_server
+    ro_server = Server()
+    cherrypy.engine.subscribe('start', _start_service)
+    cherrypy.engine.subscribe('stop', _stop_service)
+    cherrypy.quickstart(ro_server, '/ro', config_file)
+
+
+def usage():
+    print("""Usage: {} [options]
+        -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg)
+        -h|--help: shows this help
+        """.format(sys.argv[0]))
+    # --log-socket-host HOST: send logs to this host")
+    # --log-socket-port PORT: send logs using this port (default: 9022)")
+
+
+if __name__ == '__main__':
+    try:
+        # load parameters and configuration
+        opts, args = getopt.getopt(sys.argv[1:], "hvc:", ["config=", "help"])
+        # TODO add  "log-socket-host=", "log-socket-port=", "log-file="
+        config_file = None
+        for o, a in opts:
+            if o in ("-h", "--help"):
+                usage()
+                sys.exit()
+            elif o in ("-c", "--config"):
+                config_file = a
+            else:
+                assert False, "Unhandled option"
+        if config_file:
+            if not path.isfile(config_file):
+                print("configuration file '{}' that not exist".format(config_file), file=sys.stderr)
+                exit(1)
+        else:
+            for config_file in (path.dirname(__file__) + "/ro.cfg", "./ro.cfg", "/etc/osm/ro.cfg"):
+                if path.isfile(config_file):
+                    break
+            else:
+                print("No configuration file 'ro.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr)
+                exit(1)
+        ro_main(config_file)
+    except getopt.GetoptError as e:
+        print(str(e), file=sys.stderr)
+        # usage()
+        exit(1)