From b57758d4a3fd88baa348cfa0812e9f4a742761d1 Mon Sep 17 00:00:00 2001 From: Felipe Vicens Date: Tue, 16 Oct 2018 16:00:20 +0200 Subject: [PATCH 1/1] Adding slice templates to NBI Change-Id: I3a99a71a1252b8796b5988fcade9dca849476ce4 Signed-off-by: Felipe Vicens --- osm_nbi/descriptor_topics.py | 77 ++++++++++ osm_nbi/engine.py | 6 +- osm_nbi/html_out.py | 3 + osm_nbi/instance_topics.py | 149 +++++++++++++++++++ osm_nbi/nbi.py | 7 +- osm_nbi/tests/cirros_slice/cirros_slice.yaml | 21 +++ osm_nbi/tests/clear-all.sh | 2 +- osm_nbi/tests/test.py | 28 ++++ 8 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 osm_nbi/tests/cirros_slice/cirros_slice.yaml diff --git a/osm_nbi/descriptor_topics.py b/osm_nbi/descriptor_topics.py index 503f82a..9e367c9 100644 --- a/osm_nbi/descriptor_topics.py +++ b/osm_nbi/descriptor_topics.py @@ -634,6 +634,83 @@ class NsdTopic(DescriptorTopic): raise EngineException("There is some NSR that depends on this NSD", http_code=HTTPStatus.CONFLICT) +class NstTopic(DescriptorTopic): + topic = "nsts" + topic_msg = "nst" + + def __init__(self, db, fs, msg): + DescriptorTopic.__init__(self, db, fs, msg) + + @staticmethod + def _remove_envelop(indata=None): + if not indata: + return {} + clean_indata = indata + + if clean_indata.get('nst:nst'): + clean_indata = clean_indata['nst:nst'] + elif clean_indata.get('nst'): + clean_indata = clean_indata['nst'] + if clean_indata.get('nst'): + if not isinstance(clean_indata['nst'], list) or len(clean_indata['nst']) != 1: + raise EngineException("'nst' must be a list only one element") + clean_indata = clean_indata['nst'][0] + return clean_indata + + def _validate_input_new(self, indata, force=False): + # transform netslice-subnet:nsd-ref to string + if indata.get("netslice-subnet"): + for nsd_ref in indata["netslice-subnet"]: + if "nsd-ref" in nsd_ref: + nsd_ref["nsd-ref"] = str(nsd_ref["nsd-ref"]) + + # TODO validate with pyangbind, serialize + return indata + + def _validate_input_edit(self, indata, force=False): + # TODO validate with pyangbind, serialize + return indata + + def _check_descriptor_dependencies(self, session, descriptor): + """ + Check that the dependent descriptors exist on a new descriptor or edition + :param session: client session information + :param descriptor: descriptor to be inserted or edit + :return: None or raises exception + """ + if not descriptor.get("netslice-subnet"): + return + for nsd in descriptor["netslice-subnet"]: + nsd_id = nsd["nsd-ref"] + filter_q = self._get_project_filter(session, write=False, show_all=True) + filter_q["id"] = nsd_id + if not self.db.get_list("nsds", filter_q): + raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non " + "existing nsd".format(nsd_id), http_code=HTTPStatus.CONFLICT) + + def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False): + super().check_conflict_on_edit(session, final_content, edit_content, _id, force=force) + + self._check_descriptor_dependencies(session, final_content) + + def check_conflict_on_del(self, session, _id, force=False): + """ + Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note + that NST can be public and be used by other projects. + :param session: + :param _id: nsd internal id + :param force: Avoid this checking + :return: None or raises EngineException with the conflict + """ + # TODO: Check this method + if force: + return + _filter = self._get_project_filter(session, write=False, show_all=False) + _filter["nst"] = _id + if self.db.get_list("nsis", _filter): + raise EngineException("There is some NSIS that depends on this NST", http_code=HTTPStatus.CONFLICT) + + class PduTopic(BaseTopic): topic = "pdus" topic_msg = "pdu" diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index 3cdacd9..3de3d15 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -8,8 +8,8 @@ from osm_common.msgbase import MsgException from http import HTTPStatus from base_topic import EngineException, versiontuple from admin_topics import UserTopic, ProjectTopic, VimAccountTopic, SdnTopic -from descriptor_topics import VnfdTopic, NsdTopic, PduTopic -from instance_topics import NsrTopic, VnfrTopic, NsLcmOpTopic +from descriptor_topics import VnfdTopic, NsdTopic, PduTopic, NstTopic +from instance_topics import NsrTopic, VnfrTopic, NsLcmOpTopic, NsiTopic from base64 import b64encode from os import urandom @@ -21,6 +21,7 @@ class Engine(object): map_from_topic_to_class = { "vnfds": VnfdTopic, "nsds": NsdTopic, + "nsts": NstTopic, "pdus": PduTopic, "nsrs": NsrTopic, "vnfrs": VnfrTopic, @@ -29,6 +30,7 @@ class Engine(object): "sdns": SdnTopic, "users": UserTopic, "projects": ProjectTopic, + "nsis": NsiTopic, # [NEW_TOPIC]: add an entry here } diff --git a/osm_nbi/html_out.py b/osm_nbi/html_out.py index 2fe4b8f..e9237e9 100644 --- a/osm_nbi/html_out.py +++ b/osm_nbi/html_out.py @@ -24,6 +24,7 @@ html_start = """ VNFDs NSDs NSs + NSTDs USERs PROJECTs TOKENs @@ -125,6 +126,8 @@ def format(data, request, response, session): body += html_upload_body.format(request.path_info, "VNFD") elif request.path_info == "/nsd/v1/ns_descriptors": body += html_upload_body.format(request.path_info + "_content", "NSD") + elif request.path_info == "/nst/v1/nst_templates": + body += html_upload_body.format(request.path_info + "_content", "NSTD") for k in data: if isinstance(k, dict): data_id = k.pop("_id", None) diff --git a/osm_nbi/instance_topics.py b/osm_nbi/instance_topics.py index 29a391c..4228bc5 100644 --- a/osm_nbi/instance_topics.py +++ b/osm_nbi/instance_topics.py @@ -592,3 +592,152 @@ class NsLcmOpTopic(BaseTopic): def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) + + +class NsiTopic(BaseTopic): + topic = "nsis" + topic_msg = "nsi" + + def __init__(self, db, fs, msg): + BaseTopic.__init__(self, db, fs, msg) + + def _check_descriptor_dependencies(self, session, descriptor): + """ + Check that the dependent descriptors exist on a new descriptor or edition + :param session: client session information + :param descriptor: descriptor to be inserted or edit + :return: None or raises exception + """ + if not descriptor.get("nstdId"): + return + nstd_id = descriptor["nstdId"] + if not self.get_item_list(session, "nsts", {"id": nstd_id}): + raise EngineException("Descriptor error at nstdId='{}' references a non exist nstd".format(nstd_id), + http_code=HTTPStatus.CONFLICT) + + @staticmethod + def format_on_new(content, project_id=None, make_public=False): + BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public) + content["_admin"]["nstState"] = "NOT_INSTANTIATED" + + def check_conflict_on_del(self, session, _id, force=False): + if force: + return + nsi = self.db.get_one("nsis", {"_id": _id}) + if nsi["_admin"].get("nsiState") == "INSTANTIATED": + raise EngineException("nsi '{}' cannot be deleted because it is in 'INSTANTIATED' state. " + "Launch 'terminate' operation first; or force deletion".format(_id), + http_code=HTTPStatus.CONFLICT) + + def delete(self, session, _id, force=False, dry_run=False): + """ + Delete item by its internal _id + :param session: contains the used login username, working project, and admin rights + :param _id: server internal id + :param force: indicates if deletion must be forced in case of conflict + :param dry_run: make checking but do not delete + :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... + """ + # TODO add admin to filter, validate rights + BaseTopic.delete(self, session, _id, force, dry_run=True) + if dry_run: + return + v = self.db.del_one("nsis", {"_id": _id}) + self.db.del_list("nstlcmops", {"nstInstanceId": _id}) + self.db.del_list("vnfrs", {"nsr-id-ref": _id}) + # set all used pdus as free + self.db.set_list("pdus", {"_admin.usage.nsr_id": _id}, {"_admin.usageSate": "NOT_IN_USE", "_admin.usage": None}) + self._send_msg("deleted", {"_id": _id}) + return v + + def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False): + """ + Creates a new nsr into database. It also creates needed vnfrs + :param rollback: list to append the created items at database in case a rollback must be done + :param session: contains the used login username and working project + :param indata: params to be used for the nsir + :param kwargs: used to override the indata descriptor + :param headers: http request headers + :param force: If True avoid some dependence checks + :param make_public: Make the created item public to all projects + :return: the _id of nsi descriptor created at database + """ + + try: + slice_request = self._remove_envelop(indata) + # Override descriptor with query string kwargs + self._update_input_with_kwargs(slice_request, kwargs) + self._validate_input_new(slice_request, force) + + step = "" + # look for nstd + step = "getting nstd id='{}' from database".format(slice_request.get("nstdId")) + _filter = {"_id": slice_request["nstdId"]} + _filter.update(BaseTopic._get_project_filter(session, write=False, show_all=True)) + nstd = self.db.get_one("nsts", _filter) + nsi_id = str(uuid4()) + # now = time() + step = "filling nsi_descriptor with input data" + nsi_descriptor = { + "id": nsi_id, + "nst-ref": nstd["id"], + "instantiation-parameters": { + "netslice-subnet": [] + }, + "network-slice-template": nstd, + # "nsr-ref-list": [], #TODO: not used for now... + # "vlr-ref-list": [], #TODO: not used for now... + "_id": nsi_id, + + # TODO CHECK: what about the following params? + # "admin-status": "ENABLED", + # "description": slice_request.get("nsDescription", ""), + # "operational-status": "init", # typedef ns-operational- + # "config-status": "init", # typedef config-states + # "detailed-status": "scheduled", + # "orchestration-progress": {}, + # # {"networks": {"active": 0, "total": 0}, "vms": {"active": 0, "total": 0}}, + # "create-time": now, + # "operational-events": [], # "id", "timestamp", "description", "event", + # "ssh-authorized-key": slice_request.get("key-pair-ref"), + } + # nstd["nsi_id"] = nsi_id + + # TODO: ask if we have to develop the VNFR here or we can imply call the NsrTopic() for each service to + # instantiate. + # Create netslice-subnet_record + needed_nsds = {} + for member_ns in nstd["netslice-subnet"]: + nsd_id = member_ns["nsd-ref"] + step = "getting nstd id='{}' constituent-nsd='{}' from database".format( + member_ns["nsd-ref"], member_ns["id"]) + if nsd_id not in needed_nsds: + # Obtain nsd + nsd = DescriptorTopic.get_one_by_id(self.db, session, "nsds", nsd_id) + nsd.pop("_admin") + needed_nsds[nsd_id] = nsd + else: + nsd = needed_nsds[nsd_id] + + step = "filling nsir nsd-id='{}' constituent-nsd='{}' from database".format( + member_ns["nsd-ref"], member_ns["id"]) + netslice_subnet_descriptor = { + "nsName": member_ns["instantiation-parameters"]["name"], + "nsdId": member_ns["instantiation-parameters"]["nsdId"], + "vimAccountId": member_ns["instantiation-parameters"]["vimAccountId"] + } + nsi_descriptor["instantiation-parameters"]["netslice-subnet"].append(netslice_subnet_descriptor) + + step = "creating nsi at database" + self.format_on_new(nsi_descriptor, session["project_id"], make_public=make_public) + self.db.create("nsis", nsi_descriptor) + rollback.append({"topic": "nsis", "_id": nsi_id}) + return nsi_id + except Exception as e: + self.logger.exception("Exception {} at NsiTopic.new()".format(e), exc_info=True) + raise EngineException("Error {}: {}".format(step, e)) + except ValidationError as e: + raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) + + def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None): + raise EngineException("Method edit called directly", HTTPStatus.INTERNAL_SERVER_ERROR) diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index aeff32c..e8f2246 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -661,7 +661,7 @@ class Server(object): if not main_topic or not version or not topic: raise NbiException("URL must contain at least 'main_topic/version/topic'", HTTPStatus.METHOD_NOT_ALLOWED) - if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"): + if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "nst", "nsilcm"): raise NbiException("URL main_topic '{}' not supported".format(main_topic), HTTPStatus.METHOD_NOT_ALLOWED) if version != 'v1': @@ -705,14 +705,15 @@ class Server(object): elif main_topic == "nsilcm": engine_topic = "nsis" if topic == "nsi_lcm_op_occs": - engine_topic = "nsilcmops" + engine_topic = "nsilcmops" elif main_topic == "pdu": engine_topic = "pdus" if engine_topic == "vims": # TODO this is for backward compatibility, it will remove in the future engine_topic = "vim_accounts" if method == "GET": - if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"): + if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content", + "netslice_instances"): if item in ("vnfd", "nsd", "nst"): path = "$DESCRIPTOR" elif args: diff --git a/osm_nbi/tests/cirros_slice/cirros_slice.yaml b/osm_nbi/tests/cirros_slice/cirros_slice.yaml new file mode 100644 index 0000000..3b5c963 --- /dev/null +++ b/osm_nbi/tests/cirros_slice/cirros_slice.yaml @@ -0,0 +1,21 @@ +nst: + nst: + - id: cirros_nst + name: cirros_slice_template + SNSSAI-identifier: + - slice-service-type: eMBB + quality-of-service: + - id: 1 + netslice-subnet: + - id: cirros_nsd_nst + name: cirros_ns + short-name: cirros_ns + vendor: OSM + is-shared-nss: 'false' + description: Slice for osm-3.0-three 2nd-hackfest nsd2vnfs package + version: '1.0' + nsd-ref: cirros_2vnf_nsd + instantiation-parameters: + - name: cirros_2vnf_nsd + nsdId: cirros_2vnf_nsd + vimAccountId: openstack_18 \ No newline at end of file diff --git a/osm_nbi/tests/clear-all.sh b/osm_nbi/tests/clear-all.sh index e53df12..5ba9d4b 100755 --- a/osm_nbi/tests/clear-all.sh +++ b/osm_nbi/tests/clear-all.sh @@ -66,7 +66,7 @@ then done fi -for item in vim_accounts sdns nsrs vnfrs nslcmops nsds vnfds projects # vims +for item in vim_accounts sdns nsrs vnfrs nslcmops nsds vnfds projects nsts # vims do curl --insecure ${OSMNBI_URL}/test/db-clear/${item} echo " ${item}" diff --git a/osm_nbi/tests/test.py b/osm_nbi/tests/test.py index 0c08c22..bcdfadf 100755 --- a/osm_nbi/tests/test.py +++ b/osm_nbi/tests/test.py @@ -1334,6 +1334,33 @@ class TestDescriptors: self.step += 1 +class TestNstTemplates: + description = "Upload a NST to OSM" + + def __init__(self): + self.nst_filenames = ("@./cirros_slice/cirros_slice.yaml") + + def run(self, engine, test_osm, manual_check, test_params=None): + # nst CREATE + engine.get_autorization() + r = engine.test("NST", "Onboard NST", "POST", "/nst/v1/netslice_templates_content", headers_yaml, + self.nst_filenames, + 201, {"Location": "/nst/v1/netslice_templates_content", "Content-Type": "application/yaml"}, + "yaml") + location = r.headers["Location"] + nst_id = location[location.rfind("/")+1:] + + # nstd SHOW OSM format + r = engine.test("NST", "Show NSTD OSM format", "GET", + "/nst/v1/netslice_templates_content/{}".format(nst_id), headers_json, None, + 200, r_header_json, "json") + + # nstd DELETE + r = engine.test("NST", "Delete NSTD", "DELETE", + "/nst/v1/netslice_templates_content/{}".format(nst_id), headers_json, None, + 204, None, 0) + + if __name__ == "__main__": global logger test = "" @@ -1368,6 +1395,7 @@ if __name__ == "__main__": "TestDescriptors": TestDescriptors, "TestDeployHackfest1": TestDeployHackfest1, # "Deploy-MultiVIM": TestDeployMultiVIM, + "Upload-Slice-Template": TestNstTemplates, } test_to_do = [] test_params = {} -- 2.17.1