Skip to content

Commit d53f790

Browse files
authored
Merge pull request #668 from rackerlabs/understack_trunks
feat: understack trunk
2 parents 5368db2 + 55ed520 commit d53f790

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-0
lines changed

python/neutron-understack/neutron_understack/nautobot.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class NautobotNotFoundError(exc.NeutronException):
2222
message = "%(obj)s not found in Nautobot. ref=%(ref)s"
2323

2424

25+
class NautobotCustomFieldNotFoundError(exc.NeutronException):
26+
message = "Custom field with name %(cf_name)s not found for %(obj)s"
27+
28+
2529
class Nautobot:
2630
"""Basic Nautobot wrapper because pynautobot doesn't expose plugin APIs."""
2731

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

106+
def fetch_ucvni(self, network_id: str) -> dict:
107+
url = f"/api/plugins/undercloud-vni/ucvnis/{network_id}/"
108+
return self.make_api_request("GET", url)
109+
110+
def fetch_ucvni_tenant_vlan_id(self, network_id: str) -> int | None:
111+
ucvni_data = self.fetch_ucvni(network_id=network_id)
112+
custom_fields = ucvni_data.get("custom_fields", {})
113+
if "tenant_vlan_id" not in custom_fields:
114+
raise NautobotCustomFieldNotFoundError(
115+
cf_name="tenant_vlan_id", obj="UCVNI"
116+
)
117+
return custom_fields.get("tenant_vlan_id")
118+
102119
def fetch_namespace_by_name(self, name: str) -> str:
103120
url = f"/api/ipam/namespaces/?name={name}&depth=1"
104121
resp_data = self.make_api_request("GET", url)
@@ -139,6 +156,11 @@ def associate_subnet_with_network(
139156
}
140157
self.make_api_request("PATCH", url, payload)
141158

159+
def add_tenant_vlan_tag_to_ucvni(self, network_uuid: str, vlan_tag: int) -> dict:
160+
url = f"/api/plugins/undercloud-vni/ucvnis/{network_uuid}/"
161+
payload = {"custom_fields": {"tenant_vlan_id": vlan_tag}}
162+
return self.make_api_request("PATCH", url, payload)
163+
142164
def subnet_delete(self, uuid: str) -> dict:
143165
return self.make_api_request("DELETE", f"/api/ipam/prefixes/{uuid}/")
144166

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from unittest.mock import MagicMock
2+
from unittest.mock import patch
3+
4+
import pytest
5+
6+
from neutron_understack.nautobot import Nautobot
7+
from neutron_understack.neutron_understack_mech import UnderstackDriver
8+
from neutron_understack.trunk import SubportSegmentationIDError
9+
from neutron_understack.trunk import UnderStackTrunkDriver
10+
11+
12+
@pytest.fixture
13+
def subport() -> MagicMock:
14+
return MagicMock(port_id="portUUID", segmentation_id=555)
15+
16+
17+
@pytest.fixture
18+
def trunk(subport) -> MagicMock:
19+
return MagicMock(sub_ports=[subport])
20+
21+
22+
@pytest.fixture
23+
def payload_metadata(subport) -> dict:
24+
return {"subports": [subport]}
25+
26+
27+
@pytest.fixture
28+
def payload(payload_metadata, trunk) -> MagicMock:
29+
return MagicMock(metadata=payload_metadata, states=[trunk])
30+
31+
32+
@pytest.fixture
33+
def nautobot_client() -> Nautobot:
34+
return MagicMock(spec_set=Nautobot)
35+
36+
37+
driver = UnderstackDriver()
38+
driver.nb = Nautobot("", "")
39+
trunk_driver = UnderStackTrunkDriver.create(driver)
40+
41+
42+
@patch("neutron_understack.utils.fetch_subport_network_id", return_value="112233")
43+
def test_subports_added_when_ucvni_tenan_vlan_id_is_not_set_yet(
44+
nautobot_client, payload
45+
):
46+
trunk_driver.nb = nautobot_client
47+
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": None}
48+
nautobot_client.configure_mock(**attrs)
49+
trunk_driver.subports_added("", "", "", payload)
50+
51+
nautobot_client.add_tenant_vlan_tag_to_ucvni.assert_called_once_with(
52+
network_uuid="112233", vlan_tag=555
53+
)
54+
55+
56+
@patch("neutron_understack.utils.fetch_subport_network_id", return_value="223344")
57+
def test_subports_added_when_segmentation_id_is_different_to_tenant_vlan_id(
58+
nautobot_client, payload
59+
):
60+
trunk_driver.nb = nautobot_client
61+
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": 123}
62+
nautobot_client.configure_mock(**attrs)
63+
with pytest.raises(SubportSegmentationIDError):
64+
trunk_driver.subports_added("", "", "", payload)
65+
66+
67+
@patch("neutron_understack.utils.fetch_subport_network_id", return_value="112233")
68+
def test_trunk_created_when_ucvni_tenan_vlan_id_is_not_set_yet(
69+
nautobot_client, payload
70+
):
71+
trunk_driver.nb = nautobot_client
72+
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": None}
73+
nautobot_client.configure_mock(**attrs)
74+
trunk_driver.trunk_created("", "", "", payload)
75+
76+
nautobot_client.add_tenant_vlan_tag_to_ucvni.assert_called_once_with(
77+
network_uuid="112233", vlan_tag=555
78+
)
79+
80+
81+
@patch("neutron_understack.utils.fetch_subport_network_id", return_value="223344")
82+
def test_trunk_created_when_segmentation_id_is_different_to_tenant_vlan_id(
83+
nautobot_client, payload
84+
):
85+
trunk_driver.nb = nautobot_client
86+
attrs = {"fetch_ucvni_tenant_vlan_id.return_value": 123}
87+
nautobot_client.configure_mock(**attrs)
88+
with pytest.raises(SubportSegmentationIDError):
89+
trunk_driver.trunk_created("", "", "", payload)

python/neutron-understack/neutron_understack/trunk.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,51 @@
1+
from neutron.objects.trunk import SubPort
12
from neutron.services.trunk.drivers import base as trunk_base
3+
from neutron_lib import exceptions as exc
24
from neutron_lib.api.definitions import portbindings
5+
from neutron_lib.callbacks import events
6+
from neutron_lib.callbacks import registry
7+
from neutron_lib.callbacks import resources
38
from neutron_lib.services.trunk import constants as trunk_consts
49
from oslo_config import cfg
10+
from oslo_log import log
11+
12+
from neutron_understack import utils
13+
14+
LOG = log.getLogger(__name__)
515

616
SUPPORTED_INTERFACES = (portbindings.VIF_TYPE_OTHER,)
717

818
SUPPORTED_SEGMENTATION_TYPES = (trunk_consts.SEGMENTATION_TYPE_VLAN,)
919

1020

21+
class SubportSegmentationIDError(exc.NeutronException):
22+
message = (
23+
"Segmentation ID: %(seg_id)s cannot be set to the Subport: "
24+
"%(subport_id)s as there is already another Segmentation ID: "
25+
"%(nb_seg_id)s in use by the Network: %(net_id)s that is "
26+
"attached to the Subport. Please use %(nb_seg_id)s as "
27+
"segmentation_id for this subport."
28+
)
29+
30+
1131
class UnderStackTrunkDriver(trunk_base.DriverBase):
32+
def __init__(
33+
self,
34+
name,
35+
interfaces,
36+
segmentation_types,
37+
agent_type=None,
38+
can_trunk_bound_port=False,
39+
):
40+
super().__init__(
41+
name,
42+
interfaces,
43+
segmentation_types,
44+
agent_type=agent_type,
45+
can_trunk_bound_port=can_trunk_bound_port,
46+
)
47+
self.nb = self.plugin_driver.nb
48+
1249
@property
1350
def is_loaded(self):
1451
try:
@@ -26,3 +63,68 @@ def create(cls, plugin_driver):
2663
None,
2764
can_trunk_bound_port=True,
2865
)
66+
67+
@registry.receives(resources.TRUNK_PLUGIN, [events.AFTER_INIT])
68+
def register(self, resource, event, trigger, payload=None):
69+
super().register(resource, event, trigger, payload=payload)
70+
71+
registry.subscribe(
72+
self.subports_added,
73+
resources.SUBPORTS,
74+
events.AFTER_CREATE,
75+
cancellable=True,
76+
)
77+
registry.subscribe(
78+
self.trunk_created, resources.TRUNK, events.AFTER_CREATE, cancellable=True
79+
)
80+
81+
def _handle_segmentation_id_mismatch(
82+
self, subport: SubPort, ucvni_uuid: str, tenant_vlan_id: int
83+
) -> None:
84+
subport.delete()
85+
raise SubportSegmentationIDError(
86+
seg_id=subport.segmentation_id,
87+
net_id=ucvni_uuid,
88+
nb_seg_id=tenant_vlan_id,
89+
subport_id=subport.port_id,
90+
)
91+
92+
def _configure_tenant_vlan_id(self, ucvni_uuid: str, subport: SubPort) -> None:
93+
subport_seg_id = subport.segmentation_id
94+
self.nb.add_tenant_vlan_tag_to_ucvni(
95+
network_uuid=ucvni_uuid, vlan_tag=subport_seg_id
96+
)
97+
LOG.info(
98+
"Segmentation ID: %(seg_id)s is now set on Nautobot's UCVNI "
99+
"UUID: %(ucvni_uuid)s in the tenant_vlan_id custom field",
100+
{"seg_id": subport_seg_id, "ucvni_uuid": ucvni_uuid},
101+
)
102+
103+
def _subports_added(self, subports: list[SubPort]) -> None:
104+
for subport in subports:
105+
subport_network_id = utils.fetch_subport_network_id(
106+
subport_id=subport.port_id
107+
)
108+
ucvni_tenant_vlan_id = self.nb.fetch_ucvni_tenant_vlan_id(
109+
network_id=subport_network_id
110+
)
111+
if not ucvni_tenant_vlan_id:
112+
self._configure_tenant_vlan_id(
113+
ucvni_uuid=subport_network_id, subport=subport
114+
)
115+
elif ucvni_tenant_vlan_id != subport.segmentation_id:
116+
self._handle_segmentation_id_mismatch(
117+
subport=subport,
118+
ucvni_uuid=subport_network_id,
119+
tenant_vlan_id=ucvni_tenant_vlan_id,
120+
)
121+
122+
def subports_added(self, resource, event, trunk_plugin, payload):
123+
subports = payload.metadata["subports"]
124+
self._subports_added(subports)
125+
126+
def trunk_created(self, resource, event, trunk_plugin, payload):
127+
trunk = payload.states[0]
128+
subports = trunk.sub_ports
129+
if subports:
130+
self._subports_added(subports)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from neutron.objects import ports as port_obj
2+
from neutron_lib import context as n_context
3+
4+
5+
def fetch_subport_network_id(subport_id):
6+
context = n_context.get_admin_context()
7+
neutron_port = port_obj.Port.get_object(context, id=subport_id)
8+
return neutron_port.network_id

0 commit comments

Comments
 (0)