From 0952a48159c11b6d31fff6617f04f06351ed79f3 Mon Sep 17 00:00:00 2001 From: "preethika.p" Date: Fri, 20 Sep 2019 16:37:50 +0530 Subject: [PATCH] Feature7912- Enhancement of NBI API to collect VNF metrics Addressed the comments Unittest added Made changes in tox file Change-Id: Iad1e625f609272556bcc51b36c96a336d34b5c86 Signed-off-by: preethika.p --- osm_nbi/engine.py | 3 +- osm_nbi/pmjobs_topics.py | 46 +++++++-- osm_nbi/tests/pmjob_mocks/response.py | 139 ++++++++++++++++++++++++++ osm_nbi/tests/test_db_descriptors.py | 29 ++++++ osm_nbi/tests/test_pmjobs_topic.py | 105 +++++++++++++++++++ requirements.txt | 2 +- test-requirements.txt | 2 + tox.ini | 3 +- 8 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 osm_nbi/tests/pmjob_mocks/response.py create mode 100644 osm_nbi/tests/test_pmjobs_topic.py diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index 0211bd6..0cd0666 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -159,7 +159,8 @@ class Engine(object): else: self.map_topic[topic] = topic_class(self.db, self.fs, self.msg, self.auth) - self.map_topic["pm_jobs"] = PmJobsTopic(config["prometheus"].get("host"), config["prometheus"].get("port")) + self.map_topic["pm_jobs"] = PmJobsTopic(self.db, config["prometheus"].get("host"), + config["prometheus"].get("port")) except (DbException, FsException, MsgException) as e: raise EngineException(str(e), http_code=e.http_code) diff --git a/osm_nbi/pmjobs_topics.py b/osm_nbi/pmjobs_topics.py index 08b0b1d..7fa2035 100644 --- a/osm_nbi/pmjobs_topics.py +++ b/osm_nbi/pmjobs_topics.py @@ -13,26 +13,48 @@ # See the License for the specific language governing permissions and # limitations under the License. - import asyncio import aiohttp +from http import HTTPStatus +from urllib.parse import quote from osm_nbi.base_topic import EngineException __author__ = "Vijay R S " class PmJobsTopic(): - def __init__(self, host=None, port=None): + def __init__(self, db, host=None, port=None): + self.db = db self.url = 'http://{}:{}'.format(host, port) - self.metric_list = ['cpu_utilization', 'average_memory_utilization', 'disk_read_ops', - 'disk_write_ops', 'disk_read_bytes', 'disk_write_bytes', 'packets_dropped', - 'packets_sent', 'packets_received'] + self.nfvi_metric_list = ['cpu_utilization', 'average_memory_utilization', 'disk_read_ops', + 'disk_write_ops', 'disk_read_bytes', 'disk_write_bytes', + 'packets_dropped', 'packets_sent', 'packets_received'] + + def _get_vnf_metric_list(self, ns_id): + metric_list = self.nfvi_metric_list.copy() + vnfr_desc = self.db.get_list("vnfrs", {"nsr-id-ref": ns_id}) + if not vnfr_desc: + raise EngineException("NS not found with id {}".format(ns_id), http_code=HTTPStatus.NOT_FOUND) + else: + for vnfr in vnfr_desc: + vnfd_desc = self.db.get_one("vnfds", {"_id": vnfr["vnfd-id"]}, fail_on_empty=True, fail_on_more=False) + if vnfd_desc.get("vdu"): + for vdu in vnfd_desc['vdu']: + # Checks for vdu metric in vdu-configuration + if 'vdu-configuration' in vdu and 'metrics' in vdu['vdu-configuration']: + metric_list.extend([quote(metric['name']) + for metric in vdu["vdu-configuration"]["metrics"]]) + # Checks for vnf metric in vnf-configutaion + if 'vnf-configuration' in vnfd_desc and 'metrics' in vnfd_desc['vnf-configuration']: + metric_list.extend([quote(metric['name']) for metric in vnfd_desc["vnf-configuration"]["metrics"]]) + metric_list = list(set(metric_list)) + return metric_list - async def _prom_metric_request(self, ns_id): + async def _prom_metric_request(self, ns_id, metrics_list): try: async with aiohttp.ClientSession() as session: data = [] - for metlist in self.metric_list: + for metlist in metrics_list: request_url = self.url+'/api/v1/query?query=osm_'+metlist+"{ns_id='"+ns_id+"'}" async with session.get(request_url) as resp: resp = await resp.json() @@ -41,12 +63,13 @@ class PmJobsTopic(): data.append(resp) return data except aiohttp.client_exceptions.ClientConnectorError as e: - raise EngineException("Connection Failure: {}".format(e)) + raise EngineException("Connection to '{}'Failure: {}".format(self.url, e)) def show(self, session, ns_id): + metrics_list = self._get_vnf_metric_list(ns_id) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - prom_metric = loop.run_until_complete(self._prom_metric_request(ns_id)) + prom_metric = loop.run_until_complete(self._prom_metric_request(ns_id, metrics_list)) metric = {} metric_temp = [] for index_list in prom_metric: @@ -58,7 +81,10 @@ class PmJobsTopic(): process_metric['performanceValue']['performanceValue']['performanceValue'] = index['value'][1] process_metric['performanceValue']['performanceValue']['vnfMemberIndex'] \ = index['metric']['vnf_member_index'] - process_metric['performanceValue']['performanceValue']['vduName'] = index['metric']['vdu_name'] + if 'vdu_name' not in index['metric']: + pass + else: + process_metric['performanceValue']['performanceValue']['vduName'] = index['metric']['vdu_name'] metric_temp.append(process_metric) metric['entries'] = metric_temp return metric diff --git a/osm_nbi/tests/pmjob_mocks/response.py b/osm_nbi/tests/pmjob_mocks/response.py new file mode 100644 index 0000000..fa674ad --- /dev/null +++ b/osm_nbi/tests/pmjob_mocks/response.py @@ -0,0 +1,139 @@ +# Copyright 2019 Preethika P(Tata Elxsi) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = "Preethika P,preethika.p@tataelxsi.co.in" + +"""Excepted results for pmjob functions and prometheus""" + + +show_res = """ +--- +entries: +- objectInstanceId: f48163a6-c807-47bc-9682-f72caef5af85 + performanceMetric: osm_users + performanceValue: + performanceValue: + performanceValue: '1' + vduName: test_metric-1-ubuntuvdu1-1 + vnfMemberIndex: '1' + timestamp: 1573552141.409 +- objectInstanceId: f48163a6-c807-47bc-9682-f72caef5af85 + performanceMetric: osm_cpu_utilization + performanceValue: + performanceValue: + performanceValue: '0.7622979249' + vduName: test_metric-1-ubuntuvdu1-1 + vnfMemberIndex: '1' + timestamp: 1573556383.439 +- objectInstanceId: f48163a6-c807-47bc-9682-f72caef5af85 + performanceMetric: osm_load + performanceValue: + performanceValue: + performanceValue: '0' + vduName: test_metric-1-ubuntuvdu1-1 + vnfMemberIndex: '1' + timestamp: 1573552060.035 +""" +prom_res = """ +--- +- - metric: + __name__: osm_users + instance: mon:8000 + job: prometheus + ns_id: f48163a6-c807-47bc-9682-f72caef5af85 + vdu_name: test_metric-1-ubuntuvdu1-1 + vnf_member_index: '1' + value: + - 1573552141.409 + - '1' +- - metric: + __name__: osm_load + instance: mon:8000 + job: prometheus + ns_id: f48163a6-c807-47bc-9682-f72caef5af85 + vdu_name: test_metric-1-ubuntuvdu1-1 + vnf_member_index: '1' + value: + - 1573552060.035 + - '0' +- - metric: + __name__: osm_cpu_utilization + instance: mon:8000 + job: prometheus + ns_id: f48163a6-c807-47bc-9682-f72caef5af85 + vdu_name: test_metric-1-ubuntuvdu1-1 + vnf_member_index: '1' + value: + - 1573556383.439 + - '0.7622979249' +""" +cpu_utilization = """ +--- +status: success +data: + resultType: vector + result: + - metric: + __name__: osm_cpu_utilization + instance: mon:8000 + job: prometheus + ns_id: f48163a6-c807-47bc-9682-f72caef5af85 + vdu_name: test_metric-1-ubuntuvdu1-1 + vnf_member_index: '1' + value: + - 1573556383.439 + - '0.7622979249' +""" +users = """ +--- +status: success +data: + resultType: vector + result: + - metric: + __name__: osm_users + instance: mon:8000 + job: prometheus + ns_id: f48163a6-c807-47bc-9682-f72caef5af85 + vdu_name: test_metric-1-ubuntuvdu1-1 + vnf_member_index: '1' + value: + - 1573552141.409 + - '1' +""" +load = """ +--- +status: success +data: + resultType: vector + result: + - metric: + __name__: osm_load + instance: mon:8000 + job: prometheus + ns_id: f48163a6-c807-47bc-9682-f72caef5af85 + vdu_name: test_metric-1-ubuntuvdu1-1 + vnf_member_index: '1' + value: + - 1573552060.035 + - '0' +""" +empty = """ +--- +status: success +data: + resultType: vector + result: [] +""" diff --git a/osm_nbi/tests/test_db_descriptors.py b/osm_nbi/tests/test_db_descriptors.py index d445d09..057230c 100644 --- a/osm_nbi/tests/test_db_descriptors.py +++ b/osm_nbi/tests/test_db_descriptors.py @@ -105,6 +105,18 @@ db_vnfds_text = """ vdu-monitoring-param: vdu-monitoring-param-ref: dataVM_cpu_util vdu-ref: dataVM + - id: dataVM_users + aggregation-type: AVERAGE + name: dataVM_users + vdu-metric: + vdu-metric-name-ref: users + vdu-ref: dataVM + - id: dataVM_load + aggregation-type: AVERAGE + name: dataVM_load + vdu-metric: + vdu-metric-name-ref: load + vdu-ref: dataVM name: hackfest3charmed-vnf scaling-group-descriptor: - max-instance-count: 10 @@ -183,6 +195,23 @@ db_vnfds_text = """ - id: dataVM_cpu_util nfvi-metric: cpu_utilization name: dataVM + vdu-configuration: + initial-config-primitive: + - parameter: + - value: "" + name: ssh-hostname + - value: ubuntu + name: ssh-username + - value: osm2018 + name: ssh-password + name: config + seq: '1' + metrics: + - name: users + - name: load + juju: + proxy: true + charm: testmetrics vm-flavor: memory-mb: '1024' storage-gb: '10' diff --git a/osm_nbi/tests/test_pmjobs_topic.py b/osm_nbi/tests/test_pmjobs_topic.py new file mode 100644 index 0000000..5bcb383 --- /dev/null +++ b/osm_nbi/tests/test_pmjobs_topic.py @@ -0,0 +1,105 @@ +# Copyright 2019 Preethika P(Tata Elxsi) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = "Preethika P,preethika.p@tataelxsi.co.in" + +import asynctest +import yaml +import re +from aioresponses import aioresponses +from http import HTTPStatus +from osm_nbi.engine import EngineException +from osm_common.dbmemory import DbMemory +from osm_nbi.pmjobs_topics import PmJobsTopic +from osm_nbi.tests.test_db_descriptors import db_nsds_text, db_vnfds_text, db_nsrs_text, db_vnfrs_text +from osm_nbi.tests.pmjob_mocks.response import show_res, prom_res, cpu_utilization, users, load, empty + + +class PmJobsTopicTest(asynctest.TestCase): + + def setUp(self): + self.db = DbMemory() + self.pmjobs_topic = PmJobsTopic(self.db, host="prometheus", port=9091) + self.db.create_list("nsds", yaml.load(db_nsds_text, Loader=yaml.Loader)) + self.db.create_list("vnfds", yaml.load(db_vnfds_text, Loader=yaml.Loader)) + self.db.create_list("vnfrs", yaml.load(db_vnfrs_text, Loader=yaml.Loader)) + self.db.create_list("nsrs", yaml.load(db_nsrs_text, Loader=yaml.Loader)) + self.nsr = self.db.get_list("nsrs")[0] + self.nsr_id = self.nsr["_id"] + project_id = self.nsr["_admin"]["projects_write"] + """metric_check_list contains the vnf metric name used in descriptor i.e users,load""" + self.metric_check_list = ['cpu_utilization', 'average_memory_utilization', 'disk_read_ops', + 'disk_write_ops', 'disk_read_bytes', 'disk_write_bytes', + 'packets_dropped', 'packets_sent', 'packets_received', 'users', 'load'] + self.session = {"username": "admin", "project_id": project_id, "method": None, + "admin": True, "force": False, "public": False, "allow_show_user_project_role": True} + + def set_get_mock_res(self, mock_res, ns_id, metric_list): + site = "http://prometheus:9091/api/v1/query?query=osm_metric_name{ns_id='nsr'}" + site = re.sub(r'nsr', ns_id, site) + for metric in metric_list: + endpoint = re.sub(r'metric_name', metric, site) + if metric == 'cpu_utilization': + response = yaml.load(cpu_utilization, Loader=yaml.Loader) + elif metric == 'users': + response = yaml.load(users, Loader=yaml.Loader) + elif metric == 'load': + response = yaml.load(load, Loader=yaml.Loader) + else: + response = yaml.load(empty, Loader=yaml.Loader) + mock_res.get(endpoint, payload=response) + + def test_get_vnf_metric_list(self): + with self.subTest("Test case1 failed in test_get_vnf_metric_list"): + metric_list = self.pmjobs_topic._get_vnf_metric_list(self.nsr_id) + self.assertCountEqual(metric_list, self.metric_check_list, + "VNF metric list is not correctly fetched") + with self.subTest("Test case2 failed in test_get_vnf_metric_list"): + wrong_ns_id = "88d90b0c-faff-4bbc-cccc-aaaaaaaaaaaa" + with self.assertRaises(EngineException, msg="ns not found") as e: + self.pmjobs_topic._get_vnf_metric_list(wrong_ns_id) + self.assertEqual(e.exception.http_code, HTTPStatus.NOT_FOUND, "Wrong HTTP status code") + self.assertIn("NS not found with id {}".format(wrong_ns_id), + str(e.exception), "Wrong exception text") + + async def test_prom_metric_request(self): + with self.subTest("Test case1 failed in test_prom"): + prom_response = yaml.load(prom_res, Loader=yaml.Loader) + with aioresponses() as mock_res: + self.set_get_mock_res(mock_res, self.nsr_id, self.metric_check_list) + result = await self.pmjobs_topic._prom_metric_request(self.nsr_id, self.metric_check_list) + self.assertCountEqual(result, prom_response, "Metric Data is valid") + with self.subTest("Test case2 failed in test_prom"): + with self.assertRaises(EngineException, msg="Prometheus not reachable") as e: + await self.pmjobs_topic._prom_metric_request(self.nsr_id, self.metric_check_list) + self.assertIn("Connection to ", str(e.exception), "Wrong exception text") + + def test_show(self): + with self.subTest("Test case1 failed in test_show"): + show_response = yaml.load(show_res, Loader=yaml.Loader) + with aioresponses() as mock_res: + self.set_get_mock_res(mock_res, self.nsr_id, self.metric_check_list) + result = self.pmjobs_topic.show(self.session, self.nsr_id) + self.assertEqual(len(result['entries']), 3, "Number of metrics returned") + self.assertCountEqual(result, show_response, "Response is valid") + with self.subTest("Test case2 failed in test_show"): + wrong_ns_id = "88d90b0c-faff-4bbc-cccc-aaaaaaaaaaaa" + with aioresponses() as mock_res: + self.set_get_mock_res(mock_res, wrong_ns_id, self.metric_check_list) + with self.assertRaises(EngineException, msg="ns not found") as e: + self.pmjobs_topic.show(self.session, wrong_ns_id) + self.assertEqual(e.exception.http_code, HTTPStatus.NOT_FOUND, "Wrong HTTP status code") + self.assertIn("NS not found with id {}".format(wrong_ns_id), str(e.exception), + "Wrong exception text") diff --git a/requirements.txt b/requirements.txt index 6928de1..41bc5f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,5 +17,5 @@ python-keystoneclient requests git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im -aiohttp==0.20.2 +aiohttp==2.3.10 diff --git a/test-requirements.txt b/test-requirements.txt index 8f9f3b4..560046d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,4 +15,6 @@ # mock # pyangbind pyang +aioresponses +asynctest diff --git a/tox.ini b/tox.ini index ef4af53..d56ee5c 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,6 @@ toxworkdir={toxinidir}/.tox usedevelop = True basepython = python3 install_command = python3 -m pip install -r requirements.txt -U {opts} {packages} -deps = -r{toxinidir}/test-requirements.txt [testenv:flake8] basepython = python3 @@ -37,5 +36,7 @@ commands = python3 setup.py --command-packages=stdeb.command bdist_deb [testenv:unittest] basepython = python3 +deps = asynctest + aioresponses commands = python3 -m unittest discover {toxinidir}/osm_nbi/tests -v -- 2.17.1