Skip to content
Merged
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
22 changes: 22 additions & 0 deletions python/neutron-understack/neutron_understack/nautobot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class NautobotNotFoundError(exc.NeutronException):
message = "%(obj)s not found in Nautobot. ref=%(ref)s"


class NautobotCustomFieldNotFoundError(exc.NeutronException):
message = "Custom field with name %(cf_name)s not found for %(obj)s"


class Nautobot:
"""Basic Nautobot wrapper because pynautobot doesn't expose plugin APIs."""

Expand Down Expand Up @@ -99,6 +103,19 @@ def ucvni_delete(self, network_id):
url = f"/api/plugins/undercloud-vni/ucvnis/{network_id}/"
return self.make_api_request("DELETE", url)

def fetch_ucvni(self, network_id: str) -> dict:
url = f"/api/plugins/undercloud-vni/ucvnis/{network_id}/"
return self.make_api_request("GET", url)

def fetch_ucvni_tenant_vlan_id(self, network_id: str) -> int | None:
ucvni_data = self.fetch_ucvni(network_id=network_id)
custom_fields = ucvni_data.get("custom_fields", {})
if "tenant_vlan_id" not in custom_fields:
raise NautobotCustomFieldNotFoundError(
cf_name="tenant_vlan_id", obj="UCVNI"
)
return custom_fields.get("tenant_vlan_id")

def fetch_namespace_by_name(self, name: str) -> str:
url = f"/api/ipam/namespaces/?name={name}&depth=1"
resp_data = self.make_api_request("GET", url)
Expand Down Expand Up @@ -139,6 +156,11 @@ def associate_subnet_with_network(
}
self.make_api_request("PATCH", url, payload)

def add_tenant_vlan_tag_to_ucvni(self, network_uuid: str, vlan_tag: int) -> dict:
url = f"/api/plugins/undercloud-vni/ucvnis/{network_uuid}/"
payload = {"custom_fields": {"tenant_vlan_id": vlan_tag}}
return self.make_api_request("PATCH", url, payload)

def subnet_delete(self, uuid: str) -> dict:
return self.make_api_request("DELETE", f"/api/ipam/prefixes/{uuid}/")

Expand Down
89 changes: 89 additions & 0 deletions python/neutron-understack/neutron_understack/tests/test_trunk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest

from neutron_understack.nautobot import Nautobot
from neutron_understack.neutron_understack_mech import UnderstackDriver
from neutron_understack.trunk import SubportSegmentationIDError
from neutron_understack.trunk import UnderStackTrunkDriver


@pytest.fixture
def subport() -> MagicMock:
return MagicMock(port_id="portUUID", segmentation_id=555)


@pytest.fixture
def trunk(subport) -> MagicMock:
return MagicMock(sub_ports=[subport])


@pytest.fixture
def payload_metadata(subport) -> dict:
return {"subports": [subport]}


@pytest.fixture
def payload(payload_metadata, trunk) -> MagicMock:
return MagicMock(metadata=payload_metadata, states=[trunk])


@pytest.fixture
def nautobot_client() -> Nautobot:
return MagicMock(spec_set=Nautobot)


driver = UnderstackDriver()
driver.nb = Nautobot("", "")
trunk_driver = UnderStackTrunkDriver.create(driver)


@patch("neutron_understack.utils.fetch_subport_network_id", return_value="112233")
def test_subports_added_when_ucvni_tenan_vlan_id_is_not_set_yet(
nautobot_client, payload
):
trunk_driver.nb = nautobot_client
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": None}
nautobot_client.configure_mock(**attrs)
trunk_driver.subports_added("", "", "", payload)

nautobot_client.add_tenant_vlan_tag_to_ucvni.assert_called_once_with(
network_uuid="112233", vlan_tag=555
)


@patch("neutron_understack.utils.fetch_subport_network_id", return_value="223344")
def test_subports_added_when_segmentation_id_is_different_to_tenant_vlan_id(
nautobot_client, payload
):
trunk_driver.nb = nautobot_client
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": 123}
nautobot_client.configure_mock(**attrs)
with pytest.raises(SubportSegmentationIDError):
trunk_driver.subports_added("", "", "", payload)


@patch("neutron_understack.utils.fetch_subport_network_id", return_value="112233")
def test_trunk_created_when_ucvni_tenan_vlan_id_is_not_set_yet(
nautobot_client, payload
):
trunk_driver.nb = nautobot_client
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": None}
nautobot_client.configure_mock(**attrs)
trunk_driver.trunk_created("", "", "", payload)

nautobot_client.add_tenant_vlan_tag_to_ucvni.assert_called_once_with(
network_uuid="112233", vlan_tag=555
)


@patch("neutron_understack.utils.fetch_subport_network_id", return_value="223344")
def test_trunk_created_when_segmentation_id_is_different_to_tenant_vlan_id(
nautobot_client, payload
):
trunk_driver.nb = nautobot_client
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": 123}
nautobot_client.configure_mock(**attrs)
with pytest.raises(SubportSegmentationIDError):
trunk_driver.trunk_created("", "", "", payload)
102 changes: 102 additions & 0 deletions python/neutron-understack/neutron_understack/trunk.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
from neutron.objects.trunk import SubPort
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__)

SUPPORTED_INTERFACES = (portbindings.VIF_TYPE_OTHER,)

SUPPORTED_SEGMENTATION_TYPES = (trunk_consts.SEGMENTATION_TYPE_VLAN,)


class SubportSegmentationIDError(exc.NeutronException):
message = (
"Segmentation ID: %(seg_id)s cannot be set to the Subport: "
"%(subport_id)s as there is already another Segmentation ID: "
"%(nb_seg_id)s in use by the Network: %(net_id)s that is "
"attached to the Subport. Please use %(nb_seg_id)s as "
"segmentation_id for this subport."
)


class UnderStackTrunkDriver(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.nb = self.plugin_driver.nb

@property
def is_loaded(self):
try:
Expand All @@ -26,3 +63,68 @@ def create(cls, plugin_driver):
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)

registry.subscribe(
self.subports_added,
resources.SUBPORTS,
events.AFTER_CREATE,
cancellable=True,
)
registry.subscribe(
self.trunk_created, resources.TRUNK, events.AFTER_CREATE, cancellable=True
)

def _handle_segmentation_id_mismatch(
self, subport: SubPort, ucvni_uuid: str, tenant_vlan_id: int
) -> None:
subport.delete()
raise SubportSegmentationIDError(
seg_id=subport.segmentation_id,
net_id=ucvni_uuid,
nb_seg_id=tenant_vlan_id,
subport_id=subport.port_id,
)

def _configure_tenant_vlan_id(self, ucvni_uuid: str, subport: SubPort) -> None:
subport_seg_id = subport.segmentation_id
self.nb.add_tenant_vlan_tag_to_ucvni(
network_uuid=ucvni_uuid, vlan_tag=subport_seg_id
)
LOG.info(
"Segmentation ID: %(seg_id)s is now set on Nautobot's UCVNI "
"UUID: %(ucvni_uuid)s in the tenant_vlan_id custom field",
{"seg_id": subport_seg_id, "ucvni_uuid": ucvni_uuid},
)

def _subports_added(self, subports: list[SubPort]) -> None:
for subport in subports:
subport_network_id = utils.fetch_subport_network_id(
subport_id=subport.port_id
)
ucvni_tenant_vlan_id = self.nb.fetch_ucvni_tenant_vlan_id(
network_id=subport_network_id
)
if not ucvni_tenant_vlan_id:
self._configure_tenant_vlan_id(
ucvni_uuid=subport_network_id, subport=subport
)
elif ucvni_tenant_vlan_id != subport.segmentation_id:
self._handle_segmentation_id_mismatch(
subport=subport,
ucvni_uuid=subport_network_id,
tenant_vlan_id=ucvni_tenant_vlan_id,
)

def subports_added(self, resource, event, trunk_plugin, payload):
subports = payload.metadata["subports"]
self._subports_added(subports)

def trunk_created(self, resource, event, trunk_plugin, payload):
trunk = payload.states[0]
subports = trunk.sub_ports
if subports:
self._subports_added(subports)
8 changes: 8 additions & 0 deletions python/neutron-understack/neutron_understack/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from neutron.objects import ports as port_obj
from neutron_lib import context as n_context


def fetch_subport_network_id(subport_id):
context = n_context.get_admin_context()
neutron_port = port_obj.Port.get_object(context, id=subport_id)
return neutron_port.network_id