X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fnbi.py;h=467f3b9a87bb194a3f66a9b5075b5bcedd138fe3;hp=0a344fbb4fb331f657500d102723162effe3dab7;hb=f5298be0fd06ca35e827fca738f8ef5747da12fd;hpb=55945e7f57333fac8d39e4aa158c033c6f99e00f diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 0a344fb..467f3b9 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -7,20 +7,26 @@ import json import yaml import html_out as html import logging +import logging.handlers +import getopt +import sys from engine import Engine, EngineException -from dbbase import DbException -from fsbase import FsException -from msgbase import MsgException +from osm_common.dbbase import DbException +from osm_common.fsbase import FsException +from osm_common.msgbase import MsgException from base64 import standard_b64decode #from os import getenv from http import HTTPStatus #from http.client import responses as http_responses from codecs import getreader -from os import environ +from os import environ, path __author__ = "Alfonso Tierno " -__version__ = "0.3" + +# 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 specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented) @@ -56,10 +62,16 @@ URL: /osm GET POST / O O /ns_instances 5 5 / 5 5 - TO BE COMPLETED + instantiate O5 + terminate O5 + action O + scale O5 + heal 5 /ns_lcm_op_occs 5 5 / 5 5 5 TO BE COMPLETED 5 5 + /vnfrs O + / O /subscriptions 5 5 / 5 X /admin/v1 @@ -69,7 +81,7 @@ URL: /osm GET POST / O O /projects O O / O O - /vims O O + /vims_accounts (also vims for compatibility) O O / O O O /sdns O O / O O O @@ -142,8 +154,11 @@ class Server(object): "vims": {"METHODS": ("GET", "POST"), "": {"METHODS": ("GET", "DELETE")} }, + "vim_accounts": {"METHODS": ("GET", "POST"), + "": {"METHODS": ("GET", "DELETE", "PATCH")} + }, "sdns": {"METHODS": ("GET", "POST"), - "": {"METHODS": ("GET", "DELETE")} + "": {"METHODS": ("GET", "DELETE", "PATCH")} }, } }, @@ -195,9 +210,20 @@ class Server(object): "ns_instances_content": {"METHODS": ("GET", "POST"), "": {"METHODS": ("GET", "DELETE")} }, - "ns_instances": {"TODO": ("GET", "POST"), - "": {"TODO": ("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"}, + }, + "vnfrs": {"METHODS": ("GET"), + "": {"METHODS": ("GET")} + }, } }, } @@ -320,6 +346,8 @@ class Server(object): raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST) except KeyError as exc: raise NbiException("Query string error: " + str(exc), HTTPStatus.BAD_REQUEST) + except Exception as exc: + raise NbiException(error_text + str(exc), HTTPStatus.BAD_REQUEST) @staticmethod def _format_out(data, session=None, _format=None): @@ -334,7 +362,7 @@ class Server(object): 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 + # cherrypy.response.status = HTTPStatus.NO_CONTENT.value return elif hasattr(data, "read"): # file object if _format: @@ -383,6 +411,7 @@ class Server(object): @cherrypy.expose 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": @@ -584,6 +613,7 @@ class Server(object): _format = None method = "DONE" engine_item = None + rollback = None try: if not topic or not version or not item: raise NbiException("URL must contain at least 'topic/version/item'", HTTPStatus.METHOD_NOT_ALLOWED) @@ -596,6 +626,10 @@ class Server(object): method = kwargs.pop("METHOD") else: method = cherrypy.request.method + if kwargs and "FORCE" in kwargs: + force = kwargs.pop("FORCE") + else: + force = False self._check_valid_url_method(method, topic, version, item, _id, item2, *args) @@ -617,6 +651,12 @@ class Server(object): engine_item = "vnfds" elif topic == "nslcm": engine_item = "nsrs" + if item == "ns_lcm_op_occs": + engine_item = "nslcmops" + if item == "vnfrs": + engine_item = "vnfrs" + if engine_item == "vims": # TODO this is for backward compatibility, it will remove in the future + engine_item = "vim_accounts" if method == "GET": if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"): @@ -639,30 +679,49 @@ class Server(object): 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) + _id = self.engine.new_item(session, engine_item, {}, None, cherrypy.request.headers, + force=force) + rollback = {"session": session, "item": engine_item, "_id": _id, "force": True} 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 in ("ns_descriptors", "vnf_packages"): - _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers) + elif item == "ns_instances_content": + _id = self.engine.new_item(session, engine_item, indata, kwargs, force=force) + rollback = {"session": session, "item": engine_item, "_id": _id, "force": True} + self.engine.ns_operation(session, _id, "instantiate", {}, None) self._set_location_header(topic, version, item, _id) - #TODO form NsdInfo outdata = {"id": _id} + elif item == "ns_instances" and item2: + _id = self.engine.ns_operation(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: - _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers) + _id = self.engine.new_item(session, engine_item, indata, kwargs, cherrypy.request.headers, + force=force) 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, engine_item, kwargs) + cherrypy.response.status = HTTPStatus.OK.value else: # len(args) > 1 - # TODO return 202 ACCEPTED for nsrs vims - self.engine.del_item(session, engine_item, _id) - outdata = None + if item == "ns_instances_content": + opp_id = self.engine.ns_operation(session, _id, "terminate", {"autoremove": True}, None) + outdata = {"_id": opp_id} + cherrypy.response.status = HTTPStatus.ACCEPTED.value + else: + self.engine.del_item(session, engine_item, _id, force) + cherrypy.response.status = HTTPStatus.NO_CONTENT.value + if engine_item in ("vim_accounts", "sdns"): + cherrypy.response.status = HTTPStatus.ACCEPTED.value + elif method == "PUT": if not indata and not kwargs: raise NbiException("Nothing to update. Provide payload and/or query string", @@ -671,17 +730,28 @@ class Server(object): completed = self.engine.upload_content(session, engine_item, _id, indata, kwargs, cherrypy.request.headers) if not completed: cherrypy.response.headers["Transaction-Id"] = id + cherrypy.response.status = HTTPStatus.NO_CONTENT.value outdata = None else: - outdata = {"id": self.engine.edit_item(session, engine_item, args[1], indata, kwargs)} + outdata = {"id": self.engine.edit_item(session, engine_item, _id, indata, kwargs, force=force)} + elif method == "PATCH": + 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, engine_item, _id, indata, kwargs, force=force)} else: raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED) 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 + if hasattr(outdata, "close"): # is an open file + outdata.close() + if rollback: + try: + self.engine.del_item(**rollback) + except Exception as e2: + cherrypy.log("Rollback Exception {}: {}".format(rollback, e2)) error_text = str(e) if isinstance(e, MsgException): error_text = "{} has been '{}' but other modules cannot be informed because an error on bus".format( @@ -729,10 +799,10 @@ def _start_service(): update_dict['server.socket_port'] = int(v) elif k == 'OSMNBI_SOCKET_HOST' or k == 'OSMNBI_SERVER_HOST': update_dict['server.socket_host'] = v - elif k1 == "server": - update_dict['server' + k2] = v - # TODO add more entries + elif k1 in ("server", "test", "auth", "log"): + update_dict[k1 + '.' + k2] = v elif k1 in ("message", "database", "storage"): + # k2 = k2.replace('_', '.') if k2 == "port": engine_config[k1][k2] = int(v) else: @@ -744,6 +814,7 @@ def _start_service(): 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" @@ -753,8 +824,8 @@ def _start_service(): logger_cherry = logging.getLogger("cherrypy") logger_nbi = logging.getLogger("nbi") - if "logfile" in engine_config["global"]: - file_handler = logging.handlers.RotatingFileHandler(engine_config["global"]["logfile"], + 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) @@ -770,9 +841,9 @@ def _start_service(): str_handler.setFormatter(log_formatter_cherry) logger.addHandler(str_handler) - if engine_config["global"].get("loglevel"): - logger_cherry.setLevel(engine_config["global"]["loglevel"]) - logger_nbi.setLevel(engine_config["global"]["loglevel"]) + 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": "nbi.msg", "database": "nbi.db", "storage": "nbi.fs"}.items(): @@ -788,7 +859,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) @@ -802,7 +873,7 @@ def _stop_service(): cherrypy.tree.apps['/osm'].root.engine.stop() cherrypy.log.error("Stopping osm_nbi") -def nbi(): +def nbi(config_file): # conf = { # '/': { # #'request.dispatch': cherrypy.dispatch.MethodDispatcher(), @@ -822,8 +893,51 @@ def nbi(): # 'tools.auth_basic.checkpassword': validate_password}) cherrypy.engine.subscribe('start', _start_service) cherrypy.engine.subscribe('stop', _stop_service) - cherrypy.quickstart(Server(), '/osm', "nbi.cfg") + cherrypy.quickstart(Server(), '/osm', config_file) + + +def usage(): + print("""Usage: {} [options] + -c|--config [configuration_file]: loads the configuration file (default: ./nbi.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__': - nbi() + 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 + # elif o == "--log-socket-port": + # log_socket_port = a + # elif o == "--log-socket-host": + # log_socket_host = a + # elif o == "--log-file": + # log_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 (__file__[:__file__.rfind(".")] + ".cfg", "./nbi.cfg", "/etc/osm/nbi.cfg"): + if path.isfile(config_file): + break + else: + print("No configuration file 'nbi.cfg' found neither at local folder nor at /etc/osm/", file=sys.stderr) + exit(1) + nbi(config_file) + except getopt.GetoptError as e: + print(str(e), file=sys.stderr) + # usage() + exit(1)