From 65acb4d5d56a90c9c4ff101b931f5fa6d65dcb51 Mon Sep 17 00:00:00 2001 From: tierno Date: Fri, 6 Apr 2018 16:42:40 +0200 Subject: [PATCH] adding ns_lcm_op_occs instantiate, terminate, and action (primitive) Change-Id: I1106235763179a128dba9773a6d99fc2cec5da9e Signed-off-by: tierno --- osm_nbi/engine.py | 236 +++++++++++++++++++++++++++--------------- osm_nbi/html_out.py | 14 +++ osm_nbi/nbi.py | 38 +++++-- osm_nbi/validation.py | 26 ++++- 4 files changed, 224 insertions(+), 90 deletions(-) diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index e7bf7c9..16d2834 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -282,43 +282,36 @@ class Engine(object): raise EngineException("name '{}' already exist for {}".format(indata["name"], item), HTTPStatus.CONFLICT) - - def _format_new_data(self, session, item, indata, admin=None): + def _format_new_data(self, session, item, indata): now = time() if not "_admin" in indata: indata["_admin"] = {} indata["_admin"]["created"] = now indata["_admin"]["modified"] = now if item == "users": - _id = indata["username"] + indata["_id"] = indata["username"] salt = uuid4().hex indata["_admin"]["salt"] = salt indata["password"] = sha256(indata["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest() elif item == "projects": - _id = indata["name"] + indata["_id"] = indata["name"] else: - _id = None - storage = None - if admin: - _id = admin.get("_id") - storage = admin.get("storage") - if not _id: - _id = str(uuid4()) - if item in ("vnfds", "nsds"): + if not indata.get("_id"): + indata["_id"] = str(uuid4()) + if item in ("vnfds", "nsds", "nsrs"): if not indata["_admin"].get("projects_read"): indata["_admin"]["projects_read"] = [session["project_id"]] if not indata["_admin"].get("projects_write"): indata["_admin"]["projects_write"] = [session["project_id"]] + if item in ("vnfds", "nsds"): indata["_admin"]["onboardingState"] = "CREATED" indata["_admin"]["operationalState"] = "DISABLED" indata["_admin"]["usageSate"] = "NOT_IN_USE" - elif item in ("vims", "sdns"): + if item == "nsrs": + indata["_admin"]["nsState"] = "NOT_INSTANTIATED" + if item in ("vims", "sdns"): indata["_admin"]["operationalState"] = "PROCESSING" - if storage: - indata["_admin"]["storage"] = storage - indata["_id"] = _id - def upload_content(self, session, item, _id, indata, kwargs, headers): """ Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract) @@ -457,6 +450,8 @@ class Engine(object): HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE) except IOError as e: raise EngineException("invalid upload transaction sequence: '{}'".format(e), HTTPStatus.BAD_REQUEST) + except tarfile.ReadError as e: + raise EngineException("invalid file content {}".format(e), HTTPStatus.BAD_REQUEST) except (ValueError, yaml.YAMLError) as e: raise EngineException(error_text + str(e)) finally: @@ -497,12 +492,48 @@ class Engine(object): "nsd-ref": nsd["id"], "ns-instance-config-ref": _id, "id": _id, + "_id": _id, # "input-parameter": xpath, value, "ssh-authorized-key": ns_request.get("key-pair-ref"), } ns_request["nsr_id"] = _id - return nsr_descriptor, _id + return nsr_descriptor + + @staticmethod + def _update_descriptor(desc, kwargs): + """ + Update descriptor with the kwargs + :param kwargs: + :return: + """ + if not kwargs: + return + try: + for k, v in kwargs.items(): + update_content = content + kitem_old = None + klist = k.split(".") + for kitem in klist: + if kitem_old is not None: + update_content = update_content[kitem_old] + if isinstance(update_content, dict): + kitem_old = kitem + elif isinstance(update_content, list): + kitem_old = int(kitem) + else: + raise EngineException( + "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem)) + update_content[kitem_old] = v + except KeyError: + raise EngineException( + "Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old)) + except ValueError: + raise EngineException("Invalid query string '{}'. Expected integer index list instead of '{}'".format( + k, kitem)) + except IndexError: + raise EngineException( + "Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old)) def new_item(self, session, item, indata={}, kwargs=None, headers={}): """ @@ -516,77 +547,111 @@ class Engine(object): :return: _id, transaction_id: identity of the inserted data. or transaction_id if Content-Range is used """ - transaction = None + try: + item_envelop = item + if item in ("nsds", "vnfds"): + item_envelop = "userDefinedData" + content = self._remove_envelop(item_envelop, indata) - item_envelop = item - if item in ("nsds", "vnfds"): - item_envelop = "userDefinedData" - content = self._remove_envelop(item_envelop, indata) + # Override descriptor with query string kwargs + self._update_descriptor(content, kwargs) + if not indata and item not in ("nsds", "vnfds"): + raise EngineException("Empty payload") - # Override descriptor with query string kwargs - if kwargs: - try: - for k, v in kwargs.items(): - update_content = content - kitem_old = None - klist = k.split(".") - for kitem in klist: - if kitem_old is not None: - update_content = update_content[kitem_old] - if isinstance(update_content, dict): - kitem_old = kitem - elif isinstance(update_content, list): - kitem_old = int(kitem) - else: - raise EngineException( - "Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem)) - update_content[kitem_old] = v - except KeyError: - raise EngineException( - "Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old)) - except ValueError: - raise EngineException("Invalid query string '{}'. Expected integer index list instead of '{}'".format( - k, kitem)) - except IndexError: - raise EngineException( - "Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old)) - try: validate_input(content, item, new=True) + + if item == "nsrs": + # in this case the imput descriptor is not the data to be stored + ns_request = content + content = self.new_nsr(session, ns_request) + + self._validate_new_data(session, item_envelop, content) + if item in ("nsds", "vnfds"): + content = {"_admin": {"userDefinedData": content}} + self._format_new_data(session, item, content) + _id = self.db.create(item, content) + if item == "nsrs": + pass + # self.msg.write("ns", "created", _id) # sending just for information. + elif item == "vims": + msg_data = self.db.get_one(item, {"_id": _id}) + msg_data.pop("_admin", None) + self.msg.write("vim_account", "create", msg_data) + elif item == "sdns": + msg_data = self.db.get_one(item, {"_id": _id}) + msg_data.pop("_admin", None) + self.msg.write("sdn", "create", msg_data) + return _id except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - if not indata and item not in ("nsds", "vnfds"): - raise EngineException("Empty payload") + def new_nslcmop(self, session, nsInstanceId, action, params): + now = time() + _id = str(uuid4()) + nslcmop = { + "id": _id, + "_id": _id, + "operationState": "PROCESSING", # COMPLETED,PARTIALLY_COMPLETED,FAILED_TEMP,FAILED,ROLLING_BACK,ROLLED_BACK + "statusEnteredTime": now, + "nsInstanceId": nsInstanceId, + "lcmOperationType": action, + "startTime": now, + "isAutomaticInvocation": False, + "operationParams": params, + "isCancelPending": False, + "links": { + "self": "/osm/nslcm/v1/ns_lcm_op_occs/" + _id, + "nsInstance": "/osm/nslcm/v1/ns_instances/" + nsInstanceId, + } + } + return nslcmop - if item == "nsrs": - # in this case the imput descriptor is not the data to be stored - ns_request = content - content, _id = self.new_nsr(session, ns_request) - transaction = {"_id": _id} - - self._validate_new_data(session, item_envelop, content) - if item in ("nsds", "vnfds"): - content = {"_admin": {"userDefinedData": content}} - self._format_new_data(session, item, content, transaction) - _id = self.db.create(item, content) - if item == "nsrs": - self.msg.write("ns", "create", _id) - elif item == "vims": - msg_data = self.db.get_one(item, {"_id": _id}) - msg_data.pop("_admin", None) - self.msg.write("vim_account", "create", msg_data) - elif item == "sdns": - msg_data = self.db.get_one(item, {"_id": _id}) - msg_data.pop("_admin", None) - self.msg.write("sdn", "create", msg_data) - return _id + def ns_action(self, session, nsInstanceId, action, indata, kwargs=None): + """ + Performs a new action over a ns + :param session: contains the used login username and working project + :param nsInstanceId: _id of the nsr to perform the action + :param action: it can be: instantiate, terminate, action, TODO: update, heal + :param indata: descriptor with the parameters of the action + :param kwargs: used to override the indata descriptor + :return: id of the nslcmops + """ + try: + # Override descriptor with query string kwargs + self._update_descriptor(indata, kwargs) + validate_input(indata, "ns_" + action, new=True) + # get ns from nsr_id + nsr = self.get_item(session, "nsrs", nsInstanceId) + if nsr["_admin"]["nsState"] == "NOT_INSTANTIATED": + if action == "terminate" and indata.get("autoremove"): + # NSR must be deleted + return self.del_item(session, "nsrs", nsInstanceId) + if action != "instantiate": + raise EngineException("ns_instance '{}' cannot be '{}' because it is not instantiated".format( + nsInstanceId, action), HTTPStatus.CONFLICT) + else: + if action == "instantiate" and not indata.get("force"): + raise EngineException("ns_instance '{}' cannot be '{}' because it is already instantiated".format( + nsInstanceId, action), HTTPStatus.CONFLICT) + indata["nsInstanceId"] = nsInstanceId + # TODO + nslcmop = self.new_nslcmop(session, nsInstanceId, action, indata) + self._format_new_data(session, "nslcmops", nslcmop) + _id = self.db.create("nslcmops", nslcmop) + indata["_id"] = _id + self.msg.write("ns", action, nslcmop) + return _id + except ValidationError as e: + raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) + # except DbException as e: + # raise EngineException("Cannot get ns_instance '{}': {}".format(e), HTTPStatus.NOT_FOUND) def _add_read_filter(self, session, item, filter): if session["project_id"] == "admin": # allows all return filter if item == "users": filter["username"] = session["username"] - elif item == "vnfds" or item == "nsds": + elif item in ("vnfds", "nsds", "nsrs"): filter["_admin.projects_read.cont"] = ["ANY", session["project_id"]] def _add_delete_filter(self, session, item, filter): @@ -698,12 +763,13 @@ class Engine(object): self._add_read_filter(session, item, filter) return self.db.del_list(item, filter) - def del_item(self, session, item, _id): + def del_item(self, session, item, _id, force=False): """ Get complete information on an items :param session: contains the used login username and working project :param item: it can be: users, projects, vnfds, nsds, ... :param _id: server id of the item + :param force: indicates if deletion must be forced in case of conflict :return: dictionary, raise exception if not found. """ # TODO add admin to filter, validate rights @@ -711,13 +777,21 @@ class Engine(object): filter = {"_id": _id} self._add_delete_filter(session, item, filter) - if item in ("nsrs", "vims", "sdns"): + if item == "nsrs": + nsr = self.db.get_one(item, filter) + if nsr["_admin"]["nsState"] == "INSTANTIATED" and not force: + raise EngineException("nsr '{}' cannot be deleted because it is in 'INSTANTIATED' state. " + "Launch 'terminate' action first; or force deletion".format(_id), + http_code=HTTPStatus.CONFLICT) + v = self.db.del_one(item, {"_id": _id}) + self.db.del_list("nslcmops", {"nsInstanceId": _id}) + self.msg.write("ns", "deleted", {"_id": _id}) + return v + if item in ("vims", "sdns"): desc = self.db.get_one(item, filter) desc["_admin"]["to_delete"] = True self.db.replace(item, _id, desc) # TODO change to set_one - if item == "nsrs": - self.msg.write("ns", "delete", _id) - elif item == "vims": + if item == "vims": self.msg.write("vim_account", "delete", {"_id": _id}) elif item == "sdns": self.msg.write("sdn", "delete", {"_id": _id}) diff --git a/osm_nbi/html_out.py b/osm_nbi/html_out.py index 23ee97b..1d733bc 100644 --- a/osm_nbi/html_out.py +++ b/osm_nbi/html_out.py @@ -25,6 +25,7 @@ html_start = """ USERs PROJECTs TOKENs + VIMs logout @@ -88,6 +89,15 @@ html_upload_body = """ """ +html_nslcmop_body = """ +nslcm operations +
+

+ +

+
+""" + def format(data, request, response, session): """ @@ -123,6 +133,10 @@ def format(data, request, response, session): body += ' show '.format(response.headers["Location"]) else: body += ' '.format(request.path_info) + if request.path_info.startswith("/nslcm/v1/ns_instances_content/") or \ + request.path_info.startswith("/nslcm/v1/ns_instances/"): + _id = request.path_info[request.path_info.rfind("/")+1:] + body += html_nslcmop_body.format(id=_id) body += "
" + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False) + "
" elif data is None: if request.method == "DELETE" or "METHOD=DELETE" in request.query_string: diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 0520ef8..aa8105a 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -57,7 +57,11 @@ 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 @@ -196,8 +200,16 @@ 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"}, } } }, @@ -618,6 +630,8 @@ class Server(object): engine_item = "vnfds" elif topic == "nslcm": engine_item = "nsrs" + if item == "ns_lcm_op_occs": + engine_item = "nslcmops" if method == "GET": if item2 in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"): @@ -647,22 +661,32 @@ class Server(object): 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) + self.engine.ns_action(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_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: _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, engine_item, kwargs) else: # len(args) > 1 + 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 - self.engine.del_item(session, engine_item, _id) outdata = None elif method == "PUT": if not indata and not kwargs: diff --git a/osm_nbi/validation.py b/osm_nbi/validation.py index 2419c1a..5a2fbdc 100644 --- a/osm_nbi/validation.py +++ b/osm_nbi/validation.py @@ -44,6 +44,26 @@ log_level_schema = {"type": "string", "enum": ["DEBUG", "INFO", "WARNING", "ERRO checksum_schema = {"type": "string", "pattern": "^[0-9a-fA-F]{32}$"} size_schema = {"type": "integer", "minimum": 1, "maximum": 100} + +ns_instantiate = { + "title": "ns action instantiate input schema", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", +} +ns_action = { # TODO for the moment it is only contemplated the vnfd primitive execution + "title": "ns action update input schema", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "vnf_member_index": name_schema, + "primitive": name_schema, + "primitive_params": {"type": "object"}, + }, + "required": ["vnf_member_index", "primitive", "primitive_params"], + "additionalProperties": False +} + + schema_version = {"type": "string", "enum": ["1.0"]} schema_type = {"type": "string"} @@ -153,7 +173,9 @@ sdn_external_port_schema = { nbi_new_input_schemas = { "vims": vim_new_schema, - "sdns": sdn_new_schema + "sdns": sdn_new_schema, + "ns_instantiate": ns_instantiate, + "ns_action": ns_action, } nbi_edit_input_schemas = { @@ -170,7 +192,7 @@ def validate_input(indata, item, new=True): """ Validates input data agains json schema :param indata: user input data. Should be a dictionary - :param item: can be users, projects, vims, sdns + :param item: can be users, projects, vims, sdns, ns_xxxxx :param new: True if the validation is for creating or False if it is for editing :return: None if ok, raises ValidationError exception otherwise """ -- 2.25.1