diff --git a/releasenotes/notes/add-vnf-details-api-b38a225d0020b812.yaml b/releasenotes/notes/add-vnf-details-api-b38a225d0020b812.yaml new file mode 100644 index 000000000..2cf461353 --- /dev/null +++ b/releasenotes/notes/add-vnf-details-api-b38a225d0020b812.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added API to fetch VNF components details. diff --git a/tacker/extensions/vnfm.py b/tacker/extensions/vnfm.py index feca4272f..a42fe4fbf 100644 --- a/tacker/extensions/vnfm.py +++ b/tacker/extensions/vnfm.py @@ -140,6 +140,15 @@ class FilePathMissing(exceptions.InvalidInput): "tosca.artifacts.Deployment.Image.VM artifact type") +class InfraDriverUnreachable(exceptions.ServiceUnavailable): + message = _("Could not retrieve VNF resource IDs and" + " types. Please check %(service)s status.") + + +class VNFInactive(exceptions.InvalidInput): + message = _("VNF %(vnf_id)s is not in Active state %(message)s") + + def _validate_service_type_list(data, valid_values=None): if not isinstance(data, list): msg = _("invalid data format for service list: '%s'") % data @@ -358,6 +367,33 @@ SUB_RESOURCE_ATTRIBUTE_MAP = { } } } + }, + 'resources': { + 'parent': { + 'collection_name': 'vnfs', + 'member_name': 'vnf' + }, + 'members': { + 'resource': { + 'parameters': { + 'name': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'type': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'id': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + } + } + } } } @@ -464,6 +500,10 @@ class VNFMPluginBase(service_base.NFVPluginBase): def get_vnf(self, context, vnf_id, fields=None): pass + @abc.abstractmethod + def get_vnf_resources(self, context, vnf_id, fields=None, filters=None): + pass + @abc.abstractmethod def create_vnf(self, context, vnf): pass diff --git a/tacker/tests/functional/vnfm/test_vnf.py b/tacker/tests/functional/vnfm/test_vnf.py index 8b41f6594..0e1edc474 100644 --- a/tacker/tests/functional/vnfm/test_vnf.py +++ b/tacker/tests/functional/vnfm/test_vnf.py @@ -53,6 +53,12 @@ class VnfTestCreate(base.BaseTackerTest): if vim_id: self.assertEqual(vim_id, vnf_instance['vnf']['vim_id']) + # Get vnf details when vnf is in active state + vnf_details = self.client.list_vnf_resources(vnf_id)['resources'][0] + self.assertIn('name', vnf_details) + self.assertIn('id', vnf_details) + self.assertIn('type', vnf_details) + # Delete vnf_instance with vnf_id try: self.client.delete_vnf(vnf_id) diff --git a/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py b/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py index d744bb46d..ca89060c2 100644 --- a/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py +++ b/tacker/tests/unit/vm/infra_drivers/heat/test_heat.py @@ -20,6 +20,7 @@ import os import yaml from tacker import context +from tacker.extensions import vnfm from tacker.tests.unit import base from tacker.tests.unit.db import utils from tacker.vnfm.infra_drivers.heat import heat @@ -143,6 +144,28 @@ class TestDeviceHeat(base.TestCase): 'id': 'eb84260e-5ff7-4332-b032-50a14d6c1123', 'description': u'OpenWRT with services'} + def _get_expected_active_vnf(self): + return {'status': 'ACTIVE', + 'instance_id': None, + 'name': u'test_openwrt', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'vnfd_id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', + 'vnfd': { + 'service_types': [{ + 'service_type': u'vnfd', + 'id': u'4a4c2d44-8a52-4895-9a75-9d1c76c3e738'}], + 'description': u'OpenWRT with services', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'mgmt_driver': u'openwrt', + 'infra_driver': u'heat', + 'attributes': {u'vnfd': self.vnfd_openwrt}, + 'id': u'fb048660-dc1b-4f0f-bd89-b023666650ec', + 'name': u'openwrt_services'}, + 'mgmt_url': '{"vdu1": "192.168.120.31"}', + 'service_context': [], + 'id': 'eb84260e-5ff7-4332-b032-50a14d6c1123', + 'description': u'OpenWRT with services'} + def test_create(self): vnf_obj = utils.get_dummy_device_obj() expected_result = '4a4c2d44-8a52-4895-9a75-9d1c76c3e738' @@ -408,3 +431,12 @@ class TestDeviceHeat(base.TestCase): files={'scaling.yaml': 'hot_scale_custom.yaml'}, is_monitor=False ) + + def test_get_resource_info(self): + vnf_obj = self._get_expected_active_vnf() + print(vnf_obj) + self.assertRaises(vnfm.InfraDriverUnreachable, + self.heat_driver.get_resource_info, + plugin=None, context=self.context, vnf_info=vnf_obj, + auth_attr=utils.get_vim_auth_obj(), + region_name=None) diff --git a/tacker/tests/unit/vm/test_plugin.py b/tacker/tests/unit/vm/test_plugin.py index 0c3162258..e53d5b652 100644 --- a/tacker/tests/unit/vm/test_plugin.py +++ b/tacker/tests/unit/vm/test_plugin.py @@ -32,6 +32,10 @@ class FakeDriverManager(mock.Mock): def invoke(self, *args, **kwargs): if 'create' in args: return str(uuid.uuid4()) + if 'get_resource_info' in args: + return {'resources': {'name': 'dummy_vnf', + 'type': 'dummy', + 'id': str(uuid.uuid4())}} class FakeVNFMonitor(mock.Mock): @@ -208,6 +212,22 @@ class TestVNFMPlugin(db_base.SqlTestCase): res_state=mock.ANY, res_type=constants.RES_TYPE_VNF, tstamp=mock.ANY, details=mock.ANY) + def test_show_vnf_details_vnf_inactive(self): + self._insert_dummy_device_template() + vnf_obj = utils.get_dummy_vnf_obj() + result = self.vnfm_plugin.create_vnf(self.context, vnf_obj) + self.assertRaises(vnfm.VNFInactive, self.vnfm_plugin.get_vnf_resources, + self.context, result['id']) + + def test_show_vnf_details_vnf_active(self): + self._insert_dummy_device_template() + active_vnf = self._insert_dummy_device() + resources = self.vnfm_plugin.get_vnf_resources(self.context, + active_vnf['id'])[0] + self.assertIn('name', resources) + self.assertIn('type', resources) + self.assertIn('id', resources) + def test_delete_vnf(self): self._insert_dummy_device_template() dummy_device_obj = self._insert_dummy_device() diff --git a/tacker/vm/plugin.py b/tacker/vm/plugin.py index d537b46ae..ebbe4c425 100644 --- a/tacker/vm/plugin.py +++ b/tacker/vm/plugin.py @@ -629,3 +629,24 @@ class VNFMPlugin(vm_db.VNFMPluginDb, VNFMMgmtMixin): self._handle_vnf_scaling(context, policy_) return scale['scale'] + + def get_vnf_resources(self, context, vnf_id, fields=None, filters=None): + vnf_info = self.get_vnf(context, vnf_id) + infra_driver = vnf_info['vnfd']['infra_driver'] + auth = self.get_vim(context, vnf_info) + if vnf_info['status'] == constants.ACTIVE: + vnf_details = self._vnf_manager.invoke(infra_driver, + 'get_resource_info', + plugin=self, + context=context, + vnf_info=vnf_info, + auth_attr=auth) + resources = [{'name': name, + 'type': info.get('type'), + 'id': info.get('id')} + for name, info in vnf_details.items()] + return resources + # Raise exception when VNF.status != ACTIVE + else: + raise vnfm.VNFInactive(vnf_id=vnf_id, + message=_(' Cannot fetch details')) diff --git a/tacker/vnfm/infra_drivers/abstract_driver.py b/tacker/vnfm/infra_drivers/abstract_driver.py index f6adba3dd..f373b1a44 100644 --- a/tacker/vnfm/infra_drivers/abstract_driver.py +++ b/tacker/vnfm/infra_drivers/abstract_driver.py @@ -68,3 +68,9 @@ class DeviceAbstractDriver(extensions.PluginInterface): @abc.abstractmethod def delete_wait(self, plugin, context, vnf_id): pass + + @abc.abstractmethod + def get_resource_info(self, plugin, context, vnf_info, auth_attr, + region_name=None): + '''Fetches optional details of a VNF''' + pass diff --git a/tacker/vnfm/infra_drivers/heat/heat.py b/tacker/vnfm/infra_drivers/heat/heat.py index 59c2e3ba3..c31f84e70 100644 --- a/tacker/vnfm/infra_drivers/heat/heat.py +++ b/tacker/vnfm/infra_drivers/heat/heat.py @@ -855,6 +855,21 @@ class DeviceHeat(abstract_driver.DeviceAbstractDriver, return jsonutils.dumps(mgmt_ips) + def get_resource_info(self, plugin, context, vnf_info, auth_attr, + region_name=None): + stack_id = vnf_info['instance_id'] + heatclient_ = HeatClient(auth_attr, region_name) + try: + resources_ids = heatclient_.resource_get_list(stack_id) + details_dict = {resource.resource_name: + {"id": resource.physical_resource_id, + "type": resource.resource_type} + for resource in resources_ids} + return details_dict + # Raise exception when Heat API service is not available + except Exception: + raise vnfm.InfraDriverUnreachable(service="Heat API service") + class HeatClient(object): def __init__(self, auth_attr, region_name=None): @@ -862,6 +877,7 @@ class HeatClient(object): self.heat = clients.OpenstackClients(auth_attr, region_name).heat self.stacks = self.heat.stacks self.resource_types = self.heat.resource_types + self.resources = self.heat.resources def create(self, fields): fields = fields.copy()