From 5758955b7b394517ff5caf5506a4400cdc5aa372 Mon Sep 17 00:00:00 2001 From: K Sai Kiran Date: Wed, 27 Jan 2021 21:38:34 +0530 Subject: [PATCH] Feature-9904: Enhancing NG-UI to enable Juju operational view dashboard Added query parameter to NSR api and to send a message to kafka when conditions are met to trigger vca status refresh. Included unit tests for functions implemented Change-Id: Ibf691082cde15d8ee42f1a4ebaebedd99d674e8c Signed-off-by: ksaikiranr --- osm_nbi/admin_topics.py | 9 +++-- osm_nbi/base_topic.py | 3 +- osm_nbi/engine.py | 13 +++---- osm_nbi/instance_topics.py | 35 +++++++++++++++++ osm_nbi/nbi.py | 7 ++-- osm_nbi/pmjobs_topics.py | 2 +- osm_nbi/tests/test_instance_topics.py | 56 ++++++++++++++++++++++++--- 7 files changed, 103 insertions(+), 22 deletions(-) diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index 0006917..786d237 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -922,12 +922,13 @@ class UserTopicAuth(UserTopic): except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def show(self, session, _id, api_req=False): + def show(self, session, _id, filter_q=None, api_req=False): """ Get complete information on an topic :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id or username + :param filter_q: dict: query parameter :param api_req: True if this call is serving an external API request. False if serving internal request. :return: dictionary, raise exception if not found. """ @@ -1295,12 +1296,13 @@ class ProjectTopicAuth(ProjectTopic): except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) - def show(self, session, _id, api_req=False): + def show(self, session, _id, filter_q=None, api_req=False): """ Get complete information on an topic :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id + :param filter_q: dict: query parameter :param api_req: True if this call is serving an external API request. False if serving internal request. :return: dictionary, raise exception if not found. """ @@ -1594,12 +1596,13 @@ class RoleTopicAuth(BaseTopic): final_content["permissions"]["admin"] = False return None - def show(self, session, _id, api_req=False): + def show(self, session, _id, filter_q=None, api_req=False): """ Get complete information on an topic :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id + :param filter_q: dict: query parameter :param api_req: True if this call is serving an external API request. False if serving internal request. :return: dictionary, raise exception if not found. """ diff --git a/osm_nbi/base_topic.py b/osm_nbi/base_topic.py index 722ff59..8c67c2d 100644 --- a/osm_nbi/base_topic.py +++ b/osm_nbi/base_topic.py @@ -411,11 +411,12 @@ class BaseTopic: # Projection was moved to child classes return data - def show(self, session, _id, api_req=False): + def show(self, session, _id, filter_q=None, api_req=False): """ Get complete information on an topic :param session: contains "username", "admin", "force", "public", "project_id", "set_project" :param _id: server internal id + :param filter_q: dict: query parameter :param api_req: True if this call is serving an external API request. False if serving internal request. :return: dictionary, raise exception if not found. """ diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index 1bc9171..cb27414 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -288,25 +288,22 @@ class Engine(object): :return: The list, it can be empty if no one match the filter_q. """ if topic not in self.map_topic: - raise EngineException( - "Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR - ) + raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) return self.map_topic[topic].list(session, filter_q, api_req) - def get_item(self, session, topic, _id, api_req=False): + def get_item(self, session, topic, _id, filter_q=None, api_req=False): """ Get complete information on an item :param session: contains the used login username and working project :param topic: it can be: users, projects, vnfds, nsds, :param _id: server id of the item + :param filter_q: other arguments :param api_req: True if this call is serving an external API request. False if serving internal request. :return: dictionary, raise exception if not found. """ if topic not in self.map_topic: - raise EngineException( - "Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR - ) - return self.map_topic[topic].show(session, _id, api_req) + raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR) + return self.map_topic[topic].show(session, _id, filter_q, api_req) def get_file(self, session, topic, _id, path=None, accept_header=None): """ diff --git a/osm_nbi/instance_topics.py b/osm_nbi/instance_topics.py index 87b186e..1486133 100644 --- a/osm_nbi/instance_topics.py +++ b/osm_nbi/instance_topics.py @@ -945,6 +945,41 @@ class NsrTopic(BaseTopic): return vnfr_descriptor + def vca_status_refresh(self, session, ns_instance_content, filter_q): + """ + vcaStatus in ns_instance_content maybe stale, check if it is stale and create lcm op + to refresh vca status by sending message to LCM when it is stale. Ignore otherwise. + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param ns_instance_content: ns instance content + :param filter_q: dict: query parameter containing vcaStatus-refresh as true or false + :return: None + """ + time_now, time_delta = time(), time() - ns_instance_content["_admin"]["modified"] + force_refresh = isinstance(filter_q, dict) and filter_q.get('vcaStatusRefresh') == 'true' + threshold_reached = time_delta > 120 + if force_refresh or threshold_reached: + operation, _id = "vca_status_refresh", ns_instance_content["_id"] + ns_instance_content["_admin"]["modified"] = time_now + self.db.set_one(self.topic, {"_id": _id}, ns_instance_content) + nslcmop_desc = NsLcmOpTopic._create_nslcmop(_id, operation, None) + self.format_on_new(nslcmop_desc, session["project_id"], make_public=session["public"]) + nslcmop_desc["_admin"].pop("nsState") + self.msg.write("ns", operation, nslcmop_desc) + return + + def show(self, session, _id, filter_q=None, api_req=False): + """ + Get complete information on an ns instance. + :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + :param _id: string, ns instance id + :param filter_q: dict: query parameter containing vcaStatusRefresh as true or false + :param api_req: True if this call is serving an external API request. False if serving internal request. + :return: dictionary, raise exception if not found. + """ + ns_instance_content = super().show(session, _id, api_req) + self.vca_status_refresh(session, ns_instance_content, filter_q) + return ns_instance_content + def edit(self, session, _id, indata=None, kwargs=None, 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 bec0cfa..d5a81ce 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -1297,9 +1297,10 @@ class Server(object): if item == "reports": # TODO check that project_id (_id in this context) has permissions _id = args[0] - outdata = self.engine.get_item( - engine_session, engine_topic, _id, True - ) + filter_q = None + if "vcaStatusRefresh" in kwargs: + filter_q = {"vcaStatusRefresh": kwargs["vcaStatusRefresh"]} + outdata = self.engine.get_item(engine_session, engine_topic, _id, filter_q, True) elif method == "POST": cherrypy.response.status = HTTPStatus.CREATED.value diff --git a/osm_nbi/pmjobs_topics.py b/osm_nbi/pmjobs_topics.py index f8d7714..514d442 100644 --- a/osm_nbi/pmjobs_topics.py +++ b/osm_nbi/pmjobs_topics.py @@ -90,7 +90,7 @@ class PmJobsTopic: except aiohttp.client_exceptions.ClientConnectorError as e: raise EngineException("Connection to '{}'Failure: {}".format(self.url, e)) - def show(self, session, ns_id, api_req=False): + def show(self, session, ns_id, filter_q=None, api_req=False): metrics_list = self._get_vnf_metric_list(ns_id) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) diff --git a/osm_nbi/tests/test_instance_topics.py b/osm_nbi/tests/test_instance_topics.py index 5e86ae5..8efb1f9 100644 --- a/osm_nbi/tests/test_instance_topics.py +++ b/osm_nbi/tests/test_instance_topics.py @@ -16,7 +16,8 @@ ## import unittest -from unittest.mock import Mock, mock_open # patch, MagicMock +from time import time +from unittest.mock import Mock, mock_open # patch, MagicMock from osm_common.dbbase import DbException from osm_nbi.engine import EngineException from osm_common.dbmemory import DbMemory @@ -375,11 +376,54 @@ class TestNsrTopic(unittest.TestCase): self.assertTrue(e.exception.http_code == expect_code) if expect_text_list: for expect_text in expect_text_list: - self.assertIn( - expect_text, - str(e.exception).lower(), - "Expected '{}' at exception text".format(expect_text), - ) + self.assertIn(expect_text, str(e.exception).lower(), + "Expected '{}' at exception text".format(expect_text)) + + def test_show_instance(self): + session = {"force": False, "admin": False, "public": False, "project_id": [self.nsd_project], "method": "write"} + filter_q = {} + for refresh_status in ("true", "false"): + self.db.create_list("nsrs", yaml.load(db_nsrs_text, Loader=yaml.Loader)) + actual_nsr = self.db.get_list("nsrs")[0] + nsr_id = actual_nsr["_id"] + filter_q['vcaStatus-refresh'] = refresh_status + expected_nsr = self.nsr_topic.show(session, nsr_id, filter_q=filter_q) + self.nsr_topic.delete(session, nsr_id) + actual_nsr.pop("_admin") + expected_nsr.pop("_admin") + self.assertEqual(expected_nsr, actual_nsr, "Database nsr and show() nsr do not match.") + + def test_vca_status_refresh(self): + session = {"force": False, "admin": False, "public": False, "project_id": [self.nsd_project], "method": "write"} + filter_q = {'vcaStatus-refresh': 'true'} + time_delta = 120 + self.db.create_list("nsrs", yaml.load(db_nsrs_text, Loader=yaml.Loader)) + nsr = self.db.get_list("nsrs")[0] + + # When vcaStatus-refresh is true + filter_q['vcaStatus-refresh'] = "true" + self.nsr_topic.vca_status_refresh(session, nsr, filter_q) + msg_args = self.msg.write.call_args[0] + self.assertEqual(msg_args[1], "vca_status_refresh", "Wrong message action") + self.assertGreater(nsr["_admin"]["modified"], time() - time_delta) + + # When vcaStatus-refresh is false but modified time is within threshold + filter_q['vcaStatus-refresh'] = "false" + time_now = time() + nsr["_admin"]["modified"] = time_now + self.nsr_topic.vca_status_refresh(session, nsr, filter_q) + msg_args = self.msg.write.call_args[1] + self.assertEqual(msg_args, {}, "Message should not be sent.") + self.assertEqual(nsr["_admin"]["modified"], time_now, "Modified time should not be changed.") + + # When vcaStatus-refresh is false but modified time is less than threshold + filter_q['vcaStatus-refresh'] = "false" + nsr["_admin"]["modified"] = time() - (2*time_delta) + self.nsr_topic.vca_status_refresh(session, nsr, filter_q) + msg_args = self.msg.write.call_args[0] + self.assertEqual(msg_args[1], "vca_status_refresh", "Wrong message action") + self.nsr_topic.delete(session, nsr["_id"]) + self.assertGreater(nsr["_admin"]["modified"], time() - time_delta, "Modified time is not changed.") def test_delete_ns(self): self.db.create_list("nsrs", yaml.load(db_nsrs_text, Loader=yaml.Loader)) -- 2.25.1