From 2272b9be9515553c41a791b7c7b518696c4a2ebb Mon Sep 17 00:00:00 2001 From: Luca Czesla Date: Tue, 18 Oct 2022 08:33:01 +0000 Subject: [PATCH] Add address scope to the OVN LSP port registers To be able to filter on the address scope of the ports in the ovn-bgp-agent we add the address scope of the subnet pool to each LSP port in the northbound. Northd writes it to the southbound so the ovn-bgp-agent has access to it. The ovn-bgp-agent talks directly to the southbound to announce the networks via BGP that match the configured address scope. Change-Id: Ieb4cab11068747bd79868c6eb1778aab91447c30 Closes-Bug: #1996741 --- neutron/common/ovn/constants.py | 2 + .../ovn/mech_driver/ovsdb/maintenance.py | 41 ++++++++++ .../ovn/mech_driver/ovsdb/ovn_client.py | 78 +++++++++++++------ .../ovn/mech_driver/ovsdb/test_maintenance.py | 77 ++++++++++++++++++ .../ovn/mech_driver/test_mech_driver.py | 56 +++++++++++++ ...N-LSP-port-registers-1f45e34815c3896d.yaml | 6 ++ 6 files changed, 238 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/add-address-scope-to-the-OVN-LSP-port-registers-1f45e34815c3896d.yaml diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py index 2e2489c31c1..e188f7afb0f 100644 --- a/neutron/common/ovn/constants.py +++ b/neutron/common/ovn/constants.py @@ -34,6 +34,8 @@ OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id' OVN_GW_NETWORK_EXT_ID_KEY = 'neutron:gw_network_id' OVN_SUBNET_EXT_ID_KEY = 'neutron:subnet_id' OVN_SUBNET_EXT_IDS_KEY = 'neutron:subnet_ids' +OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY = 'neutron:subnet_pool_addr_scope4' +OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY = 'neutron:subnet_pool_addr_scope6' OVN_PHYSNET_EXT_ID_KEY = 'neutron:provnet-physical-network' OVN_NETTYPE_EXT_ID_KEY = 'neutron:provnet-network-type' OVN_SEGID_EXT_ID_KEY = 'neutron:provnet-segmentation-id' diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py index 6600633ae99..4a173a0e612 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py @@ -526,6 +526,47 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase): raise periodics.NeverAgain() + # TODO(czesla): Remove this in the A+4 cycle + # A static spacing value is used here, but this method will only run + # once per lock due to the use of periodics.NeverAgain(). + @periodics.periodic(spacing=600, run_immediately=True) + def check_port_has_address_scope(self): + if not self.has_lock: + return + + ports = self._nb_idl.db_find_rows( + "Logical_Switch_Port", ("type", "!=", ovn_const.LSP_TYPE_LOCALNET) + ).execute(check_error=True) + + context = n_context.get_admin_context() + with self._nb_idl.transaction(check_error=True) as txn: + for port in ports: + if ( + port.external_ids.get( + ovn_const.OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY + ) is None or + port.external_ids.get( + ovn_const.OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY + ) is None + ): + try: + port_neutron = self._ovn_client._plugin.get_port( + context, port.name + ) + + port_info, external_ids = ( + self._ovn_client.get_external_ids_from_port( + port_neutron) + ) + txn.add(self._nb_idl.set_lswitch_port( + port.name, external_ids=external_ids)) + except n_exc.PortNotFound: + # The sync function will fix this port + pass + except Exception: + LOG.exception('Failed to update port %s', port.name) + raise periodics.NeverAgain() + def _delete_default_ha_chassis_group(self, txn): # TODO(lucasgomes): Remove the deletion of the # HA_CHASSIS_GROUP_DEFAULT_NAME in the Y cycle. We no longer 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 ab41f9578e5..9237896390a 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 @@ -58,9 +58,23 @@ LOG = log.getLogger(__name__) OvnPortInfo = collections.namedtuple( - 'OvnPortInfo', ['type', 'options', 'addresses', 'port_security', - 'parent_name', 'tag', 'dhcpv4_options', 'dhcpv6_options', - 'cidrs', 'device_owner', 'security_group_ids']) + "OvnPortInfo", + [ + "type", + "options", + "addresses", + "port_security", + "parent_name", + "tag", + "dhcpv4_options", + "dhcpv6_options", + "cidrs", + "device_owner", + "security_group_ids", + "address4_scope_id", + "address6_scope_id", + ], +) GW_INFO = collections.namedtuple('GatewayInfo', ['network_id', 'subnet_id', @@ -264,6 +278,8 @@ class OVNClient(object): port_type = '' cidrs = '' + address4_scope_id = "" + address6_scope_id = "" dhcpv4_options = self._get_port_dhcp_options(port, const.IP_VERSION_4) dhcpv6_options = self._get_port_dhcp_options(port, const.IP_VERSION_6) if vtep_physical_switch: @@ -306,6 +322,26 @@ 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]) @@ -403,7 +439,9 @@ class OVNClient(object): sg_ids = ' '.join(utils.get_lsp_security_groups(port)) return OvnPortInfo(port_type, options, addresses, port_security, parent_name, tag, dhcpv4_options, dhcpv6_options, - cidrs.strip(), device_owner, sg_ids) + cidrs.strip(), device_owner, sg_ids, + address4_scope_id, address6_scope_id + ) def sync_ha_chassis_group(self, context, network_id, txn): """Return the UUID of the HA Chassis Group. @@ -493,10 +531,7 @@ class OVNClient(object): return (dhcpv4_options, dhcpv6_options) - def create_port(self, context, port): - if utils.is_lsp_ignored(port): - return - + def get_external_ids_from_port(self, port): port_info = self._get_port_options(port) external_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: port['name'], ovn_const.OVN_DEVID_EXT_ID_KEY: port['device_id'], @@ -504,6 +539,10 @@ class OVNClient(object): ovn_const.OVN_CIDRS_EXT_ID_KEY: port_info.cidrs, ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: port_info.device_owner, + ovn_const.OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY: + port_info.address4_scope_id, + ovn_const.OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY: + port_info.address6_scope_id, ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: utils.ovn_name(port['network_id']), ovn_const.OVN_SG_IDS_EXT_ID_KEY: @@ -511,6 +550,13 @@ class OVNClient(object): ovn_const.OVN_REV_NUM_EXT_ID_KEY: str( utils.get_revision_number( port, ovn_const.TYPE_PORTS))} + return port_info, external_ids + + def create_port(self, context, port): + if utils.is_lsp_ignored(port): + return + + port_info, external_ids = self.get_external_ids_from_port(port) lswitch_name = utils.ovn_name(port['network_id']) # It's possible to have a network created on one controller and then a @@ -616,20 +662,8 @@ class OVNClient(object): def update_port(self, context, port, port_object=None): if utils.is_lsp_ignored(port): return - port_info = self._get_port_options(port) - external_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: port['name'], - ovn_const.OVN_DEVID_EXT_ID_KEY: port['device_id'], - ovn_const.OVN_PROJID_EXT_ID_KEY: port['project_id'], - ovn_const.OVN_CIDRS_EXT_ID_KEY: port_info.cidrs, - ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: - port_info.device_owner, - ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: - utils.ovn_name(port['network_id']), - ovn_const.OVN_SG_IDS_EXT_ID_KEY: - port_info.security_group_ids, - ovn_const.OVN_REV_NUM_EXT_ID_KEY: str( - utils.get_revision_number( - port, ovn_const.TYPE_PORTS))} + + port_info, external_ids = self.get_external_ids_from_port(port) check_rev_cmd = self._nb_idl.check_revision_number( port['id'], port, ovn_const.TYPE_PORTS) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py index 508b5c1880f..9316412be43 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_maintenance.py @@ -30,6 +30,7 @@ from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_db_sync from neutron.tests.unit import fake_resources as fakes from neutron.tests.unit.plugins.ml2 import test_security_group as test_sg from neutron.tests.unit import testlib_api +from neutron_lib import exceptions as n_exc class TestSchemaAwarePeriodicsBase(testlib_api.SqlTestCaseLight): @@ -444,6 +445,82 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight, nb_idl.set_lswitch_port.assert_called_once_with( 'p1', ha_chassis_group=hcg0.uuid) + def test_check_port_has_address_scope(self): + self.fake_ovn_client.is_external_ports_supported.return_value = True + nb_idl = self.fake_ovn_client._nb_idl + + # Already has the address scope set but empty, nothing to do + lsp0 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={ + "uuid": "1f4323db-fb58-48e9-adae-6c6e833c581f", + "name": "lsp0", + "external_ids": { + constants.OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY: "", + constants.OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY: "", + }, + } + ) + + # address scope is missing, needs update + lsp1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={ + "uuid": "1f4323db-fb58-48e9-adae-6c6e833c581d", + "name": "lsp1", + "external_ids": {}, + } + ) + + # Already has the address scope set, nothing to do + lsp2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={ + "uuid": "1f4323db-fb58-48e9-adae-6c6e833c581a", + "name": "lsp2", + "external_ids": { + constants.OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY: "fakev4", + constants.OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY: "fakev6", + }, + } + ) + + # address scope is missing, needs update but port is missing in ovn + lsp4 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={ + "uuid": "1f4323db-fb58-48e9-adae-6c6e833c581c", + "name": "lsp4", + "external_ids": {}, + } + ) + + nb_idl.db_find_rows.return_value.execute.return_value = [ + lsp0, + lsp1, + lsp2, + lsp4, + ] + + self.fake_ovn_client._plugin.get_port.side_effect = [ + {"network_id": "net0"}, + n_exc.PortNotFound(port_id="port"), + ] + + external_ids = { + constants.OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY: "address_scope_v4", + constants.OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY: "address_scope_v6", + } + + self.fake_ovn_client.get_external_ids_from_port.return_value = ( + None, + external_ids, + ) + + self.assertRaises( + periodics.NeverAgain, self.periodic.check_port_has_address_scope + ) + + nb_idl.set_lswitch_port.assert_called_once_with( + "lsp1", external_ids=external_ids + ) + def test_check_for_mcast_flood_reports(self): nb_idl = self.fake_ovn_client._nb_idl lsp0 = fakes.FakeOvsdbRow.create_one_ovsdb_row( diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index 41fc9b2d4f8..f62999f57d7 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -1810,6 +1810,62 @@ class TestOVNMechanismDriver(TestOVNMechanismDriverBase): mock.ANY, filters={'id': subnet_ids}) + def test__get_port_options_with_addr_scope(self): + with mock.patch.object( + self.mech_driver._plugin, "get_subnets" + ) as mock_get_subnets, mock.patch.object( + self.mech_driver._plugin, + "get_subnetpool", + ) as mock_get_subnetpool: + port = { + "id": "virt-port", + "mac_address": "00:00:00:00:00:00", + "device_owner": "device_owner", + "network_id": "foo", + "fixed_ips": [ + {"subnet_id": "subnet-1", "ip_address": "10.0.0.55"}, + {"subnet_id": "subnet-2", "ip_address": "aef0::4"}, + ], + } + + subnet_ids = [ip["subnet_id"] for ip in port.get("fixed_ips")] + mock_get_subnets.return_value = [ + { + "id": "subnet-1", + "subnetpool_id": "subnetpool1", + "cidr": "10.0.0.0/24", + }, + { + "id": "subnet-2", + "subnetpool_id": "subnetpool2", + "cidr": "aef0::/64", + }, + ] + mock_get_subnetpool.side_effect = [ + { + "ip_version": const.IP_VERSION_4, + "address_scope_id": "address_scope_v4", + }, + { + "ip_version": const.IP_VERSION_6, + "address_scope_id": "address_scope_v6", + }, + ] + options = self.mech_driver._ovn_client._get_port_options(port) + mock_get_subnets.assert_called_once_with( + mock.ANY, filters={"id": subnet_ids} + ) + + expected_calls = [ + mock.call(mock.ANY, id="subnetpool1"), + mock.call(mock.ANY, id="subnetpool2"), + ] + + mock_get_subnetpool.assert_has_calls(expected_calls) + + self.assertEqual("address_scope_v4", options.address4_scope_id) + self.assertEqual("address_scope_v6", options.address6_scope_id) + def test__get_port_options_migrating_additional_chassis_missing(self): port = { 'id': 'virt-port', diff --git a/releasenotes/notes/add-address-scope-to-the-OVN-LSP-port-registers-1f45e34815c3896d.yaml b/releasenotes/notes/add-address-scope-to-the-OVN-LSP-port-registers-1f45e34815c3896d.yaml new file mode 100644 index 00000000000..90e9932c436 --- /dev/null +++ b/releasenotes/notes/add-address-scope-to-the-OVN-LSP-port-registers-1f45e34815c3896d.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Address scope is now added to all OVN LSP port registers in the + northbound. Northd then writes the address scope from the northbound to + the southbound so it can be used there by the ovn-bgp-agent. \ No newline at end of file