From 17f73bc9430af76e239be1475a69ecebf72e9545 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 20 May 2026 18:02:58 -0500 Subject: [PATCH 1/5] chore(neutron): add additional logging to undersync mechanism --- .../neutron_understack/undersync_mech.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/python/neutron-understack/neutron_understack/undersync_mech.py b/python/neutron-understack/neutron_understack/undersync_mech.py index c2c9e34b4..aed50b319 100644 --- a/python/neutron-understack/neutron_understack/undersync_mech.py +++ b/python/neutron-understack/neutron_understack/undersync_mech.py @@ -1,3 +1,5 @@ +import logging + from neutron_lib import constants as p_const from neutron_lib.api.definitions import portbindings from neutron_lib.plugins.ml2 import api @@ -5,6 +7,8 @@ from .ml2_type_annotations import PortContext +LOG = logging.getLogger(__name__) + SUPPORTED_VNIC_TYPES = [portbindings.VNIC_BAREMETAL, portbindings.VNIC_NORMAL] @@ -17,14 +21,25 @@ def initialize(self): pass def bind_port(self, context: PortContext) -> None: - vnic_type = context.current.get( - portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL + port = context.current + vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) + LOG.debug( + "bind_port called for port %s vnic_type %s segments %s", + port["id"], + vnic_type, + context.segments_to_bind, ) if vnic_type not in SUPPORTED_VNIC_TYPES: + LOG.debug("Skipping unsupported vnic_type %s", vnic_type) return for segment in context.segments_to_bind: if segment[api.NETWORK_TYPE] == p_const.TYPE_VLAN: + LOG.debug( + "bind_port: setting binding for port %s on VLAN segment %s", + port["id"], + segment, + ) context.set_binding( segment_id=segment[api.ID], vif_type=portbindings.VIF_TYPE_OTHER, @@ -32,3 +47,9 @@ def bind_port(self, context: PortContext) -> None: status=p_const.PORT_STATUS_ACTIVE, ) return + + LOG.warning( + "bind_port: no VLAN segment found for port %s in %s", + port["id"], + context.segments_to_bind, + ) From 562c1d2b3aad7a8bc8452731bd244eed61c0e29b Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 20 May 2026 18:29:34 -0500 Subject: [PATCH 2/5] docs: load all necessary mechanism drivers and document them We need to load 'ovn' early and then we need to load 'baremetal' for port status updates and both of these were omitted. --- components/neutron/values.yaml | 7 ++++--- docs/design-guide/neutron-networking.md | 12 +++++++++--- docs/operator-guide/openstack-neutron.md | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/components/neutron/values.yaml b/components/neutron/values.yaml index b8ed8b151..4552de42b 100644 --- a/components/neutron/values.yaml +++ b/components/neutron/values.yaml @@ -43,9 +43,10 @@ conf: ml2_conf: ml2: extension_drivers: 'port_security' - # set the default ml2 backend to our plugin, neutron_understack - # we'll need to use the ovn ML2 plugin to hook the routers to our network - mechanism_drivers: "understack,undersync,ovn" + # ovn is listed first so it creates virtual ports for routers before + # our baremetal drivers run, matching the recommended ordering from + # https://docs.openstack.org/networking-baremetal/latest/ + mechanism_drivers: "ovn,understack,baremetal,undersync" tenant_network_types: "vxlan" type_drivers: "vlan,vxlan" ml2_type_vxlan: diff --git a/docs/design-guide/neutron-networking.md b/docs/design-guide/neutron-networking.md index ea75f1bc5..63b17378f 100644 --- a/docs/design-guide/neutron-networking.md +++ b/docs/design-guide/neutron-networking.md @@ -7,10 +7,13 @@ many of these features can be achieved. To enable this we are using the following plugins/features of Neutron: -- [OVN driver][ovn-driver] for general [OVN][ovn] support +- [OVN driver][ovn-driver] for general [OVN][ovn] support — loaded first so it + creates virtual ports for routers before the baremetal drivers run, as + recommended by [networking-baremetal][networking-baremetal] - [networking-baremetal][networking-baremetal] to have Neutron aware of the physical networks of Ironic baremetal ports. -- our custom mechanism drivers `understack` and `undersync` (both must be loaded) +- our custom mechanism drivers `understack` and `undersync` (both must be loaded, + with `baremetal` from [networking-baremetal][networking-baremetal] loaded between them) - [ovn-router][ovn-admin] as the L3 router plugin - [trunk plugin][neutron-trunk] service plugin - [network segment range][neutron-network-segment-range] service plugin @@ -377,12 +380,15 @@ The names and the IDs all match, along with the VLAN ID of the segment where the ## ML2 Mechanism Operations Our ML2 mechanism is split across two drivers that must both be present in -`mechanism_drivers`: +`mechanism_drivers`, with the `baremetal` driver from +[networking-baremetal][networking-baremetal] loaded between them: - `understack` — the primary driver responsible for allocating dynamic VLAN segments on VXLAN networks (`bind_port()`), releasing them when ports are removed (`delete_port_postcommit()`), and triggering switch configuration updates (`update_port_postcommit()`) +- `baremetal` — the [networking-baremetal][networking-baremetal] driver that + makes Neutron aware of the physical networks of Ironic baremetal ports - `undersync` — handles level-1 binding by calling `set_binding()` on the VLAN segment that `understack` allocated via `continue_binding()`; without it port binding fails at level 1 diff --git a/docs/operator-guide/openstack-neutron.md b/docs/operator-guide/openstack-neutron.md index 6be849276..e05da6260 100644 --- a/docs/operator-guide/openstack-neutron.md +++ b/docs/operator-guide/openstack-neutron.md @@ -17,7 +17,7 @@ conf: # replacing so you'll need to pay attention # to any changes your environment might have # from the default - mechanism_drivers: "logger,understack,undersync,ovn" + mechanism_drivers: "logger,ovn,understack,baremetal,undersync" logging: loggers: # for 'keys' we are attempt to append 'mechanism_logger' From d0cfaf1cbef9c6ba5a45548708b1ab1f6ca4103a Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 20 May 2026 18:39:16 -0500 Subject: [PATCH 3/5] chore(neutron): add extra tests to validate binding Validate that binding happens in the correct order. --- .../tests/test_neutron_understack_mech.py | 50 +++++++++++++++++++ .../tests/test_undersync_mech.py | 17 ++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py index 7efd7fd11..017277242 100644 --- a/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/tests/test_neutron_understack_mech.py @@ -16,6 +16,56 @@ def test_with_simple_port(self, understack_driver, port_context): @pytest.mark.usefixtures("_ironic_baremetal_port_physical_network") class TestBindPort: + def test_does_not_bind_vlan_only_segments( + self, + mocker, + port_context, + understack_driver, + vlan_network_segment, + ): + """At level 1 understack receives only the VLAN segment and should do nothing. + + undersync is responsible for that binding. + """ + port_context._prepare_to_bind([vlan_network_segment]) + mocker.patch.object(port_context, "continue_binding") + + understack_driver.bind_port(port_context) + + port_context.continue_binding.assert_not_called() + + def test_uses_existing_vlan_segment( + self, + mocker, + port_context, + understack_driver, + vlan_network_segment, + ): + """When a VLAN segment already exists for the physnet, reuse it. + + It should not allocate a new dynamic segment. + """ + mocker.patch.object(port_context, "allocate_dynamic_segment") + mocker.patch.object(port_context, "continue_binding") + mocker.patch( + "neutron_understack.neutron_understack_mech.utils.vlan_segment_for_physnet", + return_value=vlan_network_segment, + ) + port_context._prepare_to_bind(port_context.network.network_segments) + + understack_driver.bind_port(port_context) + + port_context.allocate_dynamic_segment.assert_not_called() + vxlan_segment = next( + s + for s in port_context.network.network_segments + if s[api.NETWORK_TYPE] == "vxlan" + ) + port_context.continue_binding.assert_called_once_with( + segment_id=vxlan_segment[api.ID], + next_segments_to_bind=[vlan_network_segment], + ) + def test_with_no_trunk( self, mocker, diff --git a/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py b/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py index e1b000448..b8b4bc023 100644 --- a/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py +++ b/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py @@ -17,7 +17,7 @@ def driver(): def _make_context(vnic_type=portbindings.VNIC_BAREMETAL, segments=None): context = MagicMock() - context.current = {portbindings.VNIC_TYPE: vnic_type} + context.current = {"id": "port-1", portbindings.VNIC_TYPE: vnic_type} context.segments_to_bind = segments or [] return context @@ -100,6 +100,21 @@ def test_normal_vnic_type_is_supported(self, driver, vlan_segment): ctx.set_binding.assert_called_once() + def test_binds_vlan_when_preceded_by_vxlan( + self, driver, vxlan_segment, vlan_segment + ): + vlan = vlan_segment() + ctx = _make_context(segments=[vxlan_segment(), vlan]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_called_once_with( + segment_id=vlan[api.ID], + vif_type=portbindings.VIF_TYPE_OTHER, + vif_details={}, + status=p_const.PORT_STATUS_ACTIVE, + ) + def test_empty_segments_to_bind(self, driver): ctx = _make_context(segments=[]) From 2fe3533f5af4eb1b67370cfadc7612038e05f852 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 20 May 2026 18:39:59 -0500 Subject: [PATCH 4/5] chore(neutron): complete the mechanism rename to undersync --- .../neutron_understack/tests/test_undersync_mech.py | 6 +++--- .../neutron-understack/neutron_understack/undersync_mech.py | 2 +- python/neutron-understack/pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py b/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py index b8b4bc023..42a229a3a 100644 --- a/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py +++ b/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py @@ -5,12 +5,12 @@ from neutron_lib.api.definitions import portbindings from neutron_lib.plugins.ml2 import api -from neutron_understack.undersync_mech import UnderstackUndersyncDriver +from neutron_understack.undersync_mech import UndersyncDriver @pytest.fixture def driver(): - d = UnderstackUndersyncDriver() + d = UndersyncDriver() d.initialize() return d @@ -50,7 +50,7 @@ def _make(segment_id="seg-vxlan-1"): return _make -class TestUnderstackUndersyncDriverBindPort: +class TestUndersyncDriverBindPort: def test_binds_vlan_segment(self, driver, vlan_segment): seg = vlan_segment() ctx = _make_context(segments=[seg]) diff --git a/python/neutron-understack/neutron_understack/undersync_mech.py b/python/neutron-understack/neutron_understack/undersync_mech.py index aed50b319..f986661ff 100644 --- a/python/neutron-understack/neutron_understack/undersync_mech.py +++ b/python/neutron-understack/neutron_understack/undersync_mech.py @@ -12,7 +12,7 @@ SUPPORTED_VNIC_TYPES = [portbindings.VNIC_BAREMETAL, portbindings.VNIC_NORMAL] -class UnderstackUndersyncDriver(MechanismDriver): +class UndersyncDriver(MechanismDriver): @property def connectivity(self): # type: ignore return portbindings.CONNECTIVITY_L2 diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 2a8ce8ba2..86a017c16 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ [project.entry-points."neutron.ml2.mechanism_drivers"] understack = "neutron_understack.neutron_understack_mech:UnderstackDriver" -undersync = "neutron_understack.undersync_mech:UnderstackUndersyncDriver" +undersync = "neutron_understack.undersync_mech:UndersyncDriver" [project.entry-points."neutron.ml2.type_drivers"] understack_vxlan = "neutron_understack.type_understack_vxlan:UnderstackVxlanTypeDriver" From 2da63bc304a5063ad9d5d3126789678a772b0a75 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 20 May 2026 18:41:21 -0500 Subject: [PATCH 5/5] chore(neutron): remove dead code of unused type driver --- .../neutron_understack/type_understack_vxlan.py | 9 --------- python/neutron-understack/pyproject.toml | 3 --- 2 files changed, 12 deletions(-) delete mode 100644 python/neutron-understack/neutron_understack/type_understack_vxlan.py diff --git a/python/neutron-understack/neutron_understack/type_understack_vxlan.py b/python/neutron-understack/neutron_understack/type_understack_vxlan.py deleted file mode 100644 index 2e515f57d..000000000 --- a/python/neutron-understack/neutron_understack/type_understack_vxlan.py +++ /dev/null @@ -1,9 +0,0 @@ -from neutron.plugins.ml2.drivers import type_vxlan -from neutron_lib import constants as p_const - - -class UnderstackVxlanTypeDriver(type_vxlan.VxlanTypeDriver): - """Manage state for EVPN L2VNI networks with ML2.""" - - def get_type(self): - return p_const.TYPE_VXLAN diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 86a017c16..453bb35b0 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -33,9 +33,6 @@ dependencies = [ understack = "neutron_understack.neutron_understack_mech:UnderstackDriver" undersync = "neutron_understack.undersync_mech:UndersyncDriver" -[project.entry-points."neutron.ml2.type_drivers"] -understack_vxlan = "neutron_understack.type_understack_vxlan:UnderstackVxlanTypeDriver" - [project.entry-points."neutron.service_plugins"] l3_service_cisco_asa = "neutron_understack.l3_service_cisco_asa:CiscoAsa"