From a49b445745d8f9341de67120d8880a096ffb61c2 Mon Sep 17 00:00:00 2001 From: Aldinson Esto Date: Tue, 27 Apr 2021 18:47:07 +0900 Subject: [PATCH] Support TOSCA route for ChgExternalConnectivity Current ChangeExternalConnectivity operation only supports user data case, we will improve such operation to support TOSCA route. Implements: blueprint support-change-external-connectivity Change-Id: I5f03b27ddcbad883317d9817c08af7e59cb327f0 --- tacker/tests/functional/base.py | 29 ++ tacker/tests/functional/sol/vnflcm/base.py | 8 - .../sol/vnflcm/test_vnf_instance.py | 303 ++++++++++++++++-- .../test_vnf_instance_with_user_data.py | 21 -- .../fixture_data/fixture_data_utils.py | 128 ++++++++ .../openstack/test_openstack_driver.py | 45 ++- .../openstack/test_update_template.py | 227 +++++++++++++ .../vnfm/infra_drivers/openstack/openstack.py | 135 ++++++-- .../openstack/update_template.py | 98 ++++++ 9 files changed, 907 insertions(+), 87 deletions(-) create mode 100644 tacker/tests/unit/vnfm/infra_drivers/openstack/test_update_template.py create mode 100644 tacker/vnfm/infra_drivers/openstack/update_template.py diff --git a/tacker/tests/functional/base.py b/tacker/tests/functional/base.py index 0e2b88b14..c8ff61680 100644 --- a/tacker/tests/functional/base.py +++ b/tacker/tests/functional/base.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import time import yaml @@ -447,3 +448,31 @@ class BaseTackerTest(base.BaseTestCase): self.validate_vnf_instance(vnfd_instance, vnf_instance) return vnf_instance, tosca_dict + + def _list_op_occs(self, filter_string=''): + show_url = os.path.join( + self.base_vnf_lcm_op_occs_url) + resp, response_body = self.http_client.do_request( + show_url + filter_string, "GET") + return resp, response_body + + def _assert_occ_list(self, resp, op_occs_list): + self.assertEqual(200, resp.status_code) + + # Only check required parameters. + for op_occs_info in op_occs_list: + self.assertIsNotNone(op_occs_info.get('id')) + self.assertIsNotNone(op_occs_info.get('operationState')) + self.assertIsNotNone(op_occs_info.get('stateEnteredTime')) + self.assertIsNotNone(op_occs_info.get('vnfInstanceId')) + self.assertIsNotNone(op_occs_info.get('operation')) + self.assertIsNotNone(op_occs_info.get('isAutomaticInvocation')) + self.assertIsNotNone(op_occs_info.get('isCancelPending')) + + _links = op_occs_info.get('_links') + self.assertIsNotNone(_links.get('self')) + self.assertIsNotNone(_links.get('self').get('href')) + self.assertIsNotNone(_links.get('vnfInstance')) + self.assertIsNotNone(_links.get('vnfInstance').get('href')) + self.assertIsNotNone(_links.get('grant')) + self.assertIsNotNone(_links.get('grant').get('href')) diff --git a/tacker/tests/functional/sol/vnflcm/base.py b/tacker/tests/functional/sol/vnflcm/base.py index d1ab0b6fb..de194f7a8 100644 --- a/tacker/tests/functional/sol/vnflcm/base.py +++ b/tacker/tests/functional/sol/vnflcm/base.py @@ -526,14 +526,6 @@ class BaseVnfLcmTest(base.BaseTackerTest): return resp, response_body - def _list_op_occs(self, filter_string=''): - show_url = os.path.join( - self.base_vnf_lcm_op_occs_url) - resp, response_body = self.http_client.do_request( - show_url + filter_string, "GET") - - return resp, response_body - def _wait_terminate_vnf_instance(self, id, timeout=None): start_time = int(time.time()) diff --git a/tacker/tests/functional/sol/vnflcm/test_vnf_instance.py b/tacker/tests/functional/sol/vnflcm/test_vnf_instance.py index 42d3ce989..5d4d89626 100644 --- a/tacker/tests/functional/sol/vnflcm/test_vnf_instance.py +++ b/tacker/tests/functional/sol/vnflcm/test_vnf_instance.py @@ -30,6 +30,7 @@ VNF_PACKAGE_UPLOAD_TIMEOUT = 300 VNF_INSTANTIATE_TIMEOUT = 600 VNF_TERMINATE_TIMEOUT = 600 VNF_HEAL_TIMEOUT = 600 +VNF_CHANGE_EXT_CONN_TIMEOUT = 600 RETRY_WAIT_TIME = 5 @@ -47,9 +48,68 @@ def generate_mac_address(): return ':'.join(map(lambda x: "%02x" % x, mac)) +def generate_ip_addresses( + type_='IPV4', + fixed_addresses=None, + subnet_id=None): + if fixed_addresses: + ip_addr = { + 'type': type_, + 'fixedAddresses': fixed_addresses + } + if subnet_id: + ip_addr.update({'subnetId': subnet_id}) + return [ip_addr] + + +def get_ext_cp_with_external_link_port(nw_resource_id, port_uuid): + ext_cp = { + "id": "external_network", + "resourceId": nw_resource_id, + "extCps": [{ + "cpdId": "CP2", + "cpConfig": [{ + "linkPortId": "413f4e46-21cf-41b1-be0f-de8d23f76cfe", + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET" + }] + }] + }], + "extLinkPorts": [{ + "id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe", + "resourceHandle": { + "resourceId": port_uuid, + "vimLevelResourceType": "LINKPORT" + } + }] + } + return ext_cp + + +def get_ext_cp_with_fixed_address(nw_resource_id, fixed_addresses, subnet_id): + ext_cp = { + "id": "external_network", + "resourceId": nw_resource_id, + "extCps": [{ + "cpdId": "CP2", + "cpConfig": [{ + "cpProtocolData": [{ + "layerProtocol": "IP_OVER_ETHERNET", + "ipOverEthernet": { + "ipAddresses": generate_ip_addresses( + fixed_addresses=fixed_addresses, + subnet_id=subnet_id) + } + }] + }] + }] + } + return ext_cp + + def get_external_virtual_links(net_0_resource_id, net_mgmt_resource_id, - port_uuid): - return [ + port_uuid, fixed_addresses=None, subnet_id=None): + ext_vl = [ { "id": "net0", "resourceId": net_0_resource_id, @@ -63,28 +123,18 @@ def get_external_virtual_links(net_0_resource_id, net_mgmt_resource_id, } }] }] - }]}, - { - "id": "external_network", - "resourceId": net_mgmt_resource_id, - "extCps": [{ - "cpdId": "CP2", - "cpConfig": [{ - "linkPortId": "413f4e46-21cf-41b1-be0f-de8d23f76cfe", - "cpProtocolData": [{ - "layerProtocol": "IP_OVER_ETHERNET" - }] - }] - }], - "extLinkPorts": [{ - "id": "413f4e46-21cf-41b1-be0f-de8d23f76cfe", - "resourceHandle": { - "resourceId": port_uuid, - "vimLevelResourceType": "LINKPORT" - } }] } ] + if fixed_addresses: + ext_cp = get_ext_cp_with_fixed_address( + net_mgmt_resource_id, fixed_addresses, subnet_id) + else: + ext_cp = get_ext_cp_with_external_link_port( + net_mgmt_resource_id, port_uuid) + ext_vl.append(ext_cp) + + return ext_vl def _create_and_upload_vnf_package(tacker_client, csar_package_name, @@ -170,6 +220,7 @@ class VnfLcmTest(base.BaseTackerTest): def setUp(self): super(VnfLcmTest, self).setUp() self.base_url = "/vnflcm/v1/vnf_instances" + self.base_vnf_lcm_op_occs_url = "/vnflcm/v1/vnf_lcm_op_occs" vim_list = self.client.list_vims() if not vim_list: @@ -254,8 +305,8 @@ class VnfLcmTest(base.BaseTackerTest): self.assertEqual(200, resp.status_code) return vnf_instances - def _stack_update_wait(self, stack_id, expected_status): - timeout = VNF_HEAL_TIMEOUT + def _stack_update_wait(self, stack_id, expected_status, + timeout=VNF_HEAL_TIMEOUT): start_time = int(time.time()) while True: stack = self.h_client.stacks.get(stack_id) @@ -403,6 +454,115 @@ class VnfLcmTest(base.BaseTackerTest): # in nova. self._get_server(vdu_resource_id_current) + def _change_ext_conn_vnf_request(self, vim_id=None, ext_vl=None): + request_body = {} + if ext_vl: + request_body["extVirtualLinks"] = ext_vl + + if vim_id: + request_body["vimConnectionInfo"] = [ + {"id": uuidutils.generate_uuid(), + "vimId": vim_id, + "vimType": "ETSINFV.OPENSTACK_KEYSTONE.v_2"}] + + return request_body + + def _change_ext_conn_vnf_instance(self, vnf_instance, request_body, + expected_stack_status=infra_cnst.STACK_UPDATE_COMPLETE): + url = os.path.join(self.base_url, vnf_instance['id'], + "change_ext_conn") + resp, body = self.http_client.do_request(url, "POST", + body=jsonutils.dumps(request_body)) + self.assertEqual(202, resp.status_code) + + stack = self.h_client.stacks.get(vnf_instance['vnfInstanceName']) + # Wait until tacker changes the stack resources as requested + # in the change_ext_conn request + self._stack_update_wait(stack.id, expected_stack_status, + VNF_CHANGE_EXT_CONN_TIMEOUT) + + def _get_heat_stack(self, vnf_instance_id, stack_name): + heatclient = self.heatclient() + try: + stacks = heatclient.stacks.list() + except Exception: + return None + + target_stakcs = list( + filter( + lambda x: x.stack_name == stack_name, + stacks)) + + if len(target_stakcs) == 0: + return None + + return target_stakcs[0] + + def _get_heat_resource_info(self, stack_id, nested_depth=0, + resource_name=None): + heatclient = self.heatclient() + try: + if resource_name is None: + resources = heatclient.resources.list(stack_id, + nested_depth=nested_depth) + else: + resources = heatclient.resources.get(stack_id, + resource_name) + except Exception: + return None + return resources + + def _get_fixed_ips(self, vnf_instance, request_body): + vnf_instance_id = vnf_instance['id'] + vnf_instance_name = vnf_instance['vnfInstanceName'] + res_name = None + for extvirlink in request_body['extVirtualLinks']: + if 'extCps' not in extvirlink: + continue + for extcps in extvirlink['extCps']: + if 'cpdId' in extcps: + if res_name is None: + res_name = list() + res_name.append(extcps['cpdId']) + break + if res_name is None: + return [] + + stack = self._get_heat_stack(vnf_instance_id, + vnf_instance_name) + stack_id = stack.id + + stack_resource = self._get_heat_resource_info( + stack_id, nested_depth=2) + + releations = dict() + for elmt in stack_resource: + if elmt.resource_type != 'OS::Neutron::Port': + continue + if elmt.resource_name not in res_name: + continue + parent = getattr(elmt, 'parent_resource', None) + releations[parent] = elmt.resource_name + + details = dict() + for (parent_name, resource_name) in releations.items(): + for elmt in stack_resource: + if parent_name is None: + detail_stack = self._get_heat_resource_info( + stack_id, resource_name=resource_name) + elif parent_name != elmt.resource_name: + continue + else: + detail_stack = self._get_heat_resource_info( + elmt.physical_resource_id, resource_name=resource_name) + details[resource_name] = detail_stack + + ans_list = list() + for detail in details.values(): + ans_list.append(detail.attributes['fixed_ips']) + + return ans_list + def test_create_show_delete_vnf_instance(self): """Create, show and delete a vnf instance.""" @@ -881,3 +1041,100 @@ class VnfLcmTest(base.BaseTackerTest): self._terminate_vnf_instance(vnf_instance['id'], terminate_req_body) self._delete_vnf_instance(vnf_instance['id']) + + def test_inst_chgextconn_term(self): + """Test change external vnf connectivity. + + This test will instantiate vnf with external virtual link and + change the IP address on virtual link. + """ + + # Create vnf instance + vnf_instance_name = "vnf_with_ext_vl_and_ext_managed_vl-%s" % \ + uuidutils.generate_uuid() + vnf_instance_description = "vnf_with_ext_vl_and_ext_managed_vl" + resp, vnf_instance = self._create_vnf_instance(self.vnfd_id_3, + vnf_instance_name=vnf_instance_name, + vnf_instance_description=vnf_instance_description) + + self.assertIsNotNone(vnf_instance['id']) + self.assertEqual(201, resp.status_code) + + neutron_client = self.neutronclient() + net = neutron_client.list_networks() + networks = {} + for network in net['networks']: + networks[network['name']] = network['id'] + subnet_list = neutron_client.list_subnets() + subnets = {} + for subnet in subnet_list['subnets']: + subnets[subnet['name']] = subnet['id'] + + net1_id = networks.get('net1') + if not net1_id: + self.fail("net1 network is not available") + + net0_id = networks.get('net0') + if not net0_id: + self.fail("net0 network is not available") + + net_mgmt_id = networks.get('net_mgmt') + if not net_mgmt_id: + self.fail("net_mgmt network is not available") + + subnet_mgmt_id = subnets.get('subnet_mgmt') + if not subnet_mgmt_id: + self.fail("subnet_mgmt subnet is not available") + + ext_managed_vl = get_ext_managed_virtual_link("net1", "VL3", + net1_id) + + network_uuid = self._create_network(neutron_client, + "external_network") + subnet_uuid = self._create_subnet(neutron_client, network_uuid) + + # Instantiate vnf + ext_vl = get_external_virtual_links( + net0_id, net_mgmt_id, None, + fixed_addresses=['192.168.120.100'], + subnet_id=subnet_mgmt_id) + + request_body = self._instantiate_vnf_request("simple", + vim_id=self.vim_id, ext_vl=ext_vl, ext_managed_vl=ext_managed_vl) + + self._instantiate_vnf_instance(vnf_instance['id'], request_body) + + vnf_instance = self._show_vnf_instance(vnf_instance['id']) + vdu_count = len(vnf_instance['instantiatedVnfInfo'] + ['vnfcResourceInfo']) + self.assertEqual(1, vdu_count) + + # Change external vnf connectivity + changed_ext_vl = get_external_virtual_links( + net0_id, network_uuid, None, + fixed_addresses=['22.22.0.100'], + subnet_id=subnet_uuid) + change_ext_conn_req_body = self._change_ext_conn_vnf_request( + vim_id=self.vim_id, ext_vl=changed_ext_vl) + before_fixed_ips = self._get_fixed_ips(vnf_instance, request_body) + self._change_ext_conn_vnf_instance( + vnf_instance, change_ext_conn_req_body) + after_fixed_ips = self._get_fixed_ips(vnf_instance, request_body) + self.assertNotEqual(before_fixed_ips, after_fixed_ips) + + # Get op-occs + resp, op_occs_info = self._list_op_occs() + self._assert_occ_list(resp, op_occs_info) + + # Wait for operation state completed + time.sleep(10) + + # Terminate vnf gracefully with graceful timeout set to 60 + terminate_req_body = { + "terminationType": fields.VnfInstanceTerminationType.GRACEFUL, + 'gracefulTerminationTimeout': 60 + } + + self._terminate_vnf_instance(vnf_instance['id'], terminate_req_body) + + self._delete_vnf_instance(vnf_instance['id']) diff --git a/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py b/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py index 6dd03efc6..9d9973436 100644 --- a/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py +++ b/tacker/tests/functional/sol/vnflcm/test_vnf_instance_with_user_data.py @@ -1847,27 +1847,6 @@ class VnfLcmWithUserDataTest(vnflcm_base.BaseVnfLcmTest): self.assertIsNotNone(_links.get('grant')) self.assertIsNotNone(_links.get('grant').get('href')) - def _assert_occ_list(self, resp, op_occs_list): - self.assertEqual(200, resp.status_code) - - # Only check required parameters. - for op_occs_info in op_occs_list: - self.assertIsNotNone(op_occs_info.get('id')) - self.assertIsNotNone(op_occs_info.get('operationState')) - self.assertIsNotNone(op_occs_info.get('stateEnteredTime')) - self.assertIsNotNone(op_occs_info.get('vnfInstanceId')) - self.assertIsNotNone(op_occs_info.get('operation')) - self.assertIsNotNone(op_occs_info.get('isAutomaticInvocation')) - self.assertIsNotNone(op_occs_info.get('isCancelPending')) - - _links = op_occs_info.get('_links') - self.assertIsNotNone(_links.get('self')) - self.assertIsNotNone(_links.get('self').get('href')) - self.assertIsNotNone(_links.get('vnfInstance')) - self.assertIsNotNone(_links.get('vnfInstance').get('href')) - self.assertIsNotNone(_links.get('grant')) - self.assertIsNotNone(_links.get('grant').get('href')) - def _assert_fail_vnf_response(self, fail_response): # Only check parameters with cardinality = 1 diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py index d80413aff..8c4ddd638 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/fixture_data/fixture_data_utils.py @@ -558,6 +558,134 @@ def get_vnf_attribute_dict(): return vnf_attribute_dict +def get_stack_template(): + stack_template = { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'VDU1': { + 'type': 'OS::Nova::Server', + 'properties': { + 'name': 'VDU1', + 'flavor': 'm1.tiny', + 'image': 'None', + 'networks': [ + { + 'port': {'get_resource': 'VDU1_CP1'}, + }, { + 'port': {'get_resource': 'VDU1_CP2'}, + } + ], + }, + }, + 'VDU1_CP1': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': "nw-resource-id-1", + 'fixed_ips': [{ + 'ip_address': '10.10.0.1', + }], + }, + }, + 'VDU1_CP2': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': "nw-resource-id-1", + 'fixed_ips': [{ + 'ip_address': '10.10.0.2', + 'subnet': 'subnet-id-2', + }], + }, + }, + }, + } + + return stack_template + + +def get_stack_nested_template(): + stack_nested_template = { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'VDU2': { + 'type': 'OS::Nova::Server', + 'properties': { + 'name': 'VDU2', + 'flavor': 'm1.tiny', + 'image': 'cirros-0.4.0-x86_64-disk', + 'networks': [ + { + 'port': {'get_resource': 'VDU2_CP1'}, + }, { + 'port': {'get_resource': 'VDU2_CP2'}, + } + ], + }, + }, + 'VDU2_CP1': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': 'nw-resource-id-2', + 'fixed_ips': [{ + 'subnet': 'subnet-id-2', + }], + }, + }, + 'VDU2_CP2': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': 'nw-resource-id-2', + }, + }, + }, + } + + return stack_nested_template + + +def get_expected_update_resource_property_calls(): + calls = { + 'VDU1_CP1': { + 'resource_types': ['OS::Neutron::Port'], + 'network': 'nw-resource-id-1', + 'fixed_ips': [{ + 'ip_address': '20.0.0.1' + }], + }, + 'VDU1_CP2': { + 'resource_types': ['OS::Neutron::Port'], + 'network': 'nw-resource-id-1', + 'fixed_ips': [{ + 'ip_address': '30.0.0.2', + 'subnet': 'changed-subnet-id-1', + }], + }, + 'VDU1_CP3': { + 'resource_types': ['OS::Neutron::Port'], + 'network': 'nw-resource-id-1', + 'fixed_ips': [{ + 'ip_address': '10.0.0.1' + }], + }, + 'VDU2_CP1': { + 'resource_types': ['OS::Neutron::Port'], + 'network': 'changed-nw-resource-id-2', + 'fixed_ips': [{ + 'subnet': 'changed-subnet-id-2', + }], + }, + 'VDU2_CP2': { + 'resource_types': ['OS::Neutron::Port'], + 'network': 'changed-nw-resource-id-2', + 'fixed_ips': None, + }, + } + return calls + + def get_lcm_op_occs_object(operation="INSTANTIATE", error_point=0): vnf_lcm_op_occs = objects.VnfLcmOpOcc( diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py index 3c90373bb..7b9c5145c 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_openstack_driver.py @@ -2597,19 +2597,16 @@ class TestOpenStack(base.FixturedTestCase): self.assertEqual('30435eb8-1472-4cbc-abbe-00b395165ce7', grp_id) @mock.patch('tacker.common.clients.OpenstackClients') - @mock.patch('tacker.vnflcm.utils.get_base_nest_hot_dict') - def test_change_ext_conn_vnf(self, - mock_get_base_hot_dict, - mock_mock_OpenstackClients_heat): + @mock.patch('tacker.vnfm.infra_drivers.openstack.update_template.' + 'HOTUpdater') + def test_change_ext_conn_vnf_with_userdata(self, + mock_hot_updater, + mock_OpenstackClients_heat): inst_vnf_info = fd_utils.get_vnf_instantiated_info() vnf_instance = fd_utils.get_vnf_instance_object( instantiated_vnf_info=inst_vnf_info) - nested_hot_dict = {'parameters': {'vnf': 'test'}} - mock_get_base_hot_dict.return_value = \ - self._read_file(), nested_hot_dict - vnf_dict['vnfd'] = fd_utils.get_vnfd_dict() vnf_dict['attributes'] = fd_utils.get_vnf_attribute_dict() @@ -2624,6 +2621,38 @@ class TestOpenStack(base.FixturedTestCase): str(fd_utils.get_expect_stack_param()), vnf_dict['attributes']['stack_param']) + @mock.patch('tacker.common.clients.OpenstackClients') + @mock.patch('tacker.vnfm.infra_drivers.openstack.update_template' + '.HOTUpdater') + def test_change_ext_conn_vnf_without_userdata(self, + mock_hot_updater, + mock_OpenstackClients_heat): + inst_vnf_info = fd_utils.get_vnf_instantiated_info() + + vnf_instance = fd_utils.get_vnf_instance_object( + instantiated_vnf_info=inst_vnf_info) + + vnf_dict['vnfd'] = fd_utils.get_vnfd_dict() + vnf_dict['attributes'] = {} + + vim_connection_info = fd_utils.get_vim_connection_info_object() + change_ext_conn_request = fd_utils.get_change_ext_conn_request() + + hot_instance = mock_hot_updater.return_value + hot_instance.template = fd_utils.get_stack_template() + hot_instance.nested_templates = { + 'VDU2.yaml': fd_utils.get_stack_nested_template(), + } + + self.openstack.change_ext_conn_vnf( + self.context, vnf_instance, vnf_dict, + vim_connection_info, change_ext_conn_request) + hot_instance.get_templates_from_stack.assert_called_once() + for resource, args in (fd_utils. + get_expected_update_resource_property_calls().items()): + hot_instance.update_resource_property.assert_any_call( + resource, **args) + def test_change_ext_conn_vnf_wait(self): inst_vnf_info = fd_utils.get_vnf_instantiated_info() diff --git a/tacker/tests/unit/vnfm/infra_drivers/openstack/test_update_template.py b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_update_template.py new file mode 100644 index 000000000..fc4e39f4d --- /dev/null +++ b/tacker/tests/unit/vnfm/infra_drivers/openstack/test_update_template.py @@ -0,0 +1,227 @@ +# 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. + +from unittest import mock + +from heatclient.v1 import resources +from tacker.tests.unit import base +from tacker.tests import uuidsentinel +from tacker.vnfm.infra_drivers.openstack.heat_client import HeatClient +from tacker.vnfm.infra_drivers.openstack.update_template import HOTUpdater + + +class TestUpdateTemplate(base.TestCase): + + def setUp(self): + super(TestUpdateTemplate, self).setUp() + self.maxDiff = None + self._mock_heatclient() + self.stack_id = uuidsentinel.stack_id + self.hot_updater = HOTUpdater(self.heatclient) + + def _mock_heatclient(self): + self.heatclient = mock.Mock(spec=HeatClient) + self.heatclient.stacks = mock.Mock() + self.heatclient.stacks.template.side_effect = [ + self._get_template(), + self._get_intermediate_template1(), + self._get_nested_template1(), + self._get_intermediate_template2(), + self._get_nested_template2() + ] + self.heatclient.resource_get_list.return_value = \ + self._get_stack_resources() + + def test_get_templates_from_stack(self): + self.hot_updater.get_templates_from_stack(self.stack_id) + + self.assertDictEqual(self._get_template(), self.hot_updater.template) + self.assertDictEqual( + {'VDU1.yaml': self._get_nested_template1(), + 'VDU2.yaml': self._get_nested_template2()}, + self.hot_updater.nested_templates) + + def test_update_resource_property(self): + self.hot_updater.get_templates_from_stack(self.stack_id) + + # Test pattern 1: Change network and CP1(DHCP) address to fixed. + self.hot_updater.update_resource_property( + 'CP1', ['OS::Neutron::Port'], + network=uuidsentinel.network_id_cp1_changed, + fixed_ips=[{'ip_address': '20.20.0.100'}]) + expected = { + 'network': uuidsentinel.network_id_cp1_changed, + 'fixed_ips': [{'ip_address': '20.20.0.100'}] + } + self.assertEqual( + expected, + self.hot_updater.template['resources']['CP1']['properties']) + + # Test pattern 2: Change network and fixed address. + self.hot_updater.update_resource_property( + 'CP2', ['OS::Neutron::Port'], + network=uuidsentinel.network_id_cp2_changed, + fixed_ips=[{ + 'ip_address': '20.20.0.200', + 'subnet': uuidsentinel.subnet_id_cp2_changed + }]) + expected = { + 'network': uuidsentinel.network_id_cp2_changed, + 'fixed_ips': [{ + 'ip_address': '20.20.0.200', + 'subnet': uuidsentinel.subnet_id_cp2_changed, + }] + } + self.assertEqual( + expected, + self.hot_updater.template['resources']['CP2']['properties']) + + # Test Pattern 3: Delete fixed_ips property. + self.hot_updater.update_resource_property( + 'CP3', ['OS::Neutron::Port'], + network=uuidsentinel.network_id_cp3_changed, + fixed_ips=None) + self.assertIsNone(self.hot_updater.nested_templates[ + 'VDU1.yaml']['resources']['CP3']['properties'].get('fixed_ips')) + + def test_update_resource_property_not_found(self): + self.hot_updater.get_templates_from_stack(self.stack_id) + + # Test Pattern 4: Resource does not exist. + self.hot_updater.update_resource_property( + 'CPX', ['OS::Neutron::Port'], + network=uuidsentinel.network_id_cpx, + fixed_ips=[{'ip_address': '20.20.0.100'}]) + self.assertEqual(self._get_template(), self.hot_updater.template) + + # Test Pattern 5: Resource type does not exist. + self.hot_updater.update_resource_property( + 'CP1', ['OS::Sahara::Cluster'], + network=uuidsentinel.network_id_cp1_changed, + fixed_ips=[{'ip_address': '20.20.0.100'}]) + self.assertEqual(self._get_template(), self.hot_updater.template) + + # Test Pattern 6: Resource doess not have properties. + self.hot_updater.update_resource_property( + 'CP5', ['OS::Neutron::Port'], + network=uuidsentinel.network_id_cp5, + fixed_ips=[{'ip_address': '20.20.0.100'}]) + self.assertEqual(self._get_template(), self.hot_updater.template) + + def _get_template(self): + return { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'CP1': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': uuidsentinel.network_id_cp1, + }, + }, + 'CP2': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': uuidsentinel.network_id_cp2, + 'fixed_ips': [{ + 'ip_address': '10.10.0.200', + 'subnet': uuidsentinel.subnet_id_cp2, + }], + }, + }, + 'CP5': { + 'type': 'OS::Neutron::Port', + }, + }, + } + + def _get_intermediate_template1(self): + return { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'xxxxxxxx': { + 'type': 'VDU1.yaml', + }, + }, + } + + def _get_intermediate_template2(self): + return { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'yyyyyyyy': { + 'type': 'VDU2.yaml', + }, + }, + } + + def _get_nested_template1(self): + return { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'CP3': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': uuidsentinel.network_id_cp3, + 'fixed_ips': [{ + 'subnet': uuidsentinel.subnet_id_cp3, + }], + }, + }, + }, + } + + def _get_nested_template2(self): + return { + 'heat_template_version': '2013-05-23', + 'description': 'Simple deployment flavour for Sample VNF', + 'parameters': {}, + 'resources': { + 'CP4': { + 'type': 'OS::Neutron::Port', + 'properties': { + 'network': uuidsentinel.network_id_cp4, + 'fixed_ips': [{ + 'subnet': uuidsentinel.subnet_id_cp4, + }], + }, + }, + }, + } + + def _get_stack_resources(self): + def _create_resource(resource_name, resource_type): + return resources.Resource(None, { + 'resource_name': resource_name, + 'resource_type': resource_type, + 'resource_status': 'CREATE_COMPLETE', + 'physical_resource_id': uuidsentinel.uuid, + }) + + data = [ + ('VDU1_scale_group', 'OS::Heat::AutoScalingGroup'), + ('xxxxxxxx', 'VDU1.yaml'), + ('VDU2_scale_group', 'OS::Heat::AutoScalingGroup'), + ('yyyyyyyy', 'VDU2.yaml'), + ('CP1', 'OS::Neutron::Port'), + ('CP2', 'OS::Neutron::Port'), + ('CP3', 'OS::Neutron::Port'), + ('CP4', 'OS::Neutron::Port') + ] + return [_create_resource(row[0], row[1]) for row in data] diff --git a/tacker/vnfm/infra_drivers/openstack/openstack.py b/tacker/vnfm/infra_drivers/openstack/openstack.py index cb3a1342e..17aaa73ab 100644 --- a/tacker/vnfm/infra_drivers/openstack/openstack.py +++ b/tacker/vnfm/infra_drivers/openstack/openstack.py @@ -48,6 +48,7 @@ from tacker.vnfm.infra_drivers.openstack import constants as infra_cnst from tacker.vnfm.infra_drivers.openstack import glance_client as gc from tacker.vnfm.infra_drivers.openstack import heat_client as hc from tacker.vnfm.infra_drivers.openstack import translate_template +from tacker.vnfm.infra_drivers.openstack import update_template as ut from tacker.vnfm.infra_drivers.openstack import vdu from tacker.vnfm.infra_drivers import scale_driver from tacker.vnfm.lcm_user_data.constants import USER_DATA_TIMEOUT @@ -57,6 +58,7 @@ eventlet.monkey_patch(time=True) SCALING_GROUP_RESOURCE = "OS::Heat::AutoScalingGroup" NOVA_SERVER_RESOURCE = "OS::Nova::Server" +NEUTRON_PORT_RESOURCE = "OS::Neutron::Port" VNF_PACKAGE_HOT_DIR = 'Files' @@ -1589,6 +1591,7 @@ class OpenStack(abstract_driver.VnfAbstractDriver, ip_addr, vnfc_rsc.id) raise exceptions.InvalidIpAddr( id=vnfc_rsc.id) + ip_addr = fixed_ip.get('ip_address') ip_addresses.addresses.append(ip_addr) ip_addresses.subnet_id = fixed_ip.get( 'subnet_id') @@ -1607,7 +1610,7 @@ class OpenStack(abstract_driver.VnfAbstractDriver, resource.resource_id =\ rsc_info.physical_resource_id resource.vim_level_resource_type =\ - 'OS::Neutron::Port' + NEUTRON_PORT_RESOURCE if not vl.vnf_link_ports: vl.vnf_link_ports = [] link_port_info = objects.\ @@ -2091,14 +2094,61 @@ class OpenStack(abstract_driver.VnfAbstractDriver, def change_ext_conn_vnf(self, context, vnf_instance, vnf_dict, vim_connection_info, change_ext_conn_req): - base_hot_dict, nested_hot_dict = \ - vnflcm_utils.get_base_nest_hot_dict( - context, - vnf_instance.instantiated_vnf_info.flavour_id, - vnf_instance.vnfd_id) - stack_param = yaml.safe_load( - vnf_dict['attributes']['stack_param']) + access_info = vim_connection_info.access_info + heatclient = hc.HeatClient(access_info, + region_name=access_info.get('region')) + hot_updater = ut.HOTUpdater(heatclient) + hot_updater.get_templates_from_stack( + vnf_instance.instantiated_vnf_info.instance_id) + + if 'stack_param' in vnf_dict['attributes']: + LOG.debug('Target VNF instantiated with userdata.') + self._change_ext_conn_vnf_with_userdata( + context, + hot_updater, + vnf_instance, + vnf_dict, + vim_connection_info, + change_ext_conn_req) + else: + LOG.debug('Target VNF instantiated with ' + 'translating heat-template.') + self._change_ext_conn_vnf_with_tosca( + context, + hot_updater, + vnf_instance, + vnf_dict, + vim_connection_info, + change_ext_conn_req) + + def _get_fixed_ips_from_ip_addr(self, ip_addr): + fixed_ips = dict() + updated_fixed_ips = [] + if ip_addr.fixed_addresses: + for address in ip_addr.fixed_addresses: + fixed_ips = dict(ip_address=address) + if ip_addr.subnet_id: + fixed_ips.update(dict(subnet=ip_addr.subnet_id)) + updated_fixed_ips.append(fixed_ips) + elif ip_addr.num_dynamic_addresses > 0: + if ip_addr.subnet_id: + fixed_ips.update(dict(subnet=ip_addr.subnet_id)) + updated_fixed_ips.append(fixed_ips) + else: + updated_fixed_ips = None + + return updated_fixed_ips + + def _change_ext_conn_vnf_with_userdata( + self, + context, + hot_updater, + vnf_instance, + vnf_dict, + vim_connection_info, + change_ext_conn_req): + stack_param = yaml.safe_load(vnf_dict['attributes']['stack_param']) LOG.debug('before stack_param: {}'.format(stack_param)) cp_param = stack_param['nfv']['CP'] @@ -2120,34 +2170,65 @@ class OpenStack(abstract_driver.VnfAbstractDriver, # and ip_addresses cannot get, do nothing continue - fixed_ips = dict() - updated_fixed_ips = [] - if ip_addr.fixed_addresses: - for address in ip_addr.fixed_addresses: - fixed_ips = dict(ip_address=address) - if ip_addr.subnet_id: - fixed_ips.update(dict(subnet=ip_addr.subnet_id)) - updated_fixed_ips.append(fixed_ips) - elif ip_addr.num_dynamic_addresses > 0: - if ip_addr.subnet_id: - fixed_ips.update(dict(subnet=ip_addr.subnet_id)) - updated_fixed_ips.append(fixed_ips) + updated_fixed_ips = self._get_fixed_ips_from_ip_addr(ip_addr) if updated_fixed_ips: cp_param[cpd_id].update( dict(fixed_ips=updated_fixed_ips)) - LOG.debug('after stack_param: {}'.format(stack_param)) - access_info = vim_connection_info.access_info - heatclient = hc.HeatClient(access_info, - region_name=access_info.get('region')) - # Update heat-stack with BaseHOT and parameters self._update_stack_with_user_data( - heatclient, vnf_instance, base_hot_dict, nested_hot_dict, - stack_param, vnf_instance.instantiated_vnf_info.instance_id) + hot_updater.heatclient, + vnf_instance, + hot_updater.template, + hot_updater.nested_templates, + stack_param, + vnf_instance.instantiated_vnf_info.instance_id) vnf_dict['attributes'].update({'stack_param': str(stack_param)}) + def _change_ext_conn_vnf_with_tosca( + self, + context, + hot_updater, + vnf_instance, + vnf_dict, + vim_connection_info, + change_ext_conn_req): + for ext_virtual_link in change_ext_conn_req.ext_virtual_links: + for ext_cp in ext_virtual_link.ext_cps: + cpd_id = ext_cp.cpd_id + + try: + ip_addr = ext_cp.cp_config[0].cp_protocol_data[0].\ + ip_over_ethernet.ip_addresses[0] + except IndexError: + # If the element under ext_cp does not exist, + # and ip_addresses cannot get, do nothing + continue + + updated_fixed_ips = self._get_fixed_ips_from_ip_addr(ip_addr) + hot_updater.update_resource_property( + cpd_id, + resource_types=[NEUTRON_PORT_RESOURCE], + network=ext_virtual_link.resource_id, + fixed_ips=updated_fixed_ips) + + # Set parameters to update heat-stack + update_parameters = { + 'template': self._format_base_hot(hot_updater.template), + } + if hot_updater.nested_templates: + files_dict = dict() + for name, value in hot_updater.nested_templates.items(): + files_dict[name] = self._format_base_hot(value) + update_parameters['files'] = files_dict + + # Update heat-stack + self._update_stack( + hot_updater.heatclient, + vnf_instance.instantiated_vnf_info.instance_id, + update_parameters) + @log.log def change_ext_conn_vnf_wait(self, context, vnf_instance, vim_connection_info): diff --git a/tacker/vnfm/infra_drivers/openstack/update_template.py b/tacker/vnfm/infra_drivers/openstack/update_template.py new file mode 100644 index 000000000..9c1b75010 --- /dev/null +++ b/tacker/vnfm/infra_drivers/openstack/update_template.py @@ -0,0 +1,98 @@ +# 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. + +from oslo_log import log as logging + +from tacker.common import log + +LOG = logging.getLogger(__name__) + + +class HOTUpdater(object): + """Update HOT template.""" + + def __init__(self, heatclient): + self.heatclient = heatclient + self.template = {} + self.nested_templates = dict() + + @log.log + def get_templates_from_stack(self, stack_id): + """Get template information from the stack. + + Get the template from stack specified by stack_id, + if stack has scalable resource, get the its child + template. + """ + + def _get_resource(name, resources): + for resource in resources: + if resource.resource_name == name: + return resource + + self.template = self.heatclient.stacks.template(stack_id) + LOG.debug('got main template for stack({}). template={}'.format( + stack_id, self.template)) + + stack_resources = self.heatclient.resource_get_list(stack_id, + nested_depth=2) + for resource in stack_resources: + if resource.resource_type == 'OS::Heat::AutoScalingGroup': + intermediate_template = self.heatclient.stacks.template( + resource.physical_resource_id) + + for resource_id in intermediate_template['resources'].keys(): + corresponding_resource = _get_resource(resource_id, + stack_resources) + nested_template = self.heatclient.stacks.template( + corresponding_resource.physical_resource_id) + LOG.debug('got nested template for stack({}). template={}' + .format(corresponding_resource.physical_resource_id, + nested_template)) + if nested_template: + self.nested_templates[ + corresponding_resource.resource_type] = nested_template + + @log.log + def update_resource_property(self, + resource_id, + resource_types=[], + **kwargs): + """Update attributes of resource properties. + + Get the resource information from template's resources section, + and update properties using kwargs information. + If resource type does not include in resource_types, nothing to do. + """ + + def _update(template, resource_id, resource_types, kwargs): + resource = template.get('resources', {}).get(resource_id) + if not resource: + return + if resource.get('type', {}) not in resource_types: + return + + resource_properties = resource.get('properties', {}) + if not resource_properties: + return + + for key, value in kwargs.items(): + if value is not None: + resource_properties.update({key: value}) + elif resource_properties.get(key): + del resource_properties[key] + + _update(self.template, resource_id, resource_types, kwargs) + + for value in self.nested_templates.values(): + nested_template = value + _update(nested_template, resource_id, resource_types, kwargs)