From eae2c896cd666e1eed6f59d9379259947c1fbff8 Mon Sep 17 00:00:00 2001 From: Shubham Potale Date: Tue, 10 Dec 2019 18:53:50 +0530 Subject: [PATCH] OSC support to instantiate and show vnf Added new commands ``openstack vnflcm instantiate`` and ``openstack vnflcm show``. Blueprint: support-etsi-nfv-specs Change-Id: I528e20be6ec01c61b5ca6d646972a9ae22f1c158 --- setup.cfg | 2 + ...instantiate_vnf_instance_param_sample.json | 79 +++++++++ tackerclient/osc/v1/vnflcm/vnflcm.py | 116 ++++++++++++- tackerclient/tests/unit/osc/v1/test_vnflcm.py | 161 +++++++++++++++++- .../tests/unit/osc/v1/vnflcm_fakes.py | 74 +++++++- tackerclient/v1_0/client.py | 17 ++ 6 files changed, 436 insertions(+), 13 deletions(-) create mode 100644 tackerclient/osc/v1/vnflcm/samples/instantiate_vnf_instance_param_sample.json diff --git a/setup.cfg b/setup.cfg index cb9121b0..bbc469f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,8 @@ openstack.tackerclient.v1 = vnf_package_upload = tackerclient.osc.v1.vnfpkgm.vnf_package:UploadVnfPackage vnf_package_delete = tackerclient.osc.v1.vnfpkgm.vnf_package:DeleteVnfPackage vnflcm_create = tackerclient.osc.v1.vnflcm.vnflcm:CreateVnfLcm + vnflcm_show = tackerclient.osc.v1.vnflcm.vnflcm:ShowVnfLcm + vnflcm_instantiate = tackerclient.osc.v1.vnflcm.vnflcm:InstantiateVnfLcm [build_releasenotes] all_files = 1 diff --git a/tackerclient/osc/v1/vnflcm/samples/instantiate_vnf_instance_param_sample.json b/tackerclient/osc/v1/vnflcm/samples/instantiate_vnf_instance_param_sample.json new file mode 100644 index 00000000..d6c3c67f --- /dev/null +++ b/tackerclient/osc/v1/vnflcm/samples/instantiate_vnf_instance_param_sample.json @@ -0,0 +1,79 @@ +{ + "flavourId":"simple", + "instantiationLevelId":"instantiation_level_1", + "extVirtualLinks":[ + { + "id":"ext-vl-uuid-VL1", + "vimConnectionId":"vim-uuid", + "resourceProviderId":"resource-provider-id", + "resourceId":"neutron-network-uuid_VL1", + "extCps":[ + { + "cpdId":"CP1", + "cpConfig":[ + { + "cpInstanceId":"cp-instance-id", + "linkPortId":"link-port-uuid_CP1", + "cpProtocolData":[ + { + "layerProtocol":"IP_OVER_ETHERNET", + "ipOverEthernet":{ + "macAddress":"00:25:96:FF:FE:12:34:56", + "ipAddresses":[ + { + "addressRange":{ + "minAddress":"192.168.11.01", + "maxAddress":"192.168.21.201" + }, + "subnetId":"neutron-subnet-uuid_CP1" + } + ] + } + } + ] + } + ] + } + ], + "extLinkPorts":[ + { + "id":"link-port-uuid_CP1", + "resourceHandle":{ + "vimConnectionId":"vim-uuid", + "resourceProviderId":"resource-provider-id", + "resourceId":"neutron-port-uuid_CP1", + "vimLevelResourceType":"LINKPORT" + } + } + ] + } + ], + "extManagedVirtualLinks":[ + { + "id":"extMngVLnk-uuid_VL3", + "vnfVirtualLinkDescId":"VL3", + "vimConnectionId":"vim-uuid", + "resourceProviderId":"resource-provider-id", + "resourceId":"neutron-network-uuid_VL3" + } + ], + "vimConnectionInfo":[ + { + "id":"vim-uuid", + "vimId":"dummy-vimid", + "vimType":"ETSINFV.OPENSTACK_KEYSTONE.v_2", + "interfaceInfo":{ + "key1":"value1", + "key2":"value2" + }, + "accessInfo":{ + "key1":"value1", + "key2":"value2" + }, + "extra":{ + "key1":"value1", + "key2":"value2" + } + } + ] +} \ No newline at end of file diff --git a/tackerclient/osc/v1/vnflcm/vnflcm.py b/tackerclient/osc/v1/vnflcm/vnflcm.py index efc20682..4306bc48 100644 --- a/tackerclient/osc/v1/vnflcm/vnflcm.py +++ b/tackerclient/osc/v1/vnflcm/vnflcm.py @@ -13,18 +13,29 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import logging +import os + +from osc_lib.cli import format_columns from osc_lib.command import command from osc_lib import utils +from tackerclient.common import exceptions from tackerclient.i18n import _ from tackerclient.osc import sdk_utils +LOG = logging.getLogger(__name__) + _mixed_case_fields = ('vnfInstanceName', 'vnfInstanceDescription', 'vnfdId', 'vnfProvider', 'vnfProductName', 'vnfSoftwareVersion', - 'vnfdVersion', 'instantiationState') + 'vnfdVersion', 'instantiationState', + 'vimConnectionInfo', 'instantiatedVnfInfo') + +_VNF_INSTANCE = 'vnf_instance' -def _get_columns(item): +def _get_columns(vnflcm_obj, action=None): column_map = { 'id': 'ID', 'vnfInstanceName': 'VNF Instance Name', @@ -35,9 +46,19 @@ def _get_columns(item): 'vnfSoftwareVersion': 'VNF Software Version', 'vnfdVersion': 'VNFD Version', 'instantiationState': 'Instantiation State', - 'links': 'Links', + '_links': 'Links', } - return sdk_utils.get_osc_show_columns_for_sdk_resource(item, column_map) + if action == 'show': + if vnflcm_obj['instantiationState'] == 'INSTANTIATED': + column_map.update( + {'instantiatedVnfInfo': 'Instantiated Vnf Info'} + ) + column_map.update( + {'vimConnectionInfo': 'VIM Connection Info', + '_links': 'Links'} + ) + return sdk_utils.get_osc_show_columns_for_sdk_resource(vnflcm_obj, + column_map) class CreateVnfLcm(command.ShowOne): @@ -58,10 +79,19 @@ class CreateVnfLcm(command.ShowOne): '--description', metavar="", help=_('Description of the VNF instance to be created.')) + parser.add_argument( + '--I', + metavar="", + help=_("Instantiate VNF subsequently after it's creation. " + "Specify instantiate request parameters in a json file.")) return parser - def args2body(self, parsed_args): + def args2body(self, parsed_args, file_path=None): body = {} + + if file_path: + return instantiate_vnf_args2body(file_path) + body['vnfdId'] = parsed_args.vnfd_id if parsed_args.description: @@ -75,8 +105,84 @@ class CreateVnfLcm(command.ShowOne): def take_action(self, parsed_args): client = self.app.client_manager.tackerclient vnf = client.create_vnf_instance(self.args2body(parsed_args)) + if parsed_args.I: + # Instantiate VNF instance. + result = client.instantiate_vnf_instance( + vnf['id'], + self.args2body(parsed_args, file_path=parsed_args.I)) + if not result: + print((_('VNF Instance %(id)s is created and instantiation' + ' request has been accepted.') % {'id': vnf['id']})) display_columns, columns = _get_columns(vnf) data = utils.get_item_properties( sdk_utils.DictModel(vnf), columns, mixed_case_fields=_mixed_case_fields) return (display_columns, data) + + +class ShowVnfLcm(command.ShowOne): + _description = _("Display VNF instance details") + + def get_parser(self, prog_name): + parser = super(ShowVnfLcm, self).get_parser(prog_name) + parser.add_argument( + _VNF_INSTANCE, + metavar="", + help=_("VNF instance ID to display")) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + obj = client.show_vnf_instance(parsed_args.vnf_instance) + display_columns, columns = _get_columns(obj, action='show') + data = utils.get_item_properties( + sdk_utils.DictModel(obj), + columns, mixed_case_fields=_mixed_case_fields, + formatters={'instantiatedVnfInfo': format_columns.DictColumn}) + return (display_columns, data) + + +def instantiate_vnf_args2body(file_path): + + if file_path is not None and os.access(file_path, os.R_OK) is False: + msg = _("File %s does not exist or user does not have read " + "privileges to it") + raise exceptions.InvalidInput(msg % file_path) + + try: + with open(file_path) as f: + body = json.load(f) + except (IOError, ValueError) as ex: + msg = _("Failed to load parameter file. Error: %s") + raise exceptions.InvalidInput(msg % ex) + + if not body: + raise exceptions.InvalidInput(_('The parameter file is empty')) + + return body + + +class InstantiateVnfLcm(command.Command): + _description = _("Instantiate a VNF Instance") + + def get_parser(self, prog_name): + parser = super(InstantiateVnfLcm, self).get_parser(prog_name) + parser.add_argument( + _VNF_INSTANCE, + metavar="", + help=_("VNF instance ID to instantiate")) + parser.add_argument( + 'instantiation_request_file', + metavar="", + help=_('Specify instantiate request parameters in a json file.')) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.tackerclient + result = client.instantiate_vnf_instance( + parsed_args.vnf_instance, instantiate_vnf_args2body( + parsed_args.instantiation_request_file)) + if not result: + print((_('Instantiate request for VNF Instance %(id)s has been' + ' accepted.') % {'id': parsed_args.vnf_instance})) diff --git a/tackerclient/tests/unit/osc/v1/test_vnflcm.py b/tackerclient/tests/unit/osc/v1/test_vnflcm.py index 25670483..f94fe525 100644 --- a/tackerclient/tests/unit/osc/v1/test_vnflcm.py +++ b/tackerclient/tests/unit/osc/v1/test_vnflcm.py @@ -14,15 +14,19 @@ # under the License. import ddt +from io import StringIO import mock import os +import sys from oslo_utils.fixture import uuidsentinel +from tackerclient.common import exceptions from tackerclient.osc.v1.vnflcm import vnflcm from tackerclient.tests.unit.osc import base from tackerclient.tests.unit.osc.v1.fixture_data import client from tackerclient.tests.unit.osc.v1 import vnflcm_fakes +from tackerclient.v1_0 import client as proxy_client class TestVnfLcm(base.FixturedTestCase): @@ -38,10 +42,12 @@ class TestVnfLcm(base.FixturedTestCase): self.app.client_manager.tackerclient = self.client_manager -def _get_columns_vnflcm(): +def _get_columns_vnflcm(action='create'): columns = ['ID', 'Instantiation State', 'VNF Instance Description', 'VNF Instance Name', 'VNF Product Name', 'VNF Provider', 'VNF Software Version', 'VNFD ID', 'VNFD Version', 'Links'] + if action == 'show': + columns.extend(['Instantiated Vnf Info', 'VIM Connection Info']) return columns @@ -57,8 +63,10 @@ class TestCreateVnfLcm(TestVnfLcm): self.assertRaises(base.ParserException, self.check_parser, self.create_vnf_lcm, [], []) - @ddt.data(True, False) - def test_take_action(self, optional_arguments): + @ddt.data({"optional_arguments": True, "instantiate": True}, + {"optional_arguments": False, "instantiate": False}) + @ddt.unpack + def test_take_action(self, optional_arguments, instantiate): arglist = [uuidsentinel.vnf_package_vnfd_id] verifylist = [('vnfd_id', uuidsentinel.vnf_package_vnfd_id)] @@ -69,6 +77,13 @@ class TestCreateVnfLcm(TestVnfLcm): ('description', 'test')]) # command param + if instantiate: + param_file = ("./tackerclient/osc/v1/vnflcm/samples/" + "instantiate_vnf_instance_param_sample.json") + + arglist.extend(['--I', param_file]) + verifylist.append(('I', param_file)) + parsed_args = self.check_parser(self.create_vnf_lcm, arglist, verifylist) @@ -77,8 +92,148 @@ class TestCreateVnfLcm(TestVnfLcm): 'POST', os.path.join(self.url, 'vnflcm/v1/vnf_instances'), json=json, headers=self.header) + if instantiate: + self.requests_mock.register_uri( + 'POST', os.path.join(self.url, 'vnflcm/v1/vnf_instances', + json['id'], 'instantiate'), + json={}, headers=self.header) + + sys.stdout = buffer = StringIO() columns, data = (self.create_vnf_lcm.take_action(parsed_args)) + + expected_message = ( + 'VNF Instance ' + json['id'] + ' is created and instantiation ' + 'request has been accepted.') + if instantiate: + self.assertEqual(expected_message, buffer.getvalue().strip()) + self.assertItemsEqual(_get_columns_vnflcm(), columns) self.assertItemsEqual(vnflcm_fakes.get_vnflcm_data(json), data) + + +class TestShowVnfLcm(TestVnfLcm): + + def setUp(self): + super(TestShowVnfLcm, self).setUp() + self.show_vnf_lcm = vnflcm.ShowVnfLcm( + self.app, self.app_args, cmd_name='vnflcm show') + + def test_take_action(self): + vnf_instance = vnflcm_fakes.vnf_instance_response( + instantiation_state='INSTANTIATED') + + arglist = [vnf_instance['id']] + verifylist = [('vnf_instance', vnf_instance['id'])] + + # command param + parsed_args = self.check_parser(self.show_vnf_lcm, arglist, + verifylist) + + self.requests_mock.register_uri( + 'GET', os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id']), + json=vnf_instance, headers=self.header) + + columns, data = (self.show_vnf_lcm.take_action(parsed_args)) + self.assertItemsEqual(_get_columns_vnflcm(action='show'), + columns) + + +class TestInstantiateVnfLcm(TestVnfLcm): + + def setUp(self): + super(TestInstantiateVnfLcm, self).setUp() + self.instantiate_vnf_lcm = vnflcm.InstantiateVnfLcm( + self.app, self.app_args, cmd_name='vnflcm instantiate') + + def test_take_action(self): + vnf_instance = vnflcm_fakes.vnf_instance_response() + sample_param_file = ("./tackerclient/osc/v1/vnflcm/samples/" + "instantiate_vnf_instance_param_sample.json") + + arglist = [vnf_instance['id'], sample_param_file] + verifylist = [('vnf_instance', vnf_instance['id']), + ('instantiation_request_file', sample_param_file)] + + # command param + parsed_args = self.check_parser(self.instantiate_vnf_lcm, arglist, + verifylist) + + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id'], 'instantiate') + self.requests_mock.register_uri( + 'POST', url, headers=self.header, json={}) + + sys.stdout = buffer = StringIO() + with mock.patch.object(proxy_client.ClientBase, + '_handle_fault_response') as m: + self.instantiate_vnf_lcm.take_action(parsed_args) + # check no fault response is received + self.assertNotCalled(m) + self.assertEqual( + 'Instantiate request for VNF Instance ' + vnf_instance['id'] + + ' has been accepted.', buffer.getvalue().strip()) + + def test_take_action_vnf_instance_not_found(self): + vnf_instance = vnflcm_fakes.vnf_instance_response() + sample_param_file = ("./tackerclient/osc/v1/vnflcm/samples/" + "instantiate_vnf_instance_param_sample.json") + arglist = [vnf_instance['id'], sample_param_file] + verifylist = [('vnf_instance', vnf_instance['id']), + ('instantiation_request_file', sample_param_file)] + + # command param + parsed_args = self.check_parser(self.instantiate_vnf_lcm, arglist, + verifylist) + + url = os.path.join(self.url, 'vnflcm/v1/vnf_instances', + vnf_instance['id'], 'instantiate') + self.requests_mock.register_uri( + 'POST', url, headers=self.header, status_code=404, json={}) + + self.assertRaises(exceptions.TackerClientException, + self.instantiate_vnf_lcm.take_action, + parsed_args) + + def test_take_action_param_file_not_exists(self): + vnf_instance = vnflcm_fakes.vnf_instance_response() + sample_param_file = "./not_exists.json" + arglist = [vnf_instance['id'], sample_param_file] + verifylist = [('vnf_instance', vnf_instance['id']), + ('instantiation_request_file', sample_param_file)] + + # command param + parsed_args = self.check_parser(self.instantiate_vnf_lcm, arglist, + verifylist) + + ex = self.assertRaises(exceptions.InvalidInput, + self.instantiate_vnf_lcm.take_action, + parsed_args) + + expected_msg = ("File %s does not exist or user does not have read " + "privileges to it") + self.assertEqual(expected_msg % sample_param_file, ex.message) + + @mock.patch("os.open") + @mock.patch("os.access") + def test_take_action_invalid_format_param_file(self, mock_open, + mock_access): + vnf_instance = vnflcm_fakes.vnf_instance_response() + sample_param_file = "./invalid_param_file.json" + arglist = [vnf_instance['id'], sample_param_file] + verifylist = [('vnf_instance', vnf_instance['id']), + ('instantiation_request_file', sample_param_file)] + + mock_open.return_value = "invalid_json_data" + # command param + parsed_args = self.check_parser(self.instantiate_vnf_lcm, arglist, + verifylist) + + ex = self.assertRaises(exceptions.InvalidInput, + self.instantiate_vnf_lcm.take_action, + parsed_args) + + expected_msg = "Failed to load parameter file." + self.assertIn(expected_msg, ex.message) diff --git a/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py b/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py index 5f560626..c48cfb06 100644 --- a/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py +++ b/tackerclient/tests/unit/osc/v1/vnflcm_fakes.py @@ -16,7 +16,7 @@ from oslo_utils.fixture import uuidsentinel -def vnf_instance_response(attrs=None): +def vnf_instance_response(attrs=None, instantiation_state='NOT_INSTANTIATED'): """Create a fake vnf instance. :param Dictionary attrs: @@ -36,10 +36,74 @@ def vnf_instance_response(attrs=None): "vnfProductName": "Sample VNF", "vnfSoftwareVersion": "1.0", "vnfdVersion": "1.0", - "instantiationState": "NOT_INSTANTIATED", - "links": "vnflcm/v1/vnf_instances/" + uuidsentinel.vnf_instance_id + - "/instantiate" - } + "_links": "vnflcm/v1/vnf_instances/" + uuidsentinel.vnf_instance_id + + "/instantiate", + "instantiationState": instantiation_state} + if instantiation_state == 'INSTANTIATED': + dummy_vnf_instance.update({ + "vimConnectionInfo": [{ + 'id': uuidsentinel.uuid, + 'vimId': uuidsentinel.vimId, + 'vimType': 'openstack', + 'interfaceInfo': {'k': 'v'}, + 'accessInfo': {'k': 'v'}, + 'extra': {'k': 'v'} + }], + "instantiatedVnfInfo": { + "flavourId": uuidsentinel.flavourId, + "vnfState": "STARTED", + "extCpInfo": [{ + 'id': uuidsentinel.extCpInfo_uuid, + 'cpdId': uuidsentinel.cpdId_uuid, + 'cpProtocolInfo': [{ + 'layerProtocol': 'IP_OVER_ETHERNET', + 'ipOverEthernet': '{}' + }], + 'extLinkPortId': uuidsentinel.extLinkPortId_uuid, + 'metadata': {'k': 'v'}, + 'associatedVnfcCpId': uuidsentinel.associatedVnfcCpId_uuid + }], + "extVirtualLinkInfo": [{ + 'id': uuidsentinel.extVirtualLinkInfo_uuid, + 'resourceHandle': {}, + 'extLinkPorts': [] + }], + "extManagedVirtualLinkInfo": [{ + "id": uuidsentinel.extManagedVirtualLinkInfo_uuid, + 'vnfVirtualLinkDescId': {}, + 'networkResource': {}, + 'vnfLinkPorts': [] + }], + "vnfcResourceInfo": [{ + 'id': uuidsentinel.vnfcResourceInfo_uuid, + 'vduId': uuidsentinel.vduId_uuid, + 'computeResource': {}, + 'storageResourceIds': [], + 'reservationId': uuidsentinel.reservationId, + }], + "vnfVirtualLinkResourceInfo": [{ + 'id': uuidsentinel.vnfVirtualLinkResourceInfo, + 'vnfVirtualLinkDescId': 'VL4', + 'networkResource': {}, + 'reservationId': uuidsentinel.reservationId, + 'vnfLinkPorts': [], + 'metadata': {'k': 'v'} + }], + "virtualStorageResourceInfo": [{ + 'id': uuidsentinel.virtualStorageResourceInfo, + 'virtualStorageDescId': uuidsentinel.virtualStorageDescId, + 'storageResource': {}, + 'reservationId': uuidsentinel.reservationId, + 'metadata': {'k': 'v'} + }] + }, + "_links": { + 'self': 'self_link', + 'indicators': None, + 'instantiate': 'instantiate_link' + } + }) + return dummy_vnf_instance diff --git a/tackerclient/v1_0/client.py b/tackerclient/v1_0/client.py index 15c6d155..41169998 100644 --- a/tackerclient/v1_0/client.py +++ b/tackerclient/v1_0/client.py @@ -782,6 +782,7 @@ class VnfLCMClient(ClientBase): """ vnf_instances_path = '/vnflcm/v1/vnf_instances' + vnf_instance_path = '/vnflcm/v1/vnf_instances/%s' def build_action(self, action): return action @@ -790,6 +791,15 @@ class VnfLCMClient(ClientBase): def create_vnf_instance(self, body): return self.post(self.vnf_instances_path, body=body) + @APIParamsCall + def show_vnf_instance(self, vnf_id, **_params): + return self.get(self.vnf_instance_path % vnf_id, params=_params) + + @APIParamsCall + def instantiate_vnf_instance(self, vnf_id, body): + return self.post((self.vnf_instance_path + "/instantiate") % vnf_id, + body=body) + class Client(object): """Unified interface to interact with multiple applications of tacker service. @@ -1041,3 +1051,10 @@ class Client(object): def create_vnf_instance(self, body): return self.vnf_lcm_client.create_vnf_instance(body) + + def show_vnf_instance(self, vnf_instance, **_params): + return self.vnf_lcm_client.show_vnf_instance(vnf_instance, + **_params) + + def instantiate_vnf_instance(self, vnf_id, body): + return self.vnf_lcm_client.instantiate_vnf_instance(vnf_id, body)