Skip to content

Commit e72d9aa

Browse files
dynamic lookup network nodeId
1 parent 11b1ce1 commit e72d9aa

File tree

7 files changed

+177
-18
lines changed

7 files changed

+177
-18
lines changed

python/neutron-understack/neutron_understack/config.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,6 @@
5050
"is used to trunk all vlans used by a neutron router."
5151
),
5252
),
53-
cfg.StrOpt(
54-
"network_node_trunk_uuid",
55-
help=(
56-
"UUID of the trunk that is used to trunk all vlans used by a Neutron"
57-
" router."
58-
),
59-
),
6053
cfg.StrOpt(
6154
"network_node_switchport_physnet",
6255
help=(

python/neutron-understack/neutron_understack/ironic.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,10 @@ def _port_by_local_link(self, local_link_info: dict) -> BaremetalPort | None:
3232
)
3333
except StopIteration:
3434
return None
35+
36+
def baremetal_node_name(self, node_uuid: str) -> str | None:
37+
try:
38+
node = self.irclient.get_node(node_uuid)
39+
return node.name if node else None
40+
except Exception:
41+
return None

python/neutron-understack/neutron_understack/routers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def add_subport_to_trunk(shared_port: PortDict, segment: NetworkSegmentDict) ->
100100
}
101101
utils.fetch_trunk_plugin().add_subports(
102102
context=n_context.get_admin_context(),
103-
trunk_id=cfg.CONF.ml2_understack.network_node_trunk_uuid,
103+
trunk_id=utils.fetch_network_node_trunk_id(),
104104
subports=subports,
105105
)
106106

@@ -251,8 +251,7 @@ def handle_router_interface_removal(_resource, _event, trigger, payload) -> None
251251

252252
def handle_subport_removal(port: Port) -> None:
253253
"""Removes router's subport from a network node trunk."""
254-
# trunk_id will be discovered dynamically at some point
255-
trunk_id = cfg.CONF.ml2_understack.network_node_trunk_uuid
254+
trunk_id = utils.fetch_network_node_trunk_id()
256255
LOG.debug("Router, Removing subport: %s(port)s", {"port": port})
257256
port_id = port["id"]
258257
try:

python/neutron-understack/neutron_understack/tests/test_routers.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def test_when_successful(self, mocker):
3737
port = {"id": "port-123"}
3838
segment = {"segmentation_id": 42}
3939
mocker.patch(
40-
"oslo_config.cfg.CONF.ml2_understack.network_node_trunk_uuid",
41-
trunk_id,
40+
"neutron_understack.utils.fetch_network_node_trunk_id",
41+
return_value=trunk_id,
4242
)
4343
mocker.patch(
4444
"neutron_lib.context.get_admin_context", return_value="admin_context"
@@ -70,7 +70,8 @@ def test_when_successful(self, mocker):
7070
class TestHandleSubportRemoval:
7171
def test_when_successful(self, mocker, port_id, trunk_id):
7272
mocker.patch(
73-
"oslo_config.cfg.CONF.ml2_understack.network_node_trunk_uuid", str(trunk_id)
73+
"neutron_understack.utils.fetch_network_node_trunk_id",
74+
return_value=str(trunk_id),
7475
)
7576
mock_remove = mocker.patch("neutron_understack.utils.remove_subport_from_trunk")
7677
port = {"id": str(port_id)}

python/neutron-understack/neutron_understack/tests/test_trunk.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,13 @@ class TestCheckSubportsSegmentationId:
336336
def test_when_trunk_id_is_network_node_trunk_id(
337337
self,
338338
mocker,
339-
oslo_config,
340339
understack_trunk_driver,
341340
trunk_id,
342341
):
343-
oslo_config.config(
344-
network_node_trunk_uuid=str(trunk_id),
345-
group="ml2_understack",
342+
# Mock fetch_network_node_trunk_id to return the trunk_id
343+
mocker.patch(
344+
"neutron_understack.utils.fetch_network_node_trunk_id",
345+
return_value=str(trunk_id),
346346
)
347347
# Mock to ensure the function returns early and doesn't call this
348348
allowed_ranges_mock = mocker.patch(
@@ -362,6 +362,11 @@ def test_when_segmentation_id_is_in_allowed_range(
362362
trunk_id,
363363
subport,
364364
):
365+
# Mock fetch_network_node_trunk_id to return a different trunk ID
366+
mocker.patch(
367+
"neutron_understack.utils.fetch_network_node_trunk_id",
368+
return_value="different-trunk-id",
369+
)
365370
allowed_ranges = mocker.patch(
366371
"neutron_understack.utils.allowed_tenant_vlan_id_ranges",
367372
return_value=[(1, 1500)],
@@ -380,6 +385,11 @@ def test_when_segmentation_id_is_not_in_allowed_range(
380385
trunk_id,
381386
subport,
382387
):
388+
# Mock fetch_network_node_trunk_id to return a different trunk ID
389+
mocker.patch(
390+
"neutron_understack.utils.fetch_network_node_trunk_id",
391+
return_value="different-trunk-id",
392+
)
383393
mocker.patch(
384394
"neutron_understack.utils.allowed_tenant_vlan_id_ranges",
385395
return_value=[(1, 1500)],

python/neutron-understack/neutron_understack/trunk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def _check_subports_segmentation_id(
129129
segment VLAN tags allocated to the subports. Therefore, there is no
130130
possibility of conflict with the native VLAN.
131131
"""
132-
if trunk_id == cfg.CONF.ml2_understack.network_node_trunk_uuid:
132+
if trunk_id == utils.fetch_network_node_trunk_id():
133133
return
134134

135135
ns_ranges = utils.allowed_tenant_vlan_id_ranges()

python/neutron-understack/neutron_understack/utils.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import logging
2+
import re
13
from contextlib import contextmanager
24

5+
from neutron.common.ovn import constants as ovn_const
36
from neutron.db import models_v2
47
from neutron.objects import ports as port_obj
8+
from neutron.objects import trunk as trunk_obj
59
from neutron.objects.network import NetworkSegment
610
from neutron.objects.network_segment_range import NetworkSegmentRange
711
from neutron.plugins.ml2.driver_context import portbindings
@@ -14,10 +18,13 @@
1418
from neutron_lib.plugins.ml2 import api
1519
from oslo_config import cfg
1620

21+
from neutron_understack.ironic import IronicClient
1722
from neutron_understack.ml2_type_annotations import NetworkSegmentDict
1823
from neutron_understack.ml2_type_annotations import PortContext
1924
from neutron_understack.ml2_type_annotations import PortDict
2025

26+
LOG = logging.getLogger(__name__)
27+
2128

2229
def fetch_port_object(port_id: str) -> port_obj.Port:
2330
context = n_context.get_admin_context()
@@ -101,6 +108,148 @@ def fetch_trunk_plugin() -> TrunkPlugin:
101108
return trunk_plugin
102109

103110

111+
def _is_uuid(value: str) -> bool:
112+
"""Check if a string is a UUID."""
113+
uuid_pattern = re.compile(
114+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
115+
)
116+
return bool(uuid_pattern.match(value))
117+
118+
119+
def fetch_network_node_trunk_id() -> str:
120+
"""Dynamically discover the network node trunk ID via OVN Gateway agent.
121+
122+
This function discovers the network node trunk by:
123+
1. Finding alive OVN Controller Gateway agents
124+
2. Getting the host of the gateway agent
125+
3. Finding trunks whose parent port is bound to that host
126+
(resolving baremetal node UUIDs to names via Ironic if needed)
127+
4. Returning the first matching trunk
128+
129+
The network node trunk is used to connect router networks to the
130+
network node (OVN gateway) by adding subports for each VLAN.
131+
132+
Returns:
133+
str: The UUID of the network node trunk
134+
135+
Raises:
136+
Exception: If no gateway agent or suitable trunk is found
137+
138+
Example:
139+
>>> trunk_id = fetch_network_node_trunk_id()
140+
>>> print(trunk_id)
141+
'2e558202-0bd0-4971-a9f8-61d1adea0427'
142+
"""
143+
context = n_context.get_admin_context()
144+
core_plugin = directory.get_plugin()
145+
146+
if not core_plugin:
147+
raise Exception("Unable to obtain core plugin")
148+
149+
# Find OVN Controller Gateway agents that are alive
150+
LOG.debug("Looking for OVN Controller Gateway agents")
151+
gateway_agents = core_plugin.get_agents(
152+
context,
153+
filters={"agent_type": [ovn_const.OVN_CONTROLLER_GW_AGENT], "alive": [True]},
154+
)
155+
156+
if not gateway_agents:
157+
raise Exception(
158+
"No alive OVN Controller Gateway agents found. "
159+
"Please ensure the network node is running and the "
160+
"OVN gateway agent is active."
161+
)
162+
163+
# Use the first gateway agent's host
164+
# TODO: In the future, support multiple gateway agents for HA
165+
gateway_host = gateway_agents[0]["host"]
166+
LOG.debug(
167+
"Found OVN Gateway agent on host: %s (agent_id: %s)",
168+
gateway_host,
169+
gateway_agents[0]["id"],
170+
)
171+
172+
# Get all trunks
173+
trunks = trunk_obj.Trunk.get_objects(context)
174+
175+
if not trunks:
176+
raise Exception("No trunks found in the system")
177+
178+
ironic_client = IronicClient()
179+
# Find trunk whose parent port is bound to the gateway host
180+
for trunk in trunks:
181+
try:
182+
parent_port = port_obj.Port.get_object(context, id=trunk.port_id)
183+
184+
if not parent_port or not parent_port.bindings:
185+
LOG.debug(
186+
"Trunk %s: parent port %s has no bindings, skipping",
187+
trunk.id,
188+
trunk.port_id,
189+
)
190+
continue
191+
192+
binding = parent_port.bindings[0]
193+
port_host = binding.host
194+
195+
# If port is bound to a UUID (baremetal node), resolve it to name
196+
if _is_uuid(port_host):
197+
LOG.debug(
198+
"Trunk %s: port bound to baremetal node UUID %s, resolving to name",
199+
trunk.id,
200+
port_host,
201+
)
202+
resolved_name = ironic_client.baremetal_node_name(port_host)
203+
if resolved_name:
204+
LOG.debug(
205+
"Trunk %s: resolved baremetal node %s to name %s",
206+
trunk.id,
207+
port_host,
208+
resolved_name,
209+
)
210+
port_host = resolved_name
211+
else:
212+
LOG.debug(
213+
"Trunk %s: failed to resolve baremetal node %s, skipping",
214+
trunk.id,
215+
port_host,
216+
)
217+
continue
218+
219+
# Compare resolved host with gateway host
220+
if port_host == gateway_host:
221+
LOG.info(
222+
"Found network node trunk: %s (parent_port: %s, host: %s)",
223+
trunk.id,
224+
trunk.port_id,
225+
gateway_host,
226+
)
227+
return str(trunk.id)
228+
else:
229+
LOG.debug(
230+
"Trunk %s: parent port bound to host %s (looking for %s), skipping",
231+
trunk.id,
232+
port_host,
233+
gateway_host,
234+
)
235+
236+
except Exception as e:
237+
LOG.debug(
238+
"Error checking trunk %s: %s. Continuing to next trunk.",
239+
trunk.id,
240+
str(e),
241+
)
242+
continue
243+
244+
# If we get here, no suitable trunk was found
245+
raise Exception(
246+
f"Unable to find network node trunk on gateway host '{gateway_host}'. "
247+
f"Found {len(trunks)} trunk(s) but none have a parent port "
248+
f"bound to the gateway host. "
249+
"Please ensure a trunk exists with a parent port on the network node."
250+
)
251+
252+
104253
def allocate_dynamic_segment(
105254
network_id: str,
106255
network_type: str = "vlan",

0 commit comments

Comments
 (0)