diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index d7aa1369836..fc477ea7f3a 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -857,6 +857,48 @@ def get_ovn_chassis_other_config(chassis): return chassis.external_ids +def get_subnets_address_scopes(context, subnets, fixed_ips, ml2_plugin): + """Returns the IPv4 and IPv6 address scopes of several subnets. + + The subnets hosted on the same network must be allocated from the same + subnet pool (from ``NetworkSubnetPoolAffinityError`` exception). That + applies per IP version (it means it is possible to have two subnet pools, + one for IPv4 and one for IPv6). + + :param context: neutron api request context + :param subnets: (list of dict) subnet dictionaries + :param fixed_ips: (list of dict) fixed IPs of several subnets (usually + belonging to a network but not mandatory) + :param ml2_plugin: (``Ml2Plugin``) ML2 plugin instance + :return: (tuple of 2 strings) IPv4 and IPv6 address scope IDs + """ + address4_scope_id, address6_scope_id = '', '' + if not subnets: + return address4_scope_id, address6_scope_id + + subnets_by_id = {subnet['id']: subnet for subnet in subnets} + for fixed_ip in fixed_ips: + subnet_id = fixed_ip.get('subnet_id') + subnet = subnets_by_id.get(subnet_id) + if not subnet or not subnet['subnetpool_id']: + continue + + try: + subnet_pool = ml2_plugin.get_subnetpool(context, + id=subnet['subnetpool_id']) + if subnet_pool['address_scope_id']: + if subnet_pool['ip_version'] == const.IP_VERSION_4: + address4_scope_id = subnet_pool['address_scope_id'] + else: + address6_scope_id = subnet_pool['address_scope_id'] + except n_exc.SubnetPoolNotFound: + # swallow the exception and just continue if the + # lookup failed + pass + + return address4_scope_id, address6_scope_id + + def sync_ha_chassis_group(context, network_id, nb_idl, sb_idl, txn): """Return the UUID of the HA Chassis Group or the HA Chassis Group cmd. diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py index 506d5482461..af68b63eb98 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py @@ -307,6 +307,9 @@ class OVNClient(object): ] subnets = self._plugin.get_subnets( context, filters={'id': subnet_ids}) + address4_scope_id, address6_scope_id = ( + utils.get_subnets_address_scopes(context, subnets, ip_subnets, + self._plugin)) if subnets: for ip in ip_subnets: ip_addr = ip['ip_address'] @@ -324,26 +327,6 @@ class OVNClient(object): ip_addr) continue - if subnet["subnetpool_id"]: - try: - subnet_pool = self._plugin.get_subnetpool( - context, id=subnet["subnetpool_id"] - ) - if subnet_pool["address_scope_id"]: - ip_version = subnet_pool["ip_version"] - if ip_version == const.IP_VERSION_4: - address4_scope_id = subnet_pool[ - "address_scope_id" - ] - elif ip_version == const.IP_VERSION_6: - address6_scope_id = subnet_pool[ - "address_scope_id" - ] - except n_exc.SubnetPoolNotFound: - # swallow the exception and just continue if the - # lookup failed - pass - cidrs += ' {}/{}'.format(ip['ip_address'], subnet['cidr'].split('/')[1]) diff --git a/neutron/tests/unit/common/ovn/test_utils.py b/neutron/tests/unit/common/ovn/test_utils.py index efc84c23007..0726f1a2928 100644 --- a/neutron/tests/unit/common/ovn/test_utils.py +++ b/neutron/tests/unit/common/ovn/test_utils.py @@ -23,6 +23,7 @@ import neutron_lib from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext from neutron_lib.api.definitions import portbindings from neutron_lib import constants as n_const +from neutron_lib import exceptions as n_exc from oslo_concurrency import processutils from oslo_config import cfg import testtools @@ -943,3 +944,62 @@ class TestOvsdbClientCommand(base.BaseTestCase): def test_run_bad_schema(self): with testtools.ExpectedException(KeyError): self.OvsdbClientTestCommand.run(['foo']) + + +class GetSubnetsAddressScopeTestCase(base.BaseTestCase): + + def setUp(self): + super().setUp() + self.ml2_plugin = mock.Mock() + + def test_no_subnets(self): + subnets = [] + fixed_ips = mock.ANY + address4, address6 = utils.get_subnets_address_scopes( + mock.ANY, subnets, fixed_ips, self.ml2_plugin) + self.assertEqual(('', ''), (address4, address6)) + + def test_no_subnetpool(self): + subnets = [ + {'id': 'subnet1', 'subnetpool_id': None}, + {'id': 'subnet2', 'subnetpool_id': None}, + ] + fixed_ips = [ + {'subnet_id': 'subnet1'}, + {'subnet_id': 'subnet2'}, + ] + address4, address6 = utils.get_subnets_address_scopes( + mock.ANY, subnets, fixed_ips, self.ml2_plugin) + self.assertEqual(('', ''), (address4, address6)) + + def test_no_address_scope(self): + subnets = [ + {'id': 'subnet1', 'subnetpool_id': 'pool_ipv4'}, + {'id': 'subnet2', 'subnetpool_id': 'pool_ipv6'}, + ] + fixed_ips = [ + {'subnet_id': 'subnet1'}, + {'subnet_id': 'subnet2'}, + ] + self.ml2_plugin.get_subnetpool.side_effect = n_exc.SubnetPoolNotFound( + subnetpool_id='snp') + address4, address6 = utils.get_subnets_address_scopes( + mock.ANY, subnets, fixed_ips, self.ml2_plugin) + self.assertEqual(('', ''), (address4, address6)) + + def test_address_scope(self): + subnets = [ + {'id': 'subnet1', 'subnetpool_id': 'pool_ipv4'}, + {'id': 'subnet2', 'subnetpool_id': 'pool_ipv6'}, + ] + fixed_ips = [ + {'subnet_id': 'subnet1'}, + {'subnet_id': 'subnet2'}, + ] + self.ml2_plugin.get_subnetpool.side_effect = [ + {'address_scope_id': 'scope4', 'ip_version': n_const.IP_VERSION_4}, + {'address_scope_id': 'scope6', 'ip_version': n_const.IP_VERSION_6}, + ] + address4, address6 = utils.get_subnets_address_scopes( + mock.ANY, subnets, fixed_ips, self.ml2_plugin) + self.assertEqual(('scope4', 'scope6'), (address4, address6))