From e3378dc45a13979954bf0e58460d4b24a1d35389 Mon Sep 17 00:00:00 2001 From: Elvira Garcia Date: Tue, 21 Oct 2025 17:05:57 +0200 Subject: [PATCH 1/4] [OVS] Avoid duplicate logging of network packets We have 2 classes inheriting from BaseNeutronAgentOSKenApp: OVSNeutronAgentOSKenApp and OVSLogOSKenApp. The instantiation of packet_in_handlers as a static property in BaseNeutronAgentOSKenApp created a shared list between all the different objects created by the inheriting classes. This ended up creating duplicated processing of events and therefore duplicate logging in the ML2/OVS log API. Closes-Bug: #2121961 Change-Id: I6c37fbed8d724b7215ca21d155dde00e1229c6ea Signed-off-by: Elvira Garcia (cherry picked from commit 29f4ada3153115fd0962ce540048147bca627f23) --- .../openvswitch/agent/openflow/native/base_oskenapp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/base_oskenapp.py b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/base_oskenapp.py index 354acb4cac2..1387fe7befc 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/base_oskenapp.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/base_oskenapp.py @@ -24,7 +24,10 @@ class BaseNeutronAgentOSKenApp(app_manager.OSKenApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] - packet_in_handlers = [] + + def __init__(self): + super().__init__() + self.packet_in_handlers = [] def register_packet_in_handler(self, caller): self.packet_in_handlers.append(caller) From c76c95506ced3dfbad013364d839e6124d1b21a4 Mon Sep 17 00:00:00 2001 From: Helen Chen Date: Tue, 9 Sep 2025 16:15:57 -0400 Subject: [PATCH 2/4] Fix bug/2115776 where Neutron deletes A/AAAA records Enable the deletion of all the Designate recordssets for the same dns_name and dns_domain associated with the specified IP addresses. This change enables in the Designate driver the separate deletion of two recordsets associated to the same dns-name and dns-domain that were created by a sequence of one port create/update and one floating ip create/update. Before this change, the assumption when deleting recordsets associated to the same same dns-name and dns-domain was that they were created in a single operation. Consequently, the change handles each deletion operation of the dns_name in the dns_domain based on the IP addresses specified in the input argument. Existing unit tests in test_dns_integration.py and test_dns_domain_keywords.py failed with this change. These existing test cases are updated in this commit such that the IP addresses in the mock recordsets are strings, instead of netaddr.IPAddress() objects, to match recordset['records'] data type in _get_ids_ips_to_delete(). The unit tests passed with mismatched types in the past because the check between recordset and input argument records compared the size of two sets rather than the values of the data. A new unit test test_delete_single_record_from_two_records() is added to test that the designate driver handles the use case where only a subset of A or AAAA records are deleted. test_delete_single_record_from_two_records() was generated with the assistance of Claude Code, copying from test_delete_record_set() with a small modification to delete only one of the records. Assisted-by: Claude Code Depends-on: https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/960659 Closes-Bug: #2115776 Change-Id: I42f1d504a063f1d8542c861b3b7caffe56c47bf1 Signed-off-by: Helen Chen (cherry picked from commit 9320cd392f88a68a365bca154b3079b7317103c6) --- .../externaldns/drivers/designate/driver.py | 8 +++--- .../extensions/test_dns_domain_keywords.py | 8 +++--- .../ml2/extensions/test_dns_integration.py | 8 +++--- .../drivers/designate/test_driver.py | 25 +++++++++++++++++++ 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/neutron/services/externaldns/drivers/designate/driver.py b/neutron/services/externaldns/drivers/designate/driver.py index 6b85e6dba29..9ec1a534a2c 100644 --- a/neutron/services/externaldns/drivers/designate/driver.py +++ b/neutron/services/externaldns/drivers/designate/driver.py @@ -165,8 +165,6 @@ def _get_ids_ips_to_delete(self, dns_domain, name, records, dns_domain, criterion={"name": "%s" % name}) except (d_exc.NotFound, d_exc.Forbidden): raise dns_exc.DNSDomainNotFound(dns_domain=dns_domain) - ids = [rec['id'] for rec in recordsets] - ips = [str(ip) for rec in recordsets for ip in rec['records']] - if set(ips) != set(records): - raise dns_exc.DuplicateRecordSet(dns_name=name) - return ids + return [rec['id'] for rec in recordsets + for ip in rec['records'] + if ip in records] diff --git a/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py b/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py index 680cb74c1f3..b1bb595e969 100644 --- a/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py +++ b/neutron/tests/unit/plugins/ml2/extensions/test_dns_domain_keywords.py @@ -84,10 +84,10 @@ def _update_port_for_test(self, port, new_dns_name=test_dns_integration.NEWDNSNAME, new_dns_domain=None, **kwargs): test_dns_integration.mock_client.reset_mock() - ip_addresses = [netaddr.IPAddress(ip['ip_address']) - for ip in port['fixed_ips']] - records_v4 = [ip for ip in ip_addresses if ip.version == 4] - records_v6 = [ip for ip in ip_addresses if ip.version == 6] + records_v4 = [ip['ip_address'] for ip in port['fixed_ips'] + if netaddr.IPAddress(ip['ip_address']).version == 4] + records_v6 = [ip['ip_address'] for ip in port['fixed_ips'] + if netaddr.IPAddress(ip['ip_address']).version == 6] recordsets = [] if records_v4: recordsets.append({'id': test_dns_integration.V4UUID, diff --git a/neutron/tests/unit/plugins/ml2/extensions/test_dns_integration.py b/neutron/tests/unit/plugins/ml2/extensions/test_dns_integration.py index 8552532c9ac..615b4016f0c 100644 --- a/neutron/tests/unit/plugins/ml2/extensions/test_dns_integration.py +++ b/neutron/tests/unit/plugins/ml2/extensions/test_dns_integration.py @@ -126,10 +126,10 @@ def _create_subnet_for_test(self, network_id, cidr): def _update_port_for_test(self, port, new_dns_name=NEWDNSNAME, new_dns_domain=None, **kwargs): mock_client.reset_mock() - ip_addresses = [netaddr.IPAddress(ip['ip_address']) - for ip in port['fixed_ips']] - records_v4 = [ip for ip in ip_addresses if ip.version == 4] - records_v6 = [ip for ip in ip_addresses if ip.version == 6] + records_v4 = [ip['ip_address'] for ip in port['fixed_ips'] + if netaddr.IPAddress(ip['ip_address']).version == 4] + records_v6 = [ip['ip_address'] for ip in port['fixed_ips'] + if netaddr.IPAddress(ip['ip_address']).version == 6] recordsets = [] if records_v4: recordsets.append({'id': V4UUID, 'records': records_v4}) diff --git a/neutron/tests/unit/services/externaldns/drivers/designate/test_driver.py b/neutron/tests/unit/services/externaldns/drivers/designate/test_driver.py index 63fe978112d..0ca64bb2b8d 100644 --- a/neutron/tests/unit/services/externaldns/drivers/designate/test_driver.py +++ b/neutron/tests/unit/services/externaldns/drivers/designate/test_driver.py @@ -169,6 +169,31 @@ def test_delete_record_set(self): ) self.admin_client.recordsets.delete.assert_not_called() + def test_delete_single_record_from_two_records(self): + # Set up two records similar to test_delete_record_set + self.client.recordsets.list.return_value = [ + {'id': 123, 'records': ['192.168.0.10']}, + {'id': 456, 'records': ['2001:db8:0:1::1']} + ] + + cfg.CONF.set_override( + 'allow_reverse_dns_lookup', False, group='designate' + ) + + # Delete only the first record (IPv4) out of the two + self.driver.delete_record_set( + self.context, 'example.test.', 'test', + ['192.168.0.10'] + ) + + # Verify that only the IPv4 record was deleted + self.client.recordsets.delete.assert_called_once_with( + 'example.test.', 123 + ) + + # Admin client should not be called since reverse DNS is disabled + self.admin_client.recordsets.delete.assert_not_called() + def test_delete_record_set_with_reverse_dns(self): self.client.recordsets.list.return_value = [ {'id': 123, 'records': ['192.168.0.10']}, From d796ecd5fe735c4b73cd656e764b9a9e07fbb5b9 Mon Sep 17 00:00:00 2001 From: Yatin Karel Date: Fri, 7 Nov 2025 13:14:31 +0530 Subject: [PATCH 3/4] [CI][9-stream][stable only] Use last python3.9 supported tempest tag tempest master dropped the python3.9 support in [1], so we need to use tempest 45.0.0 which last version supporting python3.9. [1] https://review.opendev.org/c/openstack/tempest/+/966101 Related-Bug: #2130551 Signed-off-by: Yatin Karel Change-Id: Ia0bbb62119621d2cb5781a6e12bf1f2afab1d72e --- zuul.d/tempest-singlenode.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zuul.d/tempest-singlenode.yaml b/zuul.d/tempest-singlenode.yaml index a9863a67af1..e656527deb9 100644 --- a/zuul.d/tempest-singlenode.yaml +++ b/zuul.d/tempest-singlenode.yaml @@ -702,6 +702,7 @@ br-ex-tcpdump: true br-int-flows: true devstack_localrc: + TEMPEST_BRANCH: 45.0.0 TEMPEST_VENV_UPPER_CONSTRAINTS: '/opt/stack/requirements/upper-constraints.txt' devstack_local_conf: test-config: @@ -720,6 +721,7 @@ nslookup_target: 'opendev.org' configure_swap_size: 4096 devstack_localrc: + TEMPEST_BRANCH: 45.0.0 TEMPEST_VENV_UPPER_CONSTRAINTS: '/opt/stack/requirements/upper-constraints.txt' devstack_local_conf: test-config: From 5b23019e4ca7769ca05d0b0f177b85e383a31950 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 11 Nov 2025 12:37:57 +0000 Subject: [PATCH 4/4] [FT] Wait for the manager to be created This patch introduces a `WaitEvent` to check that the expected `Manager` register is created before checking it (again). Closes-Bug: #2131024 Signed-off-by: Rodolfo Alonso Hernandez Change-Id: I64a82952a0895d7ffd8d3e886c4615d62bbe4b20 (cherry picked from commit 2b63ab68770da202172cdbb75de9d32146b89297) --- .../agent/ovsdb/native/test_helpers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/neutron/tests/functional/agent/ovsdb/native/test_helpers.py b/neutron/tests/functional/agent/ovsdb/native/test_helpers.py index 76877965e7e..3b365533df4 100644 --- a/neutron/tests/functional/agent/ovsdb/native/test_helpers.py +++ b/neutron/tests/functional/agent/ovsdb/native/test_helpers.py @@ -14,6 +14,7 @@ # under the License. from neutron_lib import constants as const +from ovsdbapp.backend.ovs_idl import event from neutron.agent.common import ovs_lib from neutron.agent.ovsdb.native import helpers @@ -22,6 +23,16 @@ from neutron.tests.functional import base +class WaitOvsManagerEvent(event.WaitEvent): + event_name = 'WaitOvsManagerEvent' + + def __init__(self, manager_target): + table = 'Manager' + events = (self.ROW_CREATE,) + conditions = (('target', '=', manager_target),) + super().__init__(events, table, conditions, timeout=10) + + class EnableConnectionUriTestCase(base.BaseSudoTestCase): def test_add_manager_appends(self): @@ -39,10 +50,15 @@ def test_add_manager_appends(self): manager_connections.append('ptcp:%s:127.0.0.1' % _port) for index, conn_uri in enumerate(ovsdb_cfg_connections): + target_event = WaitOvsManagerEvent(manager_connections[index]) + ovs.ovsdb.idl.notify_handler.watch_event(target_event) helpers.enable_connection_uri(conn_uri) manager_removal.append(ovs.ovsdb.remove_manager( manager_connections[index])) self.addCleanup(manager_removal[index].execute) + target_event.wait() + # This check is redundant, the ``target_event`` ensures the + # ``Manager`` register with the expected targer is created. self.assertIn(manager_connections[index], ovs.ovsdb.get_manager().execute())