adding ns_lcm_op_occs instantiate, terminate, and action (primitive) 48/5948/9
authortierno <alfonso.tiernosepulveda@telefonica.com>
Fri, 6 Apr 2018 14:42:40 +0000 (16:42 +0200)
committertierno <alfonso.tiernosepulveda@telefonica.com>
Fri, 20 Apr 2018 14:07:18 +0000 (14:07 +0000)
Change-Id: I1106235763179a128dba9773a6d99fc2cec5da9e
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
osm_nbi/engine.py
osm_nbi/html_out.py
osm_nbi/nbi.py
osm_nbi/validation.py

index e7bf7c9..16d2834 100644 (file)
@@ -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})
index 23ee97b..1d733bc 100644 (file)
@@ -25,6 +25,7 @@ html_start = """
       <a href="/osm/admin/v1/users">USERs </a>
       <a href="/osm/admin/v1/projects">PROJECTs </a>
       <a href="/osm/admin/v1/tokens">TOKENs </a>
+      <a href="/osm/admin/v1/vims">VIMs </a>
       <a href="/osm/admin/v1/tokens?METHOD=DELETE">logout </a>
     </div>
   </div>
@@ -88,6 +89,15 @@ html_upload_body = """
 </form>
 """
 
+html_nslcmop_body = """
+<a href="/osm/nslcm/v1/ns_lcm_op_occs?nsInstanceId={id}">nslcm operations </a>
+<form action="/osm/nslcm/v1/ns_instances/{id}/terminate" method="post" enctype="multipart/form-data">
+    <h3> <table style="border: 0;"> <tr>
+        <td> <input type="submit" value="Terminate"/> </td>
+    </tr> </table> </h3>
+</form>
+"""
+
 
 def format(data, request, response, session):
     """
@@ -123,6 +133,10 @@ def format(data, request, response, session):
             body += '<a href="{}"> show </a>'.format(response.headers["Location"])
         else:
             body += '<a href="/osm/{}?METHOD=DELETE"> <img src="/osm/static/delete.png" height="25" width="25"> </a>'.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 += "<pre>" + yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False) + "</pre>"
     elif data is None:
         if request.method == "DELETE" or "METHOD=DELETE" in request.query_string:
index 0520ef8..aa8105a 100644 (file)
@@ -57,7 +57,11 @@ URL: /osm                                                       GET     POST
                 /<nsInstanceId>                                 O                       O     
             /ns_instances                                       5       5
                 /<nsInstanceId>                                 5                       5     
-                    TO BE COMPLETED                             
+                    instantiate                                         O5
+                    terminate                                           O5
+                    action                                              O
+                    scale                                               O5
+                    heal                                                5
             /ns_lcm_op_occs                                     5       5
                 /<nsLcmOpOccId>                                 5                       5       5
                     TO BE COMPLETED                             5               5
@@ -196,8 +200,16 @@ class Server(object):
                     "ns_instances_content": {"METHODS": ("GET", "POST"),
                         "<ID>": {"METHODS": ("GET", "DELETE")}
                     },
-                    "ns_instances": {"TODO": ("GET", "POST"),
-                        "<ID>": {"TODO": ("GET", "DELETE")}
+                    "ns_instances": {"METHODS": ("GET", "POST"),
+                        "<ID>": {"TODO": ("GET", "DELETE"),
+                             "scale": {"TODO": "POST"},
+                             "terminate": {"METHODS": "POST"},
+                             "instantiate": {"METHODS": "POST"},
+                             "action": {"METHODS": "POST"},
+                        }
+                    },
+                    "ns_lcm_op_occs": {"METHODS": "GET",
+                        "<ID>": {"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:
index 2419c1a..5a2fbdc 100644 (file)
@@ -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
     """