X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fnbi.py;h=c4c61ade416e30c1aa080b8087850c8d7cf3ebed;hp=9cdb409b2bd2b158e5b12f4857401101314b93ab;hb=dfe0957eb800224f10ab44797d4d9e05ed7aa69a;hpb=c94c3df90aa64298a7935a80b221f80f3c043260 diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 9cdb409..c4c61ad 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -9,49 +9,77 @@ import html_out as html import logging from engine import Engine, EngineException from dbbase import DbException +from fsbase import FsException +from msgbase import MsgException from base64 import standard_b64decode -from os import getenv +#from os import getenv from http import HTTPStatus -from http.client import responses as http_responses +#from http.client import responses as http_responses from codecs import getreader from os import environ __author__ = "Alfonso Tierno " -__version__ = "0.1" -version_date = "Feb 2018" + +# TODO consider to remove and provide version using the static version file +__version__ = "0.1.3" +version_date = "Apr 2018" +database_version = '1.0' """ -North Bound Interface (O: OSM; S: SOL5 +North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented) URL: /osm GET POST PUT DELETE PATCH - /nsd/v1 + /nsd/v1 O O + /ns_descriptors_content O O + / O O O O /ns_descriptors O5 O5 / O5 O5 5 /nsd_content O5 O5 + /nsd O + /artifacts[/] O /pnf_descriptors 5 5 / 5 5 5 /pnfd_content 5 5 - /subcriptions 5 5 - / 5 X + /subscriptions 5 5 + / 5 X /vnfpkgm/v1 + /vnf_packages_content O O + / O O /vnf_packages O5 O5 / O5 O5 5 - /vnfd O5 O /package_content O5 O5 /upload_from_uri X - /artifacts/ X X + /vnfd O5 + /artifacts[/] O5 + /subscriptions X X + / X X /nslcm/v1 - /ns_instances O5 O5 - / O5 O5 - TO BE COMPLETED + /ns_instances_content O O + / O O + /ns_instances 5 5 + / 5 5 + instantiate O5 + terminate O5 + action O + scale O5 + heal 5 /ns_lcm_op_occs 5 5 / 5 5 5 TO BE COMPLETED 5 5 - /subcriptions 5 5 - / 5 X + /subscriptions 5 5 + / 5 X + /admin/v1 + /tokens O O + / O O + /users O O + / O O + /projects O O + / O O + /vims O O + / O O O + /sdns O O + / O O O query string. [....]*[.]=[,...]&... @@ -106,6 +134,88 @@ class Server(object): def __init__(self): self.instance += 1 self.engine = Engine() + self.valid_methods = { # contains allowed URL and methods + "admin": { + "v1": { + "tokens": {"METHODS": ("GET", "POST", "DELETE"), + "": { "METHODS": ("GET", "DELETE")} + }, + "users": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "POST", "DELETE")} + }, + "projects": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} + }, + "vims": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} + }, + "sdns": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} + }, + } + }, + "nsd": { + "v1": { + "ns_descriptors_content": { "METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "PUT", "DELETE")} + }, + "ns_descriptors": { "METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH", + "nsd_content": { "METHODS": ("GET", "PUT")}, + "nsd": {"METHODS": "GET"}, # descriptor inside package + "artifacts": {"*": {"METHODS": "GET"}} + } + + }, + "pnf_descriptors": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE", "PATCH"), + "pnfd_content": {"TODO": ("GET", "PUT")} + } + }, + "subscriptions": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE"),} + }, + } + }, + "vnfpkgm": { + "v1": { + "vnf_packages_content": { "METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "PUT", "DELETE")} + }, + "vnf_packages": { "METHODS": ("GET", "POST"), + "": { "METHODS": ("GET", "DELETE"), "TODO": "PATCH", # GET: vnfPkgInfo + "package_content": { "METHODS": ("GET", "PUT"), # package + "upload_from_uri": {"TODO": "POST"} + }, + "vnfd": {"METHODS": "GET"}, # descriptor inside package + "artifacts": {"*": {"METHODS": "GET"}} + } + + }, + "subscriptions": {"TODO": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE"),} + }, + } + }, + "nslcm": { + "v1": { + "ns_instances_content": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE")} + }, + "ns_instances": {"METHODS": ("GET", "POST"), + "": {"TODO": ("GET", "DELETE"), + "scale": {"TODO": "POST"}, + "terminate": {"METHODS": "POST"}, + "instantiate": {"METHODS": "POST"}, + "action": {"METHODS": "POST"}, + } + }, + "ns_lcm_op_occs": {"METHODS": "GET", + "": {"METHODS": "GET"}, + } + } + }, + } def _authorization(self): token = None @@ -164,14 +274,15 @@ class Server(object): indata = yaml.load(cherrypy.request.body) 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"]: - indata = cherrypy.request.body.read() + "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 NbiException("empty file or content", HTTPStatus.BAD_REQUEST) - indata = filecontent.file.read() + indata = filecontent.file # .read() if filecontent.content_type.value: cherrypy.request.headers["Content-Type"] = filecontent.content_type.value else: @@ -186,10 +297,6 @@ class Server(object): if not indata: indata = {} - if "METHOD" in kwargs: - method = kwargs.pop("METHOD") - else: - method = cherrypy.request.method format_yaml = False if cherrypy.request.headers.get("Query-String-Format") == "yaml": format_yaml = True @@ -223,22 +330,37 @@ class Server(object): except: pass - return indata, method + return indata except (ValueError, yaml.YAMLError) as exc: raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST) except KeyError as exc: raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST) @staticmethod - def _format_out(data, session=None): + def _format_out(data, session=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 or text + :param data: response to be sent. Can be a dict, text or file :param session: + :param _format: The format to be set as Content-Type ir data is a file :return: None """ - if "Accept" in cherrypy.request.headers: - accept = cherrypy.request.headers["Accept"] + 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, session) + 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" @@ -246,7 +368,7 @@ class Server(object): elif "text/html" in accept: return html.format(data, cherrypy.request, cherrypy.response, session) - elif "application/yaml" in accept or "*/*" in accept: + elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept: pass else: raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value, @@ -275,20 +397,36 @@ class Server(object): return self._format_out("Welcome to OSM!", session) @cherrypy.expose - def token(self, *args, **kwargs): - if not args: - raise NbiException("URL must contain at least 'item/version'", HTTPStatus.METHOD_NOT_ALLOWED) - version = args[0] - if version != 'v1': - raise NbiException("URL version '{}' not supported".format(version), HTTPStatus.METHOD_NOT_ALLOWED) + def version(self, *args, **kwargs): + # TODO consider to remove and provide version using the static version file + global __version__, version_date + try: + if cherrypy.request.method != "GET": + raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED) + elif args or kwargs: + raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED) + return __version__ + " " + version_date + except NbiException 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) + + @cherrypy.expose + def token(self, method, token_id=None, kwargs=None): session = None # self.engine.load_dbase(cherrypy.request.app.config) + indata = self._format_in(kwargs) + if not isinstance(indata, dict): + raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST) try: - indata, method = self._format_in(kwargs) if method == "GET": session = self._authorization() - if len(args) >= 2: - outdata = self.engine.get_token(session, args[1]) + if token_id: + outdata = self.engine.get_token(session, token_id) else: outdata = self.engine.get_token_list(session) elif method == "POST": @@ -301,17 +439,17 @@ class Server(object): outdata = self.engine.new_token(session, indata, cherrypy.request.remote) session = outdata cherrypy.session['Authorization'] = outdata["_id"] + self._set_location_header("admin", "v1", "tokens", outdata["_id"]) # cherrypy.response.cookie["Authorization"] = outdata["id"] # cherrypy.response.cookie["Authorization"]['expires'] = 3600 elif method == "DELETE": - if len(args) >= 2 and "logout" not in args: - token_id = args[1] - elif "id" in kwargs: + if not token_id and "id" in kwargs: token_id = kwargs["id"] - else: + elif not token_id: session = self._authorization() token_id = session["_id"] outdata = self.engine.del_token(token_id) + oudata = None session = None cherrypy.session['Authorization'] = "logout" # cherrypy.response.cookie["Authorization"] = token_id @@ -332,7 +470,11 @@ class Server(object): @cherrypy.expose def test(self, *args, **kwargs): thread_info = None - if args and args[0] == "init": + if args and args[0] == "help": + return "
\ninit\nfile/  download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
+                    "sleep/
" + + elif args and args[0] == "init": try: # self.engine.load_dbase(cherrypy.request.app.config) self.engine.create_admin() @@ -340,6 +482,17 @@ class Server(object): 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['/osm'].config["storage"]["path"] + "/" + args[1], + "text/plain", "attachment") + elif args and args[0] == "file2": + f_path = cherrypy.tree.apps['/osm'].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": + return self.engine.del_item_list({"project_id": "admin"}, args[1], {}) elif args and args[0] == "prune": return self.engine.prune() elif args and args[0] == "login": @@ -363,12 +516,21 @@ class Server(object): # thread_info elif len(args) >= 2 and args[0] == "message": topic = args[1] + return_text = "
{} ->\n".format(topic)
             try:
-                for k, v in kwargs.items():
-                    self.engine.msg.write(topic, k, yaml.load(v))
-                return "ok"
+                if cherrypy.request.method == 'POST':
+                    to_send = yaml.load(cherrypy.request.body)
+                    for k, v in to_send.items():
+                        self.engine.msg.write(topic, k, v)
+                        return_text += "  {}: {}\n".format(k, v)
+                elif cherrypy.request.method == 'GET':
+                    for k, v in kwargs.items():
+                        self.engine.msg.write(topic, k, yaml.load(v))
+                        return_text += "  {}: {}\n".format(k, yaml.load(v))
             except Exception as e:
-                return "Error: " + format(e)
+                return_text += "Error: " + str(e)
+            return_text += "
\n" + return return_text return_text = ( "
\nheaders:\n  args: {}\n".format(args) +
@@ -390,85 +552,168 @@ class Server(object):
         return_text += "
" return return_text + def _check_valid_url_method(self, method, *args): + if len(args) < 3: + raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED) + + reference = self.valid_methods + for arg in args: + if arg is None: + break + if not isinstance(reference, dict): + raise NbiException("URL contains unexpected extra items '{}'".format(arg), + HTTPStatus.METHOD_NOT_ALLOWED) + + if arg in reference: + reference = reference[arg] + elif "" in reference: + reference = reference[""] + elif "*" in reference: + reference = reference["*"] + break + else: + raise NbiException("Unexpected URL item {}".format(arg), HTTPStatus.METHOD_NOT_ALLOWED) + if "TODO" in reference and method in reference["TODO"]: + raise NbiException("Method {} not supported yet for this URL".format(method), HTTPStatus.NOT_IMPLEMENTED) + elif "METHODS" in reference and not method in reference["METHODS"]: + raise NbiException("Method {} not supported for this URL".format(method), HTTPStatus.METHOD_NOT_ALLOWED) + return + + @staticmethod + def _set_location_header(topic, version, item, id): + """ + Insert response header Location with the URL of created item base on URL params + :param topic: + :param version: + :param item: + :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"] = "/osm/{}/{}/{}/{}".format(topic, version, item, id) + return + @cherrypy.expose - def default(self, *args, **kwargs): + def default(self, topic=None, version=None, item=None, _id=None, item2=None, *args, **kwargs): session = None + outdata = None + _format = None + method = "DONE" + engine_item = None try: - if not args or len(args) < 2: - raise NbiException("URL must contain at least 'item/version'", HTTPStatus.METHOD_NOT_ALLOWED) - item = args[0] - version = args[1] - if item not in ("token", "user", "project", "vnfpkgm", "nsd", "nslcm"): - raise NbiException("URL item '{}' not supported".format(item), HTTPStatus.METHOD_NOT_ALLOWED) + if not topic or not version or not item: + raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED) + if topic not in ("admin", "vnfpkgm", "nsd", "nslcm"): + raise NbiException("URL topic '{}' not supported".format(topic), HTTPStatus.METHOD_NOT_ALLOWED) if version != 'v1': raise NbiException("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 + + self._check_valid_url_method(method, topic, version, item, _id, item2, *args) + + if topic == "admin" and item == "tokens": + return self.token(method, _id, kwargs) + # self.engine.load_dbase(cherrypy.request.app.config) session = self._authorization() - indata, method = self._format_in(kwargs) - _id = None - - if item == "nsd": - item = "nsds" - if len(args) < 3 or args[2] != "ns_descriptors": - raise NbiException("only ns_descriptors is allowed", HTTPStatus.METHOD_NOT_ALLOWED) - if len(args) > 3: - _id = args[3] - if len(args) > 4 and args[4] != "nsd_content": - raise NbiException("only nsd_content is allowed", HTTPStatus.METHOD_NOT_ALLOWED) - elif item == "vnfpkgm": - item = "vnfds" - if len(args) < 3 or args[2] != "vnf_packages": - raise NbiException("only vnf_packages is allowed", HTTPStatus.METHOD_NOT_ALLOWED) - if len(args) > 3: - _id = args[3] - if len(args) > 4 and args[4] not in ("vnfd", "package_content"): - raise NbiException("only vnfd or package_content are allowed", HTTPStatus.METHOD_NOT_ALLOWED) - elif item == "nslcm": - item = "nsrs" - if len(args) < 3 or args[2] != "ns_instances": - raise NbiException("only ns_instances is allowed", HTTPStatus.METHOD_NOT_ALLOWED) - if len(args) > 3: - _id = args[3] - if len(args) > 4: - raise NbiException("This feature is not implemented", HTTPStatus.METHOD_NOT_ALLOWED) - else: - if len(args) >= 3: - _id = args[2] - item += "s" + indata = self._format_in(kwargs) + engine_item = item + if item == "subscriptions": + engine_item = topic + "_" + item + if item2: + engine_item = item2 + + if topic == "nsd": + engine_item = "nsds" + elif topic == "vnfpkgm": + engine_item = "vnfds" + elif topic == "nslcm": + engine_item = "nsrs" + if item == "ns_lcm_op_occs": + engine_item = "nslcmops" if method == "GET": - if not _id: - outdata = self.engine.get_item_list(session, item, kwargs) - else: # len(args) > 1 - outdata = self.engine.get_item(session, item, _id) + if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"): + if item2 in ("vnfd", "nsd"): + path = "$DESCRIPTOR" + elif args: + path = args + elif item2 == "artifacts": + path = () + else: + path = None + file, _format = self.engine.get_file(session, engine_item, _id, path, + cherrypy.request.headers.get("Accept")) + outdata = file + elif not _id: + outdata = self.engine.get_item_list(session, engine_item, kwargs) + else: + outdata = self.engine.get_item(session, engine_item, _id) elif method == "POST": - id, completed = self.engine.new_item(session, item, indata, kwargs, cherrypy.request.headers) - if not completed: - cherrypy.response.headers["Transaction-Id"] = id - cherrypy.response.status = HTTPStatus.CREATED.value + if item in ("ns_descriptors_content", "vnf_packages_content"): + _id = cherrypy.request.headers.get("Transaction-Id") + if not _id: + _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers) + completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers) + if completed: + self._set_location_header(topic, version, item, _id) + else: + cherrypy.response.headers["Transaction-Id"] = _id + outdata = {"id": _id} + elif item == "ns_instances_content": + _id = self.engine.new_item(session, engine_item, indata, kwargs) + self.engine.ns_action(session, _id, "instantiate", {}, None) + self._set_location_header(topic, version, item, _id) + outdata = {"id": _id} + elif item == "ns_instances" and item2: + _id = self.engine.ns_action(session, _id, item2, indata, kwargs) + self._set_location_header(topic, version, "ns_lcm_op_occs", _id) + outdata = {"id": _id} + cherrypy.response.status = HTTPStatus.ACCEPTED.value else: - cherrypy.response.headers["Location"] = cherrypy.request.base + "/osm/" + "/".join(args[0:3]) + "/" + id - outdata = {"id": id} + _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers) + self._set_location_header(topic, version, item, _id) + outdata = {"id": _id} + # TODO form NsdInfo when item in ("ns_descriptors", "vnf_packages") + cherrypy.response.status = HTTPStatus.CREATED.value elif method == "DELETE": if not _id: - outdata = self.engine.del_item_list(session, item, kwargs) + outdata = self.engine.del_item_list(session, engine_item, kwargs) else: # len(args) > 1 - outdata = self.engine.del_item(session, item, _id) + if item == "ns_instances_content": + self.engine.ns_action(session, _id, "terminate", {"autoremove": True}, None) + else: + force = kwargs.get("FORCE") + self.engine.del_item(session, engine_item, _id, force) + # TODO return 202 ACCEPTED for nsrs vims + outdata = None elif method == "PUT": - if not _id: - raise NbiException("Missing '/' at the URL to identify item to be updated", - HTTPStatus.METHOD_NOT_ALLOWED) - elif not indata and not kwargs: + if not indata and not kwargs: raise NbiException("Nothing to update. Provide payload and/or query string", HTTPStatus.BAD_REQUEST) - outdata = {"id": self.engine.edit_item(session, item, args[1], indata, kwargs)} + if item2 in ("nsd_content", "package_content"): + completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers) + if not completed: + cherrypy.response.headers["Transaction-Id"] = id + outdata = None + else: + outdata = {"id": self.engine.edit_item(session, engine_item, args[1], indata, kwargs)} else: raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED) - return self._format_out(outdata, session) - except (NbiException, EngineException, DbException) as e: + return self._format_out(outdata, session, _format) + except (NbiException, EngineException, DbException, FsException, MsgException) as e: + if hasattr(outdata, "close"): # is an open file + outdata.close() cherrypy.log("Exception {}".format(e)) cherrypy.response.status = e.http_code.value + error_text = str(e) + if isinstance(e, MsgException): + error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format( + engine_item[:-1], method, error_text) problem_details = { "code": e.http_code.name, "status": e.http_code.value, @@ -571,7 +816,7 @@ def _start_service(): # TODO add more entries, e.g.: storage cherrypy.tree.apps['/osm'].root.engine.start(engine_config) try: - cherrypy.tree.apps['/osm'].root.engine.create_admin() + cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version) except EngineException: pass # getenv('OSMOPENMANO_TENANT', None)