Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions python/neutron-understack/neutron_understack/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from neutron_understack.tests.helpers import extend_port_dict_with_trunk
from neutron_understack.trunk import UnderStackTrunkDriver
from neutron_understack.undersync import Undersync
from neutron_understack.undersync_mech import UndersyncDriver
from neutron_understack.undersync_trunk import UndersyncTrunkDriver


@pytest.fixture
Expand Down Expand Up @@ -291,6 +293,18 @@ def understack_trunk_driver(understack_driver) -> UnderStackTrunkDriver:
return UnderStackTrunkDriver.create(understack_driver)


@pytest.fixture
def undersync_driver(oslo_config) -> UndersyncDriver:
driver = UndersyncDriver()
driver.undersync = MagicMock(spec_set=Undersync)
return driver


@pytest.fixture
def undersync_trunk_driver(undersync_driver) -> UndersyncTrunkDriver:
return UndersyncTrunkDriver.create(undersync_driver)


@pytest.fixture
def _ironic_baremetal_port_physical_network(mocker, understack_driver) -> None:
mocker.patch.object(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,33 @@
from neutron_lib.api.definitions import portbindings
from neutron_lib.plugins.ml2 import api

from neutron_understack.undersync import Undersync
from neutron_understack.undersync_mech import UndersyncDriver


@pytest.fixture
def driver():
def driver(mocker):
mocker.patch("neutron_understack.config.get_session")
d = UndersyncDriver()
d.initialize()
return d


class TestInitialize:
def test_creates_undersync_client(self, mocker, oslo_config):
mocker.patch("neutron_understack.config.get_session")
oslo_config.config(
undersync_url="http://undersync.example.com",
group="ml2_understack",
)

driver = UndersyncDriver()
driver.initialize()

assert isinstance(driver.undersync, Undersync)
assert driver.undersync.api_url == "http://undersync.example.com"


def _make_context(vnic_type=portbindings.VNIC_BAREMETAL, segments=None):
context = MagicMock()
context.current = {"id": "port-1", portbindings.VNIC_TYPE: vnic_type}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from unittest.mock import MagicMock

import pytest
from neutron_lib.api.definitions import portbindings
from neutron_lib.services.trunk import constants as trunk_consts

from neutron_understack.undersync_trunk import PhysicalNetworkNotFoundError


@pytest.fixture
def bound_parent_port():
binding = MagicMock()
binding.vif_type = portbindings.VIF_TYPE_OTHER
binding.vnic_type = portbindings.VNIC_BAREMETAL
binding.profile = {"physical_network": "physnet-a"}

binding_level = MagicMock()
binding_level.driver = "undersync"

port = MagicMock()
port.id = "parent-port-1"
port.bindings = [binding]
port.binding_levels = [binding_level]
return port


@pytest.fixture
def bound_parent_port_without_physnet(bound_parent_port):
bound_parent_port.bindings[0].profile = {"local_link_information": []}
return bound_parent_port


@pytest.fixture
def unbound_parent_port(bound_parent_port):
bound_parent_port.bindings[0].vif_type = portbindings.VIF_TYPE_UNBOUND
return bound_parent_port


@pytest.fixture
def parent_port_bound_by_other_driver(bound_parent_port):
bound_parent_port.binding_levels[0].driver = "understack"
return bound_parent_port


class TestSubportsAdded:
def test_calls_sync_parent_port_physical_network(
self, mocker, undersync_trunk_driver, trunk_payload, trunk
):
sync = mocker.patch.object(
undersync_trunk_driver, "_sync_parent_port_physical_network"
)
update = mocker.patch.object(trunk, "update")

undersync_trunk_driver.subports_added("", "", "", trunk_payload)

sync.assert_called_once_with(trunk)
update.assert_called_once_with(status=trunk_consts.TRUNK_ACTIVE_STATUS)


class TestSubportsDeleted:
def test_calls_sync_parent_port_physical_network(
self, mocker, undersync_trunk_driver, trunk_payload, trunk
):
sync = mocker.patch.object(
undersync_trunk_driver, "_sync_parent_port_physical_network"
)

undersync_trunk_driver.subports_deleted("", "", "", trunk_payload)

sync.assert_called_once_with(trunk)


class TestSyncParentPortPhysicalNetwork:
def test_syncs_when_parent_port_bound_with_physical_network(
self, mocker, undersync_trunk_driver, trunk, bound_parent_port
):
mocker.patch(
"neutron_understack.utils.fetch_port_object",
return_value=bound_parent_port,
)

undersync_trunk_driver._sync_parent_port_physical_network(trunk)

undersync_trunk_driver.undersync.sync.assert_called_once_with("physnet-a")

def test_skips_when_parent_port_is_unbound(
self, mocker, undersync_trunk_driver, trunk, unbound_parent_port
):
mocker.patch(
"neutron_understack.utils.fetch_port_object",
return_value=unbound_parent_port,
)

undersync_trunk_driver._sync_parent_port_physical_network(trunk)

undersync_trunk_driver.undersync.sync.assert_not_called()

def test_raises_when_physical_network_missing(
self, mocker, undersync_trunk_driver, trunk, bound_parent_port_without_physnet
):
mocker.patch(
"neutron_understack.utils.fetch_port_object",
return_value=bound_parent_port_without_physnet,
)

with pytest.raises(PhysicalNetworkNotFoundError):
undersync_trunk_driver._sync_parent_port_physical_network(trunk)

undersync_trunk_driver.undersync.sync.assert_not_called()

def test_skips_when_parent_port_not_bound_by_undersync(
self, mocker, undersync_trunk_driver, trunk, parent_port_bound_by_other_driver
):
mocker.patch(
"neutron_understack.utils.fetch_port_object",
return_value=parent_port_bound_by_other_driver,
)

undersync_trunk_driver._sync_parent_port_physical_network(trunk)

undersync_trunk_driver.undersync.sync.assert_not_called()
11 changes: 10 additions & 1 deletion python/neutron-understack/neutron_understack/undersync_mech.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from neutron_lib.api.definitions import portbindings
from neutron_lib.plugins.ml2 import api
from neutron_lib.plugins.ml2.api import MechanismDriver
from oslo_config import cfg

from neutron_understack import config
from neutron_understack.undersync import Undersync
from neutron_understack.undersync_trunk import UndersyncTrunkDriver

from .ml2_type_annotations import PortContext

Expand All @@ -18,7 +23,11 @@ def connectivity(self): # type: ignore
return portbindings.CONNECTIVITY_L2

def initialize(self):
pass
config.register_ml2_understack_opts(cfg.CONF)
conf = cfg.CONF.ml2_understack

self.undersync = Undersync(conf.undersync_url)
self.trunk_driver = UndersyncTrunkDriver.create(self)

def bind_port(self, context: PortContext) -> None:
port = context.current
Expand Down
122 changes: 122 additions & 0 deletions python/neutron-understack/neutron_understack/undersync_trunk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from neutron.objects.trunk import Trunk
from neutron.services.trunk.drivers import base as trunk_base
from neutron_lib import exceptions as exc
from neutron_lib.api.definitions import portbindings
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib.services.trunk import constants as trunk_consts
from oslo_config import cfg
from oslo_log import log

from neutron_understack import utils

LOG = log.getLogger(__name__)

# name this driver is registered under in the `neutron.ml2.mechanism_drivers`
# entry point namespace, also recorded as PortBindingLevel.driver for any
# port level bound by this mechanism driver.
MECHANISM_DRIVER_NAME = "undersync"

SUPPORTED_INTERFACES = (portbindings.VIF_TYPE_OTHER,)

SUPPORTED_SEGMENTATION_TYPES = (trunk_consts.SEGMENTATION_TYPE_VLAN,)


class PhysicalNetworkNotFoundError(exc.NeutronException):
message = (
"No physical_network found in binding profile for parent port "
"%(port_id)s of trunk %(trunk_id)s, cannot sync undersync."
)


class UndersyncTrunkDriver(trunk_base.DriverBase):
def __init__(
self,
name,
interfaces,
segmentation_types,
agent_type=None,
can_trunk_bound_port=False,
):
super().__init__(
name,
interfaces,
segmentation_types,
agent_type=agent_type,
can_trunk_bound_port=can_trunk_bound_port,
)
self.undersync = self.plugin_driver.undersync

@property
def is_loaded(self):
try:
return MECHANISM_DRIVER_NAME in cfg.CONF.ml2.mechanism_drivers
except cfg.NoSuchOptError:
return False

@classmethod
def create(cls, plugin_driver):
cls.plugin_driver = plugin_driver
# can_trunk_bound_port means that a trunk can be added to an already
# bound port, which is possible in baremetal environments so always
# report this as true
return cls(
MECHANISM_DRIVER_NAME,
SUPPORTED_INTERFACES,
SUPPORTED_SEGMENTATION_TYPES,
None,
can_trunk_bound_port=True,
)

@registry.receives(resources.TRUNK_PLUGIN, [events.AFTER_INIT])
def register(self, resource, event, trigger, payload=None):
super().register(resource, event, trigger, payload=payload)

# events that we want to listen to and the functions they should
# call. cancellable=True means that an Exception raised will
# interrupt / fail the operation that's happening.
registry.subscribe(
self.subports_added,
resources.SUBPORTS,
events.AFTER_CREATE,
cancellable=True,
)
registry.subscribe(
self.subports_deleted,
resources.SUBPORTS,
events.AFTER_DELETE,
cancellable=True,
)

def _sync_parent_port_physical_network(self, trunk: Trunk) -> None:
parent_port = utils.fetch_port_object(trunk.port_id)
if not utils.parent_port_is_bound(parent_port):
return
if not self._parent_port_bound_by_undersync(parent_port):
return

binding_profile = parent_port.bindings[0].profile
vlan_group_name = binding_profile.get("physical_network")
if not vlan_group_name:
raise PhysicalNetworkNotFoundError(
port_id=parent_port.id, trunk_id=trunk.id
)

self.undersync.sync(vlan_group_name)

@staticmethod
def _parent_port_bound_by_undersync(parent_port) -> bool:
return any(
level.driver == MECHANISM_DRIVER_NAME
for level in parent_port.binding_levels
)

def subports_added(self, resource, event, trunk_plugin, payload):
trunk = payload.states[0]
self._sync_parent_port_physical_network(trunk)
trunk.update(status=trunk_consts.TRUNK_ACTIVE_STATUS)

def subports_deleted(self, resource, event, trunk_plugin, payload):
trunk = payload.states[0]
self._sync_parent_port_physical_network(trunk)
Loading