From 3a465948e085fb0a0e91a97903740983a9bc7e73 Mon Sep 17 00:00:00 2001 From: Nidhi Rai Date: Mon, 18 May 2026 16:25:39 +0530 Subject: [PATCH 1/2] neutron split mechanism --- .../ml2_type_annotations.py | 4 + .../neutron_understack_mech.py | 12 +- .../tests/test_neutron_understack_mech.py | 10 ++ .../tests/test_undersync_mech.py | 108 ++++++++++++++++++ .../neutron_understack/undersync_mech.py | 34 ++++++ python/neutron-understack/pyproject.toml | 1 + 6 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 python/neutron-understack/neutron_understack/tests/test_undersync_mech.py create mode 100644 python/neutron-understack/neutron_understack/undersync_mech.py diff --git a/python/neutron-understack/neutron_understack/ml2_type_annotations.py b/python/neutron-understack/neutron_understack/ml2_type_annotations.py index 2c0999e1b..30510fd0c 100644 --- a/python/neutron-understack/neutron_understack/ml2_type_annotations.py +++ b/python/neutron-understack/neutron_understack/ml2_type_annotations.py @@ -158,6 +158,10 @@ def set_binding( self, segment_id: str, vif_type: str, vif_details: dict, status: str ) -> None: ... + def continue_binding( + self, segment_id: str, next_segments_to_bind: list + ) -> None: ... + def allocate_dynamic_segment(self, segment: dict) -> dict: ... def release_dynamic_segment(self, segment_id: str) -> dict: ... diff --git a/python/neutron-understack/neutron_understack/neutron_understack_mech.py b/python/neutron-understack/neutron_understack/neutron_understack_mech.py index fd7e7cd97..5526f62f3 100644 --- a/python/neutron-understack/neutron_understack/neutron_understack_mech.py +++ b/python/neutron-understack/neutron_understack/neutron_understack_mech.py @@ -279,7 +279,7 @@ def bind_port(self, context: PortContext) -> None: LOG.debug("Refusing to bind due to unsupported vnic_type: %s", vnic_type) return - for segment in context.network.network_segments: + for segment in context.segments_to_bind: if segment[api.NETWORK_TYPE] == p_const.TYPE_VXLAN: self._bind_port_segment(context, segment) return @@ -335,12 +335,10 @@ def _bind_port_segment(self, context: PortContext, segment): if trunk_details: self.trunk_driver.configure_trunk(trunk_details, port_id) - LOG.debug("set_binding for segment: %s", segment) - context.set_binding( - segment_id=dynamic_segment[api.ID], - vif_type=portbindings.VIF_TYPE_OTHER, - vif_details={}, - status=p_const.PORT_STATUS_ACTIVE, + LOG.debug("continue_binding for segment: %s", segment) + context.continue_binding( + segment_id=segment[api.ID], + next_segments_to_bind=[dynamic_segment], ) def invoke_undersync(self, vlan_group_name: str): 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 2c301c31f..4716f9a5a 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 @@ -26,11 +26,16 @@ def test_with_no_trunk( mocker.patch.object( port_context, "allocate_dynamic_segment", return_value=vlan_network_segment ) + mocker.patch.object(port_context, "continue_binding") + type(port_context).segments_to_bind = mocker.PropertyMock( + return_value=port_context.network.network_segments + ) understack_driver.bind_port(port_context) understack_driver.trunk_driver = understack_trunk_driver port_context.allocate_dynamic_segment.assert_called_once() + port_context.continue_binding.assert_called_once() @pytest.mark.parametrize("port_dict", [{"trunk": True}], indirect=True) def test_with_trunk_details( @@ -39,11 +44,16 @@ def test_with_trunk_details( mocker.patch( "neutron_understack.utils.fetch_subport_network_id", return_value="112233" ) + mocker.patch.object(port_context, "continue_binding") + type(port_context).segments_to_bind = mocker.PropertyMock( + return_value=port_context.network.network_segments + ) understack_driver.trunk_driver = understack_trunk_driver mocker.patch.object(understack_driver.trunk_driver, "configure_trunk") understack_driver.bind_port(port_context) understack_driver.trunk_driver.configure_trunk.assert_called_once() + port_context.continue_binding.assert_called_once() class TestCreateNetworkPostCommit: diff --git a/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py b/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py new file mode 100644 index 000000000..e1b000448 --- /dev/null +++ b/python/neutron-understack/neutron_understack/tests/test_undersync_mech.py @@ -0,0 +1,108 @@ +from unittest.mock import MagicMock + +import pytest +from neutron_lib import constants as p_const +from neutron_lib.api.definitions import portbindings +from neutron_lib.plugins.ml2 import api + +from neutron_understack.undersync_mech import UnderstackUndersyncDriver + + +@pytest.fixture +def driver(): + d = UnderstackUndersyncDriver() + d.initialize() + return d + + +def _make_context(vnic_type=portbindings.VNIC_BAREMETAL, segments=None): + context = MagicMock() + context.current = {portbindings.VNIC_TYPE: vnic_type} + context.segments_to_bind = segments or [] + return context + + +@pytest.fixture +def vlan_segment(): + def _make(segment_id="seg-vlan-1"): + return { + api.ID: segment_id, + api.NETWORK_TYPE: p_const.TYPE_VLAN, + api.SEGMENTATION_ID: 100, + api.PHYSICAL_NETWORK: "physnet1", + api.MTU: 1500, + } + + return _make + + +@pytest.fixture +def vxlan_segment(): + def _make(segment_id="seg-vxlan-1"): + return { + api.ID: segment_id, + api.NETWORK_TYPE: p_const.TYPE_VXLAN, + api.SEGMENTATION_ID: 1000, + api.PHYSICAL_NETWORK: None, + api.MTU: 1450, + } + + return _make + + +class TestUnderstackUndersyncDriverBindPort: + def test_binds_vlan_segment(self, driver, vlan_segment): + seg = vlan_segment() + ctx = _make_context(segments=[seg]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_called_once_with( + segment_id=seg[api.ID], + vif_type=portbindings.VIF_TYPE_OTHER, + vif_details={}, + status=p_const.PORT_STATUS_ACTIVE, + ) + + def test_binds_first_vlan_segment_only(self, driver, vlan_segment): + seg1 = vlan_segment("seg-vlan-1") + seg2 = vlan_segment("seg-vlan-2") + ctx = _make_context(segments=[seg1, seg2]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_called_once_with( + segment_id=seg1[api.ID], + vif_type=portbindings.VIF_TYPE_OTHER, + vif_details={}, + status=p_const.PORT_STATUS_ACTIVE, + ) + + def test_skips_vxlan_segment(self, driver, vxlan_segment): + ctx = _make_context(segments=[vxlan_segment()]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_not_called() + + def test_skips_unsupported_vnic_type(self, driver, vlan_segment): + ctx = _make_context(vnic_type="direct", segments=[vlan_segment()]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_not_called() + + def test_normal_vnic_type_is_supported(self, driver, vlan_segment): + seg = vlan_segment() + ctx = _make_context(vnic_type=portbindings.VNIC_NORMAL, segments=[seg]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_called_once() + + def test_empty_segments_to_bind(self, driver): + ctx = _make_context(segments=[]) + + driver.bind_port(ctx) + + ctx.set_binding.assert_not_called() diff --git a/python/neutron-understack/neutron_understack/undersync_mech.py b/python/neutron-understack/neutron_understack/undersync_mech.py new file mode 100644 index 000000000..c2c9e34b4 --- /dev/null +++ b/python/neutron-understack/neutron_understack/undersync_mech.py @@ -0,0 +1,34 @@ +from neutron_lib import constants as p_const +from neutron_lib.api.definitions import portbindings +from neutron_lib.plugins.ml2 import api +from neutron_lib.plugins.ml2.api import MechanismDriver + +from .ml2_type_annotations import PortContext + +SUPPORTED_VNIC_TYPES = [portbindings.VNIC_BAREMETAL, portbindings.VNIC_NORMAL] + + +class UnderstackUndersyncDriver(MechanismDriver): + @property + def connectivity(self): # type: ignore + return portbindings.CONNECTIVITY_L2 + + def initialize(self): + pass + + def bind_port(self, context: PortContext) -> None: + vnic_type = context.current.get( + portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL + ) + if vnic_type not in SUPPORTED_VNIC_TYPES: + return + + for segment in context.segments_to_bind: + if segment[api.NETWORK_TYPE] == p_const.TYPE_VLAN: + context.set_binding( + segment_id=segment[api.ID], + vif_type=portbindings.VIF_TYPE_OTHER, + vif_details={}, + status=p_const.PORT_STATUS_ACTIVE, + ) + return diff --git a/python/neutron-understack/pyproject.toml b/python/neutron-understack/pyproject.toml index 8d10c91ee..251a6810b 100644 --- a/python/neutron-understack/pyproject.toml +++ b/python/neutron-understack/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ [project.entry-points."neutron.ml2.mechanism_drivers"] understack = "neutron_understack.neutron_understack_mech:UnderstackDriver" +understack_undersync = "neutron_understack.undersync_mech:UnderstackUndersyncDriver" [project.entry-points."neutron.ml2.type_drivers"] understack_vxlan = "neutron_understack.type_understack_vxlan:UnderstackVxlanTypeDriver" From 8e6920ff8473252f569e2a1e12d5ce2c04d10bc7 Mon Sep 17 00:00:00 2001 From: Nidhi Rai Date: Wed, 20 May 2026 22:40:12 +0530 Subject: [PATCH 2/2] test: assert continue_binding called with correct segment args --- .../tests/test_neutron_understack_mech.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 4716f9a5a..7efd7fd11 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 @@ -1,6 +1,7 @@ from dataclasses import dataclass import pytest +from neutron_lib.plugins.ml2 import api from oslo_config import cfg from neutron_understack.neutron_understack_mech import UnderstackDriver @@ -27,15 +28,21 @@ def test_with_no_trunk( port_context, "allocate_dynamic_segment", return_value=vlan_network_segment ) mocker.patch.object(port_context, "continue_binding") - type(port_context).segments_to_bind = mocker.PropertyMock( - return_value=port_context.network.network_segments - ) + port_context._prepare_to_bind(port_context.network.network_segments) understack_driver.bind_port(port_context) understack_driver.trunk_driver = understack_trunk_driver port_context.allocate_dynamic_segment.assert_called_once() - port_context.continue_binding.assert_called_once() + 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], + ) @pytest.mark.parametrize("port_dict", [{"trunk": True}], indirect=True) def test_with_trunk_details( @@ -45,9 +52,7 @@ def test_with_trunk_details( "neutron_understack.utils.fetch_subport_network_id", return_value="112233" ) mocker.patch.object(port_context, "continue_binding") - type(port_context).segments_to_bind = mocker.PropertyMock( - return_value=port_context.network.network_segments - ) + port_context._prepare_to_bind(port_context.network.network_segments) understack_driver.trunk_driver = understack_trunk_driver mocker.patch.object(understack_driver.trunk_driver, "configure_trunk")