X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FRO.git;a=blobdiff_plain;f=NG-RO%2Fosm_ng_ro%2Fro_main.py;h=bd2ea9e029a434cea0049fa125c460e06b3e1d19;hp=485f15db4cfd1c7a40be7aca5d7ae052a59d4b68;hb=78f474e69fde9d64e8716978b5ea38f9f5aace48;hpb=e493e9b91720e5116e00b4c06cf66c767bccce2f diff --git a/NG-RO/osm_ng_ro/ro_main.py b/NG-RO/osm_ng_ro/ro_main.py index 485f15db..bd2ea9e0 100644 --- a/NG-RO/osm_ng_ro/ro_main.py +++ b/NG-RO/osm_ng_ro/ro_main.py @@ -18,36 +18,37 @@ # limitations under the License. ## -import cherrypy -import time + +from codecs import getreader +import getopt +from http import HTTPStatus import json -import yaml -import osm_ng_ro.html_out as html import logging import logging.handlers -import getopt +from os import environ, path import sys +import time -from osm_ng_ro.ns import Ns, NsException -from osm_ng_ro.validation import ValidationError -from osm_ng_ro.vim_admin import VimAdminThread +import cherrypy 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 +import osm_ng_ro.html_out as html +from osm_ng_ro.ns import Ns, NsException +from osm_ng_ro.validation import ValidationError +from osm_ng_ro.vim_admin import VimAdminThread +import yaml -__author__ = "Alfonso Tierno " -__version__ = "0.1." # file version, not NBI version +__author__ = "Alfonso Tierno " +__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_admin_thread = None # instance of VimAdminThread class +database_version = "1.2" +auth_database_version = "1.0" +ro_server = None # instance of Server class +vim_admin_thread = None # instance of VimAdminThread class # vim_threads = None # instance of VimThread class @@ -76,15 +77,36 @@ valid_url_methods = { "tokens": { "METHODS": ("POST",), "ROLE_PERMISSION": "tokens:", - "": { - "METHODS": ("DELETE",), - "ROLE_PERMISSION": "tokens:id:" - } + "": {"METHODS": ("DELETE",), "ROLE_PERMISSION": "tokens:id:"}, }, } }, "ns": { "v1": { + "rebuild": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "rebuild:", + "": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "rebuild:id:", + }, + }, + "start": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "start:", + "": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "start:id:", + }, + }, + "stop": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "stop:", + "": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "stop:id:", + }, + }, "deploy": { "METHODS": ("GET",), "ROLE_PERMISSION": "deploy:", @@ -97,9 +119,29 @@ valid_url_methods = { "cancel": { "METHODS": ("POST",), "ROLE_PERMISSION": "deploy:id:id:cancel", - } - } - } + }, + }, + }, + }, + "recreate": { + "": { + "METHODS": ("POST"), + "ROLE_PERMISSION": "recreate:id:", + "": { + "METHODS": ("GET",), + "ROLE_PERMISSION": "recreate:id:id:", + }, + }, + }, + "migrate": { + "": { + "METHODS": ("POST"), + "ROLE_PERMISSION": "migrate:id:", + "": { + "METHODS": ("GET",), + "ROLE_PERMISSION": "migrate:id:id:", + }, + }, }, } }, @@ -107,7 +149,6 @@ valid_url_methods = { class RoException(Exception): - def __init__(self, message, http_code=HTTPStatus.METHOD_NOT_ALLOWED): Exception.__init__(self, message) self.http_code = http_code @@ -118,18 +159,15 @@ class AuthException(RoException): 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} + return {"token": "ok", "id": "ok", "remote": remote} def del_token(self, token_id): pass @@ -156,11 +194,18 @@ class Server(object): "deploy:id:delete": self.ns.delete, "deploy:id:id:get": self.ns.status, "deploy:id:id:cancel:post": self.ns.cancel, + "rebuild:id:post": self.ns.rebuild_start_stop, + "start:id:post": self.ns.rebuild_start_stop, + "stop:id:post": self.ns.rebuild_start_stop, + "recreate:id:post": self.ns.recreate, + "recreate:id:id:get": self.ns.recreate_status, + "migrate:id:post": self.ns.migrate, } def _format_in(self, kwargs): try: indata = None + if cherrypy.request.body.length: error_text = "Invalid input format " @@ -171,32 +216,50 @@ class Server(object): 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) + 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"]: + 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"]: + 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) + 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 + 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) + 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 = {} @@ -213,7 +276,12 @@ class Server(object): 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"): + elif ( + k.endswith(".gt") + or k.endswith(".lt") + or k.endswith(".gte") + or k.endswith(".lte") + ): try: kwargs[k] = int(v) except Exception: @@ -251,55 +319,83 @@ class Server(object): :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) + 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' + cherrypy.response.headers["Content-Type"] = "application/zip" else: - cherrypy.response.headers["Content-Type"] = 'text/plain' + 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' + 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: + 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='"' + 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 + outdata = token_info # Home page else: - raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value, - "Method {} not allowed for tokens".format(cherrypy.request.method)) + 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 @@ -307,11 +403,19 @@ class Server(object): # 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) + 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) + 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 @@ -320,6 +424,7 @@ class Server(object): "status": e.http_code.value, "detail": str(e), } + return self._format_out(problem_details, None) def new_token(self, engine_session, indata, *args, **kwargs): @@ -329,58 +434,77 @@ class Server(object): 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"] + 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.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"): + 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 "
\ninit\nfile/  download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
-                   "sleep/
" + if args and args[0] == "help": + return ( + "
\ninit\nfile/  download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"
+                "sleep/
" + ) 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") + 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_path = cherrypy.tree.apps["/ro"].config["storage"]["path"] + "/" + args[1] f = open(f_path, "r") cherrypy.response.headers["Content-type"] = "text/plain" return f @@ -393,24 +517,32 @@ class Server(object): 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.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.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) @@ -418,53 +550,76 @@ class Server(object): elif len(args) >= 2 and args[0] == "message": main_topic = args[1] return_text = "
{} ->\n".format(main_topic)
+
             try:
-                if cherrypy.request.method == 'POST':
+                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':
+                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))
+                        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 += "
\n" + return return_text return_text = ( - "
\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")
+            "
\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)))))
+                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 += "
" + 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) + 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) + raise RoException( + "URL contains unexpected extra items '{}'".format(arg), + HTTPStatus.METHOD_NOT_ALLOWED, + ) if arg in reference: reference = reference[arg] @@ -474,11 +629,22 @@ class Server(object): # reference = reference["*"] break else: - raise RoException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED) + 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) + 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) + raise RoException( + "Method {} not supported for this URL".format(method), + HTTPStatus.METHOD_NOT_ALLOWED, + ) + return reference["ROLE_PERMISSION"] + method.lower() @staticmethod @@ -492,71 +658,137 @@ class Server(object): :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) + 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): + 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"): + 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) + 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) + 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 + + 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)): + 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 + 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) + 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) + 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) + 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) @@ -565,6 +797,7 @@ class Server(object): "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: @@ -572,7 +805,9 @@ class Server(object): 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]) + cherrypy.request.login += ";{}={}".format( + logging_id, outdata[logging_id][:36] + ) def _start_service(): @@ -587,24 +822,27 @@ def _start_service(): cherrypy.log.error("Starting osm_ng_ro") # update general cherrypy configuration update_dict = {} + engine_config = cherrypy.tree.apps["/ro"].config - 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) + 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) + engine_config["/"]["tools." + k2.replace("_", ".")] = yaml.safe_load(v) elif k1 in ("message", "database", "storage", "authentication"): engine_config[k1][k2] = yaml.safe_load(v) @@ -616,26 +854,35 @@ def _start_service(): 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') + 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 = 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 = 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.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 - }.items(): + 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, + }.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') + 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) @@ -643,24 +890,32 @@ def _start_service(): if engine_config["global"].get("log.level"): logger_cherry.setLevel(engine_config["global"]["log.level"]) logger.setLevel(engine_config["global"]["log.level"]) + # logging other modules - for k1, logname in {"message": "ro.msg", "database": "ro.db", "storage": "ro.fs"}.items(): + 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 = 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) + 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_admin_thread = VimAdminThread(config=engine_config, engine=ro_server.ns) @@ -678,37 +933,45 @@ def _stop_service(): TODO: Ending database connections. """ global vim_admin_thread + # terminate vim_admin_thread if vim_admin_thread: vim_admin_thread.terminate() + vim_admin_thread = None - cherrypy.tree.apps['/ro'].root.ns.stop() + 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) + cherrypy.engine.subscribe("start", _start_service) + cherrypy.engine.subscribe("stop", _stop_service) + cherrypy.quickstart(ro_server, "/ro", config_file) def usage(): - print("""Usage: {} [options] + print( + """Usage: {} [options] -c|--config [configuration_file]: loads the configuration file (default: ./ro.cfg) -h|--help: shows this help - """.format(sys.argv[0])) + """.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__': +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() @@ -717,17 +980,29 @@ if __name__ == '__main__': 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) + 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"): + 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) + 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 KeyboardInterrupt: print("KeyboardInterrupt. Finishing", file=sys.stderr)