From: Benjamin Diaz Date: Tue, 16 Apr 2019 15:41:24 +0000 (-0300) Subject: Adds support for interface specific metrics in OpenStack plugin X-Git-Tag: v6.0.0~11 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FMON.git;a=commitdiff_plain;h=6b45f219dc6bbb1dde1d3cb88eb51372ee81d359 Adds support for interface specific metrics in OpenStack plugin Change-Id: I374a2c623a783215b2bbbdf8a69e600e3cb0f55d Signed-off-by: Benjamin Diaz --- diff --git a/debian/python3-osm-mon.postinst b/debian/python3-osm-mon.postinst index fc18a7a..f41b674 100644 --- a/debian/python3-osm-mon.postinst +++ b/debian/python3-osm-mon.postinst @@ -25,8 +25,6 @@ echo "Installing python dependencies via pip..." pip3 install aiokafka==0.4.* pip3 install requests==2.18.* -pip3 install jsmin==2.2.* -pip3 install jsonschema==2.6.* pip3 install python-keystoneclient==3.15.* pip3 install six==1.11.* pip3 install peewee==3.8.* @@ -38,4 +36,5 @@ pip3 install python-ceilometerclient==2.9.* pip3 install peewee-migrate==1.1.* pip3 install python-novaclient==12.0.* pip3 install pymysql==0.9.* +pip3 install python-neutronclient==5.1.* echo "Installation of python dependencies finished" diff --git a/osm_mon/collector/vnf_collectors/openstack.py b/osm_mon/collector/vnf_collectors/openstack.py index e8ee508..c11f63f 100644 --- a/osm_mon/collector/vnf_collectors/openstack.py +++ b/osm_mon/collector/vnf_collectors/openstack.py @@ -22,14 +22,16 @@ import datetime import json import logging +from enum import Enum from typing import List import gnocchiclient.exceptions from ceilometerclient.v2 import client as ceilometer_client from gnocchiclient.v1 import client as gnocchi_client from keystoneauth1 import session -from keystoneauth1.exceptions import EndpointNotFound from keystoneauth1.identity import v3 +from keystoneclient.v3 import client as keystone_client +from neutronclient.v2_0 import client as neutron_client from osm_mon.collector.metric import Metric from osm_mon.collector.utils import CollectorUtils @@ -56,44 +58,28 @@ METRIC_MAPPINGS = { INTERFACE_METRICS = ['packets_in_dropped', 'packets_out_dropped', 'packets_received', 'packets_sent'] +class MetricType(Enum): + INSTANCE = 'instance' + INTERFACE_ALL = 'interface_all' + INTERFACE_ONE = 'interface_one' + + class OpenstackCollector(BaseVimCollector): def __init__(self, config: Config, vim_account_id: str): super().__init__(config, vim_account_id) self.conf = config self.common_db = CommonDbClient(config) self.backend = self._get_backend(vim_account_id) - self.client = self._build_client(vim_account_id) - self.granularity = self._get_granularity(vim_account_id) - def _get_resource_uuid(self, nsr_id, vnf_member_index, vdur_name) -> str: + def _build_keystone_client(self, vim_account_id: str) -> keystone_client.Client: + sess = OpenstackBackend.get_session(vim_account_id) + return keystone_client.Client(session=sess) + + def _get_resource_uuid(self, nsr_id: str, vnf_member_index: str, vdur_name: str) -> str: vdur = self.common_db.get_vdur(nsr_id, vnf_member_index, vdur_name) return vdur['vim-id'] - def _build_gnocchi_client(self, vim_account_id: str) -> gnocchi_client.Client: - creds = CollectorUtils.get_credentials(vim_account_id) - verify_ssl = CollectorUtils.is_verify_ssl(creds) - auth = v3.Password(auth_url=creds.url, - username=creds.user, - password=creds.password, - project_name=creds.tenant_name, - project_domain_id='default', - user_domain_id='default') - sess = session.Session(auth=auth, verify=verify_ssl) - return gnocchi_client.Client(session=sess) - - def _build_ceilometer_client(self, vim_account_id: str) -> ceilometer_client.Client: - creds = CollectorUtils.get_credentials(vim_account_id) - verify_ssl = CollectorUtils.is_verify_ssl(creds) - auth = v3.Password(auth_url=creds.url, - username=creds.user, - password=creds.password, - project_name=creds.tenant_name, - project_domain_id='default', - user_domain_id='default') - sess = session.Session(auth=auth, verify=verify_ssl) - return ceilometer_client.Client(session=sess) - - def _get_granularity(self, vim_account_id): + def _get_granularity(self, vim_account_id: str): creds = CollectorUtils.get_credentials(vim_account_id) vim_config = json.loads(creds.config) if 'granularity' in vim_config: @@ -116,7 +102,9 @@ class OpenstackCollector(BaseVimCollector): if 'monitoring-param' in vdu: for param in vdu['monitoring-param']: metric_name = param['nfvi-metric'] + interface_name = param['interface-name-ref'] if 'interface-name-ref' in param else None openstack_metric_name = METRIC_MAPPINGS[metric_name] + metric_type = self._get_metric_type(metric_name, interface_name) try: resource_id = self._get_resource_uuid(nsr_id, vnf_member_index, vdur['name']) except ValueError: @@ -125,74 +113,142 @@ class OpenstackCollector(BaseVimCollector): "Was it recently deleted?", vdur['name'], vnf_member_index, nsr_id) continue - if self.backend == 'ceilometer': - measures = self.client.samples.list(meter_name=openstack_metric_name, limit=1, q=[ - {'field': 'resource_id', 'op': 'eq', 'value': resource_id}]) - if measures: - metric = VnfMetric(nsr_id, vnf_member_index, vdur['name'], metric_name, - measures[0].counter_volume) - metrics.append(metric) - if self.backend == 'gnocchi': - delta = 10 * self.granularity - start_date = datetime.datetime.now() - datetime.timedelta(seconds=delta) - if metric_name in INTERFACE_METRICS: - total_measure = None - interfaces = self.client.resource.search(resource_type='instance_network_interface', - query={'=': {'instance_id': resource_id}}) - for interface in interfaces: - try: - measures = self.client.metric.get_measures(openstack_metric_name, - start=start_date, - resource_id=interface['id'], - granularity=self.granularity) - if measures: - if not total_measure: - total_measure = 0.0 - total_measure += measures[-1][2] - - except gnocchiclient.exceptions.NotFound as e: - log.debug("No metric %s found for interface %s: %s", openstack_metric_name, - interface['id'], e) - if total_measure: - metric = VnfMetric(nsr_id, vnf_member_index, vdur['name'], metric_name, - total_measure) - metrics.append(metric) - else: - try: - measures = self.client.metric.get_measures(openstack_metric_name, - start=start_date, - resource_id=resource_id, - granularity=self.granularity) - if measures: - metric = VnfMetric(nsr_id, vnf_member_index, vdur['name'], metric_name, - measures[-1][2]) - metrics.append(metric) - except gnocchiclient.exceptions.NotFound as e: - log.debug("No metric %s found for instance %s: %s", openstack_metric_name, resource_id, - e) - - else: - raise Exception('Unknown client class: %s', self.client) + value = self.backend.collect_metric(metric_type, openstack_metric_name, resource_id, interface_name) + if value is not None: + metric = VnfMetric(nsr_id, vnf_member_index, vdur['name'], metric_name, value) + metrics.append(metric) return metrics - def _build_client(self, vim_account_id): - if self.backend == 'ceilometer': - return self._build_ceilometer_client(vim_account_id) - elif self.backend == 'gnocchi': - return self._build_gnocchi_client(vim_account_id) + def _get_backend(self, vim_account_id: str): + keystone = self._build_keystone_client(vim_account_id) + if keystone.services.list('gnocchi'): + granularity = self._get_granularity(vim_account_id) + return GnocchiBackend(vim_account_id, granularity) + elif keystone.services.list('ceilometer'): + return CeilometerBackend(vim_account_id) + + def _get_metric_type(self, metric_name: str, interface_name: str) -> MetricType: + if metric_name not in INTERFACE_METRICS: + return MetricType.INSTANCE else: - raise Exception('Unknown metric backend: %s', self.backend) + if interface_name: + return MetricType.INTERFACE_ONE + return MetricType.INTERFACE_ALL - def _get_backend(self, vim_account_id): - try: - gnocchi = self._build_gnocchi_client(vim_account_id) - gnocchi.resource.list(limit=1) - return 'gnocchi' - except EndpointNotFound: + +class OpenstackBackend: + def collect_metric(self, metric_type: MetricType, metric_name: str, resource_id: str, interface_name: str): + pass + + @staticmethod + def get_session(vim_account_id: str): + creds = CollectorUtils.get_credentials(vim_account_id) + verify_ssl = CollectorUtils.is_verify_ssl(creds) + auth = v3.Password(auth_url=creds.url, + username=creds.user, + password=creds.password, + project_name=creds.tenant_name, + project_domain_id='default', + user_domain_id='default') + return session.Session(auth=auth, verify=verify_ssl) + + +class GnocchiBackend(OpenstackBackend): + + def __init__(self, vim_account_id: str, granularity: int): + self.client = self._build_gnocchi_client(vim_account_id) + self.neutron = self._build_neutron_client(vim_account_id) + self.granularity = granularity + + def _build_gnocchi_client(self, vim_account_id: str) -> gnocchi_client.Client: + sess = OpenstackBackend.get_session(vim_account_id) + return gnocchi_client.Client(session=sess) + + def _build_neutron_client(self, vim_account_id: str) -> neutron_client.Client: + sess = OpenstackBackend.get_session(vim_account_id) + return neutron_client.Client(session=sess) + + def collect_metric(self, metric_type: MetricType, metric_name: str, resource_id: str, interface_name: str): + if metric_type == MetricType.INTERFACE_ONE: + return self._collect_interface_one_metric(metric_name, resource_id, interface_name) + + if metric_type == MetricType.INTERFACE_ALL: + return self._collect_interface_all_metric(metric_name, resource_id) + + elif metric_type == MetricType.INSTANCE: + return self._collect_instance_metric(metric_name, resource_id) + + else: + raise Exception('Unknown metric type %s' % metric_type.value) + + def _collect_interface_one_metric(self, metric_name, resource_id, interface_name): + delta = 10 * self.granularity + start_date = datetime.datetime.now() - datetime.timedelta(seconds=delta) + ports = self.neutron.list_ports(name=interface_name, device_id=resource_id) + if not ports or not ports['ports']: + raise Exception( + 'Port not found for interface %s on instance %s' % (interface_name, resource_id)) + port = ports['ports'][0] + port_uuid = port['id'][:11] + tap_name = 'tap' + port_uuid + interfaces = self.client.resource.search(resource_type='instance_network_interface', + query={'=': {'name': tap_name}}) + measures = self.client.metric.get_measures(metric_name, + start=start_date, + resource_id=interfaces[0]['id'], + granularity=self.granularity) + return measures[-1][2] if measures else None + + def _collect_interface_all_metric(self, openstack_metric_name, resource_id): + delta = 10 * self.granularity + start_date = datetime.datetime.now() - datetime.timedelta(seconds=delta) + total_measure = None + interfaces = self.client.resource.search(resource_type='instance_network_interface', + query={'=': {'instance_id': resource_id}}) + for interface in interfaces: try: - ceilometer = self._build_ceilometer_client(vim_account_id) - ceilometer.resources.list(limit=1) - return 'ceilometer' - except Exception: - log.exception('Error trying to determine metric backend') - raise Exception('Could not determine metric backend') + measures = self.client.metric.get_measures(openstack_metric_name, + start=start_date, + resource_id=interface['id'], + granularity=self.granularity) + if measures: + if not total_measure: + total_measure = 0.0 + total_measure += measures[-1][2] + + except gnocchiclient.exceptions.NotFound as e: + log.debug("No metric %s found for interface %s: %s", openstack_metric_name, + interface['id'], e) + return total_measure + + def _collect_instance_metric(self, openstack_metric_name, resource_id): + delta = 10 * self.granularity + start_date = datetime.datetime.now() - datetime.timedelta(seconds=delta) + value = None + try: + measures = self.client.metric.get_measures(openstack_metric_name, + start=start_date, + resource_id=resource_id, + granularity=self.granularity) + if measures: + value = measures[-1][2] + except gnocchiclient.exceptions.NotFound as e: + log.debug("No metric %s found for instance %s: %s", openstack_metric_name, resource_id, + e) + return value + + +class CeilometerBackend(OpenstackBackend): + def __init__(self, vim_account_id: str): + self.client = self._build_ceilometer_client(vim_account_id) + + def _build_ceilometer_client(self, vim_account_id: str) -> ceilometer_client.Client: + sess = OpenstackBackend.get_session(vim_account_id) + return ceilometer_client.Client(session=sess) + + def collect_metric(self, metric_type: MetricType, metric_name: str, resource_id: str, interface_name: str): + if metric_type != MetricType.INSTANCE: + raise NotImplementedError('Ceilometer backend only support instance metrics') + measures = self.client.samples.list(meter_name=metric_name, limit=1, q=[ + {'field': 'resource_id', 'op': 'eq', 'value': resource_id}]) + return measures[0].counter_volume if measures else None diff --git a/osm_mon/tests/unit/collector/vnf_collectors/__init__.py b/osm_mon/tests/unit/collector/vnf_collectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/osm_mon/tests/unit/collector/vnf_collectors/test_openstack.py b/osm_mon/tests/unit/collector/vnf_collectors/test_openstack.py new file mode 100644 index 0000000..0d813a2 --- /dev/null +++ b/osm_mon/tests/unit/collector/vnf_collectors/test_openstack.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018 Whitestack, LLC +# ************************************************************* + +# This file is part of OSM Monitoring module +# All Rights Reserved to Whitestack, LLC + +# 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. +# For those usages not covered by the Apache License, Version 2.0 please +# contact: bdiaz@whitestack.com or glavado@whitestack.com +## +import datetime +from unittest import TestCase, mock + +from osm_mon.collector.vnf_collectors.openstack import GnocchiBackend +from osm_mon.core.config import Config + + +class CollectorTest(TestCase): + def setUp(self): + super().setUp() + self.config = Config() + + def tearDown(self): + super().tearDown() + + @mock.patch.object(GnocchiBackend, '_build_neutron_client') + @mock.patch.object(GnocchiBackend, '_build_gnocchi_client') + def test_collect_gnocchi_instance(self, build_gnocchi_client, build_neutron_client): + mock_gnocchi_client = mock.Mock() + mock_gnocchi_client.metric.get_measures.return_value = [(datetime.datetime(2019, 4, 12, 15, 43, + tzinfo=datetime.timezone( + datetime.timedelta(0), + '+00:00')), 60.0, 0.0345442539), + (datetime.datetime(2019, 4, 12, 15, 44, + tzinfo=datetime.timezone( + datetime.timedelta(0), + '+00:00')), 60.0, 0.0333070363)] + build_gnocchi_client.return_value = mock_gnocchi_client + + backend = GnocchiBackend('test_uuid', 60) + value = backend._collect_instance_metric('cpu_utilization', 'test_resource_id') + self.assertEqual(value, 0.0333070363) + mock_gnocchi_client.metric.get_measures.assert_called_once_with('cpu_utilization', granularity=60, + resource_id='test_resource_id', start=mock.ANY) + + @mock.patch.object(GnocchiBackend, '_build_neutron_client') + @mock.patch.object(GnocchiBackend, '_build_gnocchi_client') + def test_collect_interface_one_metric(self, build_gnocchi_client, build_neutron_client): + mock_gnocchi_client = mock.Mock() + mock_gnocchi_client.resource.search.return_value = [{'id': 'test_id'}] + mock_gnocchi_client.metric.get_measures.return_value = [(datetime.datetime(2019, 4, 12, 15, 43, + tzinfo=datetime.timezone( + datetime.timedelta(0), + '+00:00')), 60.0, 0.0345442539), + (datetime.datetime(2019, 4, 12, 15, 44, + tzinfo=datetime.timezone( + datetime.timedelta(0), + '+00:00')), 60.0, 0.0333070363)] + mock_neutron_client = mock.Mock() + mock_neutron_client.list_ports.return_value = {'ports': [ + {'binding:vnic_type': 'normal', 'created_at': '2019-04-15T17:11:39Z', + 'tenant_id': '88b78c76e01f4b228e68a06f8ebec6da', + 'binding:vif_details': {'port_filter': True, 'ovs_hybrid_plug': True, 'datapath_type': 'system'}, + 'revision_number': 6, 'updated_at': '2019-04-15T17:11:48Z', 'binding:profile': {}, + 'mac_address': 'fa:16:3e:1c:e3:00', 'status': 'ACTIVE', 'project_id': '88b78c76e01f4b228e68a06f8ebec6da', + 'network_id': '7f34edad-9064-4de8-b535-b14f9b715ed9', 'port_security_enabled': True, 'tags': [], + 'description': '', 'security_groups': ['3d60c37e-6229-487b-858d-3aff6d98d66f'], 'admin_state_up': True, + 'binding:vif_type': 'ovs', 'allowed_address_pairs': [], 'name': 'eth0', + 'id': '1c6f9a06-6b88-45f3-8d32-dc1216436f0a', 'binding:host_id': 's111412', + 'fixed_ips': [{'ip_address': '10.0.0.51', 'subnet_id': '4c3fa16c-3e22-43f4-9798-7b10593aff93'}], + 'device_owner': 'compute:nova', 'device_id': '5cf5bbc4-e4f8-4e22-8bbf-6970218e774d', + 'extra_dhcp_opts': []}]} + + build_gnocchi_client.return_value = mock_gnocchi_client + build_neutron_client.return_value = mock_neutron_client + + backend = GnocchiBackend('test_uuid', 60) + value = backend._collect_interface_one_metric('packets_received', 'test_resource_id', 'eth0') + self.assertEqual(value, 0.0333070363) + mock_gnocchi_client.metric.get_measures.assert_called_once_with('packets_received', granularity=60, + resource_id='test_id', start=mock.ANY) + mock_neutron_client.list_ports.assert_called_once_with(device_id='test_resource_id', name='eth0') + + @mock.patch.object(GnocchiBackend, '_build_neutron_client') + @mock.patch.object(GnocchiBackend, '_build_gnocchi_client') + def test_collect_interface_all_metric(self, build_gnocchi_client, build_neutron_client): + mock_gnocchi_client = mock.Mock() + mock_gnocchi_client.resource.search.return_value = [{'id': 'test_id_1'}, {'id': 'test_id_2'}] + mock_gnocchi_client.metric.get_measures.return_value = [(datetime.datetime(2019, 4, 12, 15, 43, + tzinfo=datetime.timezone( + datetime.timedelta(0), + '+00:00')), 60.0, 0.0345442539), + (datetime.datetime(2019, 4, 12, 15, 44, + tzinfo=datetime.timezone( + datetime.timedelta(0), + '+00:00')), 60.0, 0.0333070363)] + + build_gnocchi_client.return_value = mock_gnocchi_client + + backend = GnocchiBackend('test_uuid', 60) + value = backend._collect_interface_all_metric('packets_received', 'test_resource_id') + self.assertEqual(value, 0.0666140726) + mock_gnocchi_client.metric.get_measures.assert_any_call('packets_received', granularity=60, + resource_id='test_id_1', start=mock.ANY) + mock_gnocchi_client.metric.get_measures.assert_any_call('packets_received', granularity=60, + resource_id='test_id_2', start=mock.ANY) diff --git a/osm_mon/tests/unit/core/__init__.py b/osm_mon/tests/unit/core/__init__.py index ce869a3..971f4e9 100644 --- a/osm_mon/tests/unit/core/__init__.py +++ b/osm_mon/tests/unit/core/__init__.py @@ -1,11 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2017 Intel Research and Development Ireland Limited +# Copyright 2018 Whitestack, LLC # ************************************************************* # This file is part of OSM Monitoring module -# All Rights Reserved to Intel Corporation +# All Rights Reserved to Whitestack, LLC # 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 @@ -20,4 +17,5 @@ # under the License. # For those usages not covered by the Apache License, Version 2.0 please -# contact: prithiv.mohan@intel.com or adrian.hoban@intel.com +# contact: bdiaz@whitestack.com or glavado@whitestack.com +## diff --git a/osm_mon/tests/unit/evaluator/__init__.py b/osm_mon/tests/unit/evaluator/__init__.py index ce869a3..971f4e9 100644 --- a/osm_mon/tests/unit/evaluator/__init__.py +++ b/osm_mon/tests/unit/evaluator/__init__.py @@ -1,11 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2017 Intel Research and Development Ireland Limited +# Copyright 2018 Whitestack, LLC # ************************************************************* # This file is part of OSM Monitoring module -# All Rights Reserved to Intel Corporation +# All Rights Reserved to Whitestack, LLC # 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 @@ -20,4 +17,5 @@ # under the License. # For those usages not covered by the Apache License, Version 2.0 please -# contact: prithiv.mohan@intel.com or adrian.hoban@intel.com +# contact: bdiaz@whitestack.com or glavado@whitestack.com +## diff --git a/osm_mon/tests/unit/server/__init__.py b/osm_mon/tests/unit/server/__init__.py index ce869a3..971f4e9 100644 --- a/osm_mon/tests/unit/server/__init__.py +++ b/osm_mon/tests/unit/server/__init__.py @@ -1,11 +1,8 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright 2017 Intel Research and Development Ireland Limited +# Copyright 2018 Whitestack, LLC # ************************************************************* # This file is part of OSM Monitoring module -# All Rights Reserved to Intel Corporation +# All Rights Reserved to Whitestack, LLC # 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 @@ -20,4 +17,5 @@ # under the License. # For those usages not covered by the Apache License, Version 2.0 please -# contact: prithiv.mohan@intel.com or adrian.hoban@intel.com +# contact: bdiaz@whitestack.com or glavado@whitestack.com +## diff --git a/requirements.txt b/requirements.txt index 9da8612..d1436d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,8 +20,6 @@ # contact: prithiv.mohan@intel.com or adrian.hoban@intel.com aiokafka==0.4.* requests==2.18.* -jsmin==2.2.* -jsonschema==2.6.* python-keystoneclient==3.15.* six==1.11.* peewee==3.8.* @@ -33,5 +31,6 @@ pyvcloud==19.1.* python-ceilometerclient==2.9.* peewee-migrate==1.1.* python-novaclient==12.0.* +python-neutronclient==5.1.* git+https://osm.etsi.org/gerrit/osm/common.git@v5.0#egg=osm-common git+https://osm.etsi.org/gerrit/osm/N2VC.git@v5.0#egg=n2vc diff --git a/setup.py b/setup.py index 15828bb..032ee87 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,6 @@ setup( install_requires=[ "aiokafka==0.4.*", "requests==2.18.*", - "jsmin==2.2.*", - "jsonschema==2.6.*", "python-keystoneclient==3.15.*", "six==1.11.*", "peewee==3.8.*", @@ -66,6 +64,7 @@ setup( "peewee-migrate==1.1.*", "python-novaclient==12.0.*", "pymysql==0.9.*", + "python-neutronclient==5.1.*", "osm-common", "n2vc" ], @@ -79,7 +78,8 @@ setup( ] }, dependency_links=[ - 'git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common' + 'git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common@v5.0', + 'git+https://osm.etsi.org/gerrit/osm/common.git#egg=n2vc@v5.0' ], setup_requires=['setuptools-version-command'] )