diff --git a/neutron/agent/ovn/metadata/agent.py b/neutron/agent/ovn/metadata/agent.py index d40aa26f5af..b90fed0fdb9 100644 --- a/neutron/agent/ovn/metadata/agent.py +++ b/neutron/agent/ovn/metadata/agent.py @@ -18,6 +18,7 @@ import re import threading import uuid +import netaddr from neutron_lib import constants as n_const from oslo_concurrency import lockutils from oslo_log import log @@ -46,7 +47,8 @@ MAC_PATTERN = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I) OVN_VIF_PORT_TYPES = ("", "external", ) MetadataPortInfo = collections.namedtuple('MetadataPortInfo', ['mac', - 'ip_addresses']) + 'ip_addresses', + 'logical_port']) OVN_METADATA_UUID_NAMESPACE = uuid.UUID('d34bf9f6-da32-4871-9af8-15a4626b41ab') @@ -87,7 +89,7 @@ class PortBindingChassisEvent(row_event.RowEvent): net_name = ovn_utils.get_network_name_from_datapath( row.datapath) LOG.info(self.LOG_MSG, row.logical_port, net_name) - self.agent.update_datapath(str(row.datapath.uuid), net_name) + self.agent.provision_datapath(row.datapath) except ConfigException: # We're now in the reader lock mode, we need to exit the # context and then use writer lock @@ -315,11 +317,12 @@ class MetadataAgent(object): "br-int instead.") return 'br-int' - def get_networks(self): + def get_networks_datapaths(self): + """Return a set of datapath objects of the VIF ports on the current + chassis. + """ ports = self.sb_idl.get_ports_on_chassis(self.chassis) - return {(str(p.datapath.uuid), - ovn_utils.get_network_name_from_datapath(p.datapath)) - for p in self._vif_ports(ports)} + return set(p.datapath for p in self._vif_ports(ports)) @_sync_lock def sync(self): @@ -334,10 +337,10 @@ class MetadataAgent(object): system_namespaces = tuple( ns.decode('utf-8') if isinstance(ns, bytes) else ns for ns in ip_lib.list_network_namespaces()) - nets = self.get_networks() + net_datapaths = self.get_networks_datapaths() metadata_namespaces = [ - self._get_namespace_name(net[1]) - for net in nets + self._get_namespace_name(str(datapath.uuid)) + for datapath in net_datapaths ] unused_namespaces = [ns for ns in system_namespaces if ns.startswith(NS_PREFIX) and @@ -347,7 +350,8 @@ class MetadataAgent(object): # now that all obsolete namespaces are cleaned up, deploy required # networks - self.ensure_all_networks_provisioned(nets) + for datapath in net_datapaths: + self.provision_datapath(datapath) @staticmethod def _get_veth_name(datapath): @@ -395,25 +399,6 @@ class MetadataAgent(object): ip.garbage_collect_namespace() - def update_datapath(self, datapath, net_name): - """Update the metadata service for this datapath. - - This function will: - * Provision the namespace if it wasn't already in place. - * Update the namespace if it was already serving metadata (for example, - after binding/unbinding the first/last port of a subnet in our - chassis). - * Tear down the namespace if there are no more ports in our chassis - for this datapath. - """ - ports = self.sb_idl.get_ports_on_chassis(self.chassis) - datapath_ports = [p for p in self._vif_ports(ports) if - str(p.datapath.uuid) == datapath] - if datapath_ports: - self.provision_datapath(datapath, net_name) - else: - self.teardown_datapath(datapath, net_name) - def _ensure_datapath_checksum(self, namespace): """Ensure the correct checksum in the metadata packets in DPDK bridges @@ -433,45 +418,130 @@ class MetadataAgent(object): iptables_mgr.ipv4['mangle'].add_rule('POSTROUTING', rule, wrap=False) iptables_mgr.apply() - def provision_datapath(self, datapath, net_name): - """Provision the datapath so that it can serve metadata. + def _get_port_ips(self, port): + # Retrieve IPs from the port mac column which is in form + # [" ... "] + mac_field_attrs = port.mac[0].split() + ips = mac_field_attrs[1:] + if not ips: + LOG.debug("Port %s IP addresses were not retrieved from the " + "Port_Binding MAC column %s", port.uuid, mac_field_attrs) + return ips - This function will create the namespace and VETH pair if needed - and assign the IP addresses to the interface corresponding to the - metadata port of the network. It will also remove existing IP - addresses that are no longer needed. + def _active_subnets_cidrs(self, datapath_ports_ips, metadata_port_cidrs): + active_subnets_cidrs = set() + # Prepopulate a dictionary where each metadata_port_cidr(string) maps + # to its netaddr.IPNetwork object. This is so we dont have to + # reconstruct IPNetwork objects repeatedly in the for loop + metadata_cidrs_to_network_objects = { + metadata_port_cidr: netaddr.IPNetwork(metadata_port_cidr) + for metadata_port_cidr in metadata_port_cidrs + } + + for datapath_port_ip in datapath_ports_ips: + ip_obj = netaddr.IPAddress(datapath_port_ip) + for metadata_cidr, metadata_cidr_obj in \ + metadata_cidrs_to_network_objects.items(): + if ip_obj in metadata_cidr_obj: + active_subnets_cidrs.add(metadata_cidr) + break + return active_subnets_cidrs + + def _process_cidrs(self, current_namespace_cidrs, + datapath_ports_ips, metadata_port_subnet_cidrs): + active_subnets_cidrs = self._active_subnets_cidrs( + datapath_ports_ips, metadata_port_subnet_cidrs) + + cidrs_to_add = active_subnets_cidrs - current_namespace_cidrs + + if n_const.METADATA_CIDR not in current_namespace_cidrs: + cidrs_to_add.add(n_const.METADATA_CIDR) + else: + active_subnets_cidrs.add(n_const.METADATA_CIDR) + + cidrs_to_delete = current_namespace_cidrs - active_subnets_cidrs + + return cidrs_to_add, cidrs_to_delete + + def _get_provision_params(self, datapath): + """Performs datapath preprovision checks and returns paremeters + needed to provision namespace. + + Function will confirm that: + 1. Datapath metadata port has valid MAC and subnet CIDRs + 2. There are datapath port IPs + + If any of those rules are not valid the nemaspace for the + provided datapath will be tore down. + If successful, returns datapath's network name, ports IPs + and meta port info """ - LOG.debug("Provisioning metadata for network %s", net_name) - port = self.sb_idl.get_metadata_port_network(datapath) + net_name = ovn_utils.get_network_name_from_datapath(datapath) + datapath_uuid = str(datapath.uuid) + + metadata_port = self.sb_idl.get_metadata_port_network(datapath_uuid) # If there's no metadata port or it doesn't have a MAC or IP # addresses, then tear the namespace down if needed. This might happen # when there are no subnets yet created so metadata port doesn't have # an IP address. - if not (port and port.mac and - port.external_ids.get(ovn_const.OVN_CIDRS_EXT_ID_KEY, None)): + if not (metadata_port and metadata_port.mac and + metadata_port.external_ids.get( + ovn_const.OVN_CIDRS_EXT_ID_KEY, None)): LOG.debug("There is no metadata port for network %s or it has no " "MAC or IP addresses configured, tearing the namespace " "down if needed", net_name) - self.teardown_datapath(datapath, net_name) + self.teardown_datapath(datapath_uuid, net_name) return # First entry of the mac field must be the MAC address. - match = MAC_PATTERN.match(port.mac[0].split(' ')[0]) - # If it is not, we can't provision the namespace. Tear it down if - # needed and log the error. + match = MAC_PATTERN.match(metadata_port.mac[0].split(' ')[0]) if not match: LOG.error("Metadata port for network %s doesn't have a MAC " "address, tearing the namespace down if needed", net_name) - self.teardown_datapath(datapath) + self.teardown_datapath(datapath_uuid, net_name) return mac = match.group() ip_addresses = set( - port.external_ids[ovn_const.OVN_CIDRS_EXT_ID_KEY].split(' ')) - ip_addresses.add(n_const.METADATA_CIDR) - metadata_port = MetadataPortInfo(mac, ip_addresses) + metadata_port.external_ids[ + ovn_const.OVN_CIDRS_EXT_ID_KEY].split(' ')) + metadata_port_info = MetadataPortInfo(mac, ip_addresses, + metadata_port.logical_port) + chassis_ports = self.sb_idl.get_ports_on_chassis(self.chassis) + datapath_ports_ips = [] + for chassis_port in self._vif_ports(chassis_ports): + if str(chassis_port.datapath.uuid) == datapath_uuid: + datapath_ports_ips.extend(self._get_port_ips(chassis_port)) + + if not datapath_ports_ips: + LOG.debug("No valid VIF ports were found for network %s, " + "tearing the namespace down if needed", net_name) + self.teardown_datapath(datapath_uuid, net_name) + return + + return net_name, datapath_ports_ips, metadata_port_info + + def provision_datapath(self, datapath): + """Provision the datapath so that it can serve metadata. + + This function will create the namespace and VETH pair if needed + and assign the IP addresses to the interface corresponding to the + metadata port of the network. It will also remove existing IP from + the namespace if they are no longer needed. + + :param datapath: datapath object. + :return: The metadata namespace name for the datapath or None + if namespace was not provisioned + """ + + provision_params = self._get_provision_params(datapath) + if not provision_params: + return + net_name, datapath_ports_ips, metadata_port_info = provision_params + + LOG.info("Provisioning metadata for network %s", net_name) # Create the VETH pair if it's not created. Also the add_veth function # will create the namespace for us. namespace = self._get_namespace_name(net_name) @@ -496,22 +566,24 @@ class MetadataAgent(object): ip2.link.set_up() # Configure the MAC address. - ip2.link.set_address(metadata_port.mac) - dev_info = ip2.addr.list() + ip2.link.set_address(metadata_port_info.mac) - # Configure the IP addresses on the VETH pair and remove those - # that we no longer need. - current_cidrs = {dev['cidr'] for dev in dev_info} - for ipaddr in current_cidrs - metadata_port.ip_addresses: - ip2.addr.delete(ipaddr) - for ipaddr in metadata_port.ip_addresses - current_cidrs: + cidrs_to_add, cidrs_to_delete = self._process_cidrs( + {dev['cidr'] for dev in ip2.addr.list()}, + datapath_ports_ips, + metadata_port_info.ip_addresses + ) + # Delete any non active addresses from the network namespace + for cidr in cidrs_to_delete: + ip2.addr.delete(cidr) + for cidr in cidrs_to_add: # NOTE(dalvarez): metadata only works on IPv4. We're doing this # extra check here because it could be that the metadata port has # an IPv6 address if there's an IPv6 subnet with SLAAC in its # network. Neutron IPAM will autoallocate an IPv6 address for every # port in the network. - if utils.get_ip_version(ipaddr) == 4: - ip2.addr.add(ipaddr) + if utils.get_ip_version(cidr) == n_const.IP_VERSION_4: + ip2.addr.add(cidr) # Check that this port is not attached to any other OVS bridge. This # can happen when the OVN bridge changes (for example, during a @@ -536,7 +608,8 @@ class MetadataAgent(object): veth_name[0]).execute() self.ovs_idl.db_set( 'Interface', veth_name[0], - ('external_ids', {'iface-id': port.logical_port})).execute() + ('external_ids', {'iface-id': + metadata_port_info.logical_port})).execute() # Ensure the correct checksum in the metadata traffic. self._ensure_datapath_checksum(namespace) @@ -546,14 +619,3 @@ class MetadataAgent(object): self._process_monitor, namespace, n_const.METADATA_PORT, self.conf, bind_address=n_const.METADATA_V4_IP, network_id=net_name) - - def ensure_all_networks_provisioned(self, nets): - """Ensure that all requested datapaths are provisioned. - - This function will make sure that requested datapaths have their - namespaces, VETH pair and OVS ports created and metadata proxies are up - and running. - """ - # Make sure that all those datapaths are serving metadata - for datapath, net_name in nets: - self.provision_datapath(datapath, net_name) diff --git a/neutron/tests/unit/agent/ovn/metadata/test_agent.py b/neutron/tests/unit/agent/ovn/metadata/test_agent.py index 79357c1ffa8..8cc13aecdfa 100644 --- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py +++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py @@ -36,7 +36,15 @@ from neutron.tests import base OvnPortInfo = collections.namedtuple( 'OvnPortInfo', ['datapath', 'type', 'mac', 'external_ids', 'logical_port']) -DatapathInfo = collections.namedtuple('DatapathInfo', ['uuid', 'external_ids']) + + +class DatapathInfo: + def __init__(self, uuid, external_ids): + self.uuid = uuid + self.external_ids = external_ids + + def __hash__(self): + return hash(self.uuid) def makePort(datapath=None, type='', mac=None, external_ids=None, @@ -82,7 +90,7 @@ class TestMetadataAgent(base.BaseTestCase): def test_sync(self): with mock.patch.object( - self.agent, 'ensure_all_networks_provisioned') as enp,\ + self.agent, 'provision_datapath') as pdp,\ mock.patch.object( ip_lib, 'list_network_namespaces') as lnn,\ mock.patch.object( @@ -91,10 +99,13 @@ class TestMetadataAgent(base.BaseTestCase): self.agent.sync() - enp.assert_called_once_with({ - (p.datapath.uuid, p.datapath.uuid) - for p in self.ports - }) + pdp.assert_has_calls( + [ + mock.call(p.datapath) + for p in self.ports + ], + any_order=True + ) lnn.assert_called_once_with() tdp.assert_not_called() @@ -102,7 +113,7 @@ class TestMetadataAgent(base.BaseTestCase): def test_sync_teardown_namespace(self): """Test that sync tears down unneeded metadata namespaces.""" with mock.patch.object( - self.agent, 'ensure_all_networks_provisioned') as enp,\ + self.agent, 'provision_datapath') as pdp,\ mock.patch.object( ip_lib, 'list_network_namespaces') as lnn,\ mock.patch.object( @@ -112,57 +123,54 @@ class TestMetadataAgent(base.BaseTestCase): self.agent.sync() - enp.assert_called_once_with({ - (p.datapath.uuid, p.datapath.uuid) - for p in self.ports - }) + pdp.assert_has_calls( + [ + mock.call(p.datapath) + for p in self.ports + ], + any_order=True + ) lnn.assert_called_once_with() tdp.assert_called_once_with('3') - def test_get_networks(self): - """Test which networks are provisioned. - + def test_get_networks_datapaths(self): + """Test get_networks_datapaths returns only datapath objects for the + networks containing vif ports of type ''(blank) and 'external'. This test simulates that this chassis has the following ports: - * datapath '0': 1 port - * datapath '1': 2 ports - * datapath '2': 1 port + * datapath '1': 1 port type '' , 1 port 'external' and + 1 port 'unknown' + * datapath '2': 1 port type '' * datapath '3': 1 port with type 'external' - * datapath '5': 1 port with type 'unknown' + * datapath '4': 1 port with type 'unknown' - It is expected that only datapaths '0', '1' and '2' are scheduled for - provisioning. + It is expected that only datapaths '1', '2' and '3' are returned """ - self.ports.append(makePort(datapath=DatapathInfo(uuid='1', - external_ids={'name': 'neutron-1'}))) - self.ports.append(makePort(datapath=DatapathInfo(uuid='3', - external_ids={'name': 'neutron-3'}), type='external')) - self.ports.append(makePort(datapath=DatapathInfo(uuid='5', - external_ids={'name': 'neutron-5'}), type='unknown')) + datapath_1 = DatapathInfo(uuid='uuid1', + external_ids={'name': 'neutron-1'}) + datapath_2 = DatapathInfo(uuid='uuid2', + external_ids={'name': 'neutron-2'}) + datapath_3 = DatapathInfo(uuid='uuid3', + external_ids={'name': 'neutron-3'}) + datapath_4 = DatapathInfo(uuid='uuid4', + external_ids={'name': 'neutron-4'}) - expected_networks = {(str(i), str(i)) for i in range(0, 4)} - self.assertEqual(expected_networks, self.agent.get_networks()) + ports = [ + makePort(datapath_1, type=''), + makePort(datapath_1, type='external'), + makePort(datapath_1, type='unknown'), + makePort(datapath_2, type=''), + makePort(datapath_3, type='external'), + makePort(datapath_4, type='unknown') + ] - def test_update_datapath_provision(self): - self.ports.append(makePort(datapath=DatapathInfo(uuid='3', - external_ids={'name': 'neutron-3'}), type='external')) - - with mock.patch.object(self.agent, 'provision_datapath', - return_value=None) as pdp,\ - mock.patch.object(self.agent, 'teardown_datapath') as tdp: - self.agent.update_datapath('1', 'a') - self.agent.update_datapath('3', 'b') - expected_calls = [mock.call('1', 'a'), mock.call('3', 'b')] - pdp.assert_has_calls(expected_calls) - tdp.assert_not_called() - - def test_update_datapath_teardown(self): - with mock.patch.object(self.agent, 'provision_datapath', - return_value=None) as pdp,\ - mock.patch.object(self.agent, 'teardown_datapath') as tdp: - self.agent.update_datapath('5', 'a') - tdp.assert_called_once_with('5', 'a') - pdp.assert_not_called() + with mock.patch.object(self.agent.sb_idl, 'get_ports_on_chassis', + return_value=ports): + expected_datapaths = set([datapath_1, datapath_2, datapath_3]) + self.assertSetEqual( + expected_datapaths, + self.agent.get_networks_datapaths() + ) def test_teardown_datapath(self): """Test teardown datapath. @@ -191,6 +199,174 @@ class TestMetadataAgent(base.BaseTestCase): del_veth.assert_called_once_with('veth_0') garbage_collect.assert_called_once_with() + def test__process_cidrs_when_current_namespace_empty(self): + current_namespace_cidrs = set() + datapath_port_ips = ['10.0.0.2', '10.0.0.3', '10.0.1.5'] + metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] + + expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28', + n_const.METADATA_CIDR]) + expected_cidrs_to_delete = set() + + actual_result = self.agent._process_cidrs(current_namespace_cidrs, + datapath_port_ips, + metadaport_subnet_cidrs) + actual_cidrs_to_add, actual_cidrs_to_delete = actual_result + + self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) + self.assertSetEqual(actual_cidrs_to_delete, expected_cidrs_to_delete) + + def test__process_cidrs_when_current_namespace_only_contains_metadata_cidr( + self): + current_namespace_cidrs = set([n_const.METADATA_CIDR]) + datapath_port_ips = ['10.0.0.2', '10.0.0.3', '10.0.1.5'] + metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] + + expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28']) + expected_cidrs_to_delete = set() + + actual_result = self.agent._process_cidrs(current_namespace_cidrs, + datapath_port_ips, + metadaport_subnet_cidrs) + actual_cidrs_to_add, actual_cidrs_to_delete = actual_result + + self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) + self.assertSetEqual(actual_cidrs_to_delete, expected_cidrs_to_delete) + + def test__process_cidrs_when_current_namespace_contains_stale_cidr(self): + current_namespace_cidrs = set([n_const.METADATA_CIDR, '10.0.1.0/31']) + datapath_port_ips = ['10.0.0.2', '10.0.0.3', '10.0.1.5'] + metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] + + expected_cidrs_to_add = set(['10.0.0.0/30', '10.0.1.0/28']) + expected_cidrs_to_delete = set(['10.0.1.0/31']) + + actual_result = self.agent._process_cidrs(current_namespace_cidrs, + datapath_port_ips, + metadaport_subnet_cidrs) + actual_cidrs_to_add, actual_cidrs_to_delete = actual_result + + self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) + self.assertSetEqual(actual_cidrs_to_delete, expected_cidrs_to_delete) + + def test__process_cidrs_when_current_namespace_contains_mix_cidrs(self): + """Current namespace cidrs contains stale cidrs and it is missing + new required cidrs. + """ + current_namespace_cidrs = set([n_const.METADATA_CIDR, + '10.0.1.0/31', + '10.0.1.0/28']) + datapath_port_ips = ['10.0.0.2', '10.0.1.5'] + metadaport_subnet_cidrs = ['10.0.0.0/30', '10.0.1.0/28', '11.0.1.2/24'] + + expected_cidrs_to_add = set(['10.0.0.0/30']) + expected_cidrs_to_delete = set(['10.0.1.0/31']) + + actual_result = self.agent._process_cidrs(current_namespace_cidrs, + datapath_port_ips, + metadaport_subnet_cidrs) + actual_cidrs_to_add, actual_cidrs_to_delete = actual_result + + self.assertSetEqual(actual_cidrs_to_add, expected_cidrs_to_add) + self.assertSetEqual(actual_cidrs_to_delete, expected_cidrs_to_delete) + + def test__get_provision_params_returns_none_when_metadata_port_is_missing( + self): + """Should return None when there is no metadata port in datapath and + call teardown datapath. + """ + network_id = '1' + datapath = DatapathInfo(uuid='test123', + external_ids={'name': 'neutron-{}'.format(network_id)}) + + with mock.patch.object( + self.agent.sb_idl, 'get_metadata_port_network', + return_value=None),\ + mock.patch.object( + self.agent, 'teardown_datapath') as tdp: + self.assertIsNone(self.agent._get_provision_params(datapath)) + tdp.assert_called_once_with(datapath.uuid, network_id) + + def test__get_provision_params_returns_none_when_metadata_port_missing_mac( + self): + """Should return None when metadata port is missing MAC and + call teardown datapath. + """ + network_id = '1' + datapath = DatapathInfo(uuid='test123', + external_ids={'name': 'neutron-{}'.format(network_id)}) + metadadata_port = makePort(datapath, + mac=['NO_MAC_HERE 1.2.3.4'], + external_ids={'neutron:cidrs': + '10.204.0.10/29'}) + + with mock.patch.object( + self.agent.sb_idl, 'get_metadata_port_network', + return_value=metadadata_port),\ + mock.patch.object( + self.agent, 'teardown_datapath') as tdp: + self.assertIsNone(self.agent._get_provision_params(datapath)) + tdp.assert_called_once_with(datapath.uuid, network_id) + + def test__get_provision_params_returns_none_when_no_vif_ports(self): + """Should return None when there are no datapath ports with type + "external" or ""(blank) and call teardown datapath. + """ + network_id = '1' + datapath = DatapathInfo(uuid='test123', + external_ids={'name': 'neutron-{}'.format(network_id)}) + datapath_ports = [makePort(datapath, type='not_vif_type')] + metadadata_port = makePort(datapath, + mac=['fa:16:3e:22:65:18 1.2.3.4'], + external_ids={'neutron:cidrs': + '10.204.0.10/29'}) + + with mock.patch.object(self.agent.sb_idl, 'get_metadata_port_network', + return_value=metadadata_port),\ + mock.patch.object(self.agent.sb_idl, 'get_ports_on_chassis', + return_value=datapath_ports),\ + mock.patch.object(self.agent, 'teardown_datapath') as tdp: + self.assertIsNone(self.agent._get_provision_params(datapath)) + tdp.assert_called_once_with(datapath.uuid, network_id) + + def test__get_provision_params_returns_provision_parameters(self): + """The happy path when datapath has ports with "external" or ""(blank) + types and metadata port contains MAC and subnet CIDRs. + """ + network_id = '1' + port_ip = '1.2.3.4' + metada_port_mac = "fa:16:3e:22:65:18" + metada_port_subnet_cidr = "10.204.0.10/29" + metada_port_logical_port = "3b66c176-199b-48ec-8331-c1fd3f6e2b44" + + datapath = DatapathInfo(uuid='test123', + external_ids={'name': 'neutron-{}'.format(network_id)}) + datapath_ports = [makePort(datapath, + mac=['fa:16:3e:e7:ac {}'.format(port_ip)])] + metadadata_port = makePort(datapath, + mac=[ + '{} 10.204.0.1'.format(metada_port_mac) + ], + external_ids={'neutron:cidrs': + metada_port_subnet_cidr}, + logical_port=metada_port_logical_port) + + with mock.patch.object(self.agent.sb_idl, 'get_metadata_port_network', + return_value=metadadata_port),\ + mock.patch.object(self.agent.sb_idl, 'get_ports_on_chassis', + return_value=datapath_ports): + actual_params = self.agent._get_provision_params(datapath) + + net_name, datapath_port_ips, metadata_port_info = actual_params + + self.assertEqual(network_id, net_name) + self.assertListEqual([port_ip], datapath_port_ips) + self.assertEqual(metada_port_mac, metadata_port_info.mac) + self.assertSetEqual(set([metada_port_subnet_cidr]), + metadata_port_info.ip_addresses) + self.assertEqual(metada_port_logical_port, + metadata_port_info.logical_port) + def test_provision_datapath(self): """Test datapath provisioning. @@ -198,16 +374,21 @@ class TestMetadataAgent(base.BaseTestCase): namespace are created, that the interface is properly configured with the right IP addresses and that the metadata proxy is spawned. """ + net_name = '123' + metadaport_logical_port = '123-abc-456' + datapath_ports_ips = ['10.0.0.1', '10.0.0.2'] + metada_port_info = agent.MetadataPortInfo( + mac='aa:bb:cc:dd:ee:ff', + ip_addresses=['10.0.0.1/23', + '2001:470:9:1224:5595:dd51:6ba2:e788/64'], + logical_port=metadaport_logical_port + ) + provision_params = (net_name, datapath_ports_ips, metada_port_info,) + nemaspace_name = 'namespace' - metadata_port = makePort(mac=['aa:bb:cc:dd:ee:ff'], - external_ids={ - 'neutron:cidrs': '10.0.0.1/23 ' - '2001:470:9:1224:5595:dd51:6ba2:e788/64'}, - logical_port='port') - - with mock.patch.object(self.agent.sb_idl, - 'get_metadata_port_network', - return_value=metadata_port),\ + with mock.patch.object(self.agent, + '_get_provision_params', + return_value=provision_params),\ mock.patch.object( ip_lib, 'device_exists', return_value=False),\ mock.patch.object( @@ -215,7 +396,7 @@ class TestMetadataAgent(base.BaseTestCase): mock.patch.object(agent.MetadataAgent, '_get_veth_name', return_value=['veth_0', 'veth_1']),\ mock.patch.object(agent.MetadataAgent, '_get_namespace_name', - return_value='namespace'),\ + return_value=nemaspace_name),\ mock.patch.object(ip_link, 'set_up') as link_set_up,\ mock.patch.object(ip_link, 'set_address') as link_set_addr,\ mock.patch.object(ip_addr, 'list', return_value=[]),\ @@ -234,13 +415,14 @@ class TestMetadataAgent(base.BaseTestCase): # We need to assert that it was deleted first. self.agent.ovs_idl.list_br.return_value.execute.return_value = ( ['br-int', 'br-fake']) - self.agent.provision_datapath('1', '1') + self.agent.provision_datapath('fake_datapath') # Check that the port was deleted from br-fake self.agent.ovs_idl.del_port.assert_called_once_with( 'veth_0', bridge='br-fake', if_exists=True) # Check that the VETH pair is created - add_veth.assert_called_once_with('veth_0', 'veth_1', 'namespace') + add_veth.assert_called_once_with('veth_0', 'veth_1', + nemaspace_name) # Make sure that the two ends of the VETH pair have been set as up. self.assertEqual(2, link_set_up.call_count) link_set_addr.assert_called_once_with('aa:bb:cc:dd:ee:ff') @@ -248,7 +430,8 @@ class TestMetadataAgent(base.BaseTestCase): self.agent.ovs_idl.add_port.assert_called_once_with( 'br-int', 'veth_0') self.agent.ovs_idl.db_set.assert_called_once_with( - 'Interface', 'veth_0', ('external_ids', {'iface-id': 'port'})) + 'Interface', 'veth_0', + ('external_ids', {'iface-id': metadaport_logical_port})) # Check that the metadata port has the IP addresses properly # configured and that IPv6 address has been skipped. expected_calls = [mock.call('10.0.0.1/23'), @@ -257,9 +440,9 @@ class TestMetadataAgent(base.BaseTestCase): sorted(ip_addr_add.call_args_list)) # Check that metadata proxy has been spawned spawn_mdp.assert_called_once_with( - mock.ANY, 'namespace', 80, mock.ANY, - bind_address=n_const.METADATA_V4_IP, network_id='1') - mock_checksum.assert_called_once_with('namespace') + mock.ANY, nemaspace_name, 80, mock.ANY, + bind_address=n_const.METADATA_V4_IP, network_id=net_name) + mock_checksum.assert_called_once_with(nemaspace_name) def test__load_config(self): # Chassis name UUID formatted string. OVN bridge "br-ovn".