Skip to content

Commit e1dc599

Browse files
committed
give PaloAltoWiring a public, router_id-based API
1 parent 59f6e1d commit e1dc599

3 files changed

Lines changed: 170 additions & 189 deletions

File tree

‎python/neutron-understack/neutron_understack/l3_router/palo_alto.py‎

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def _process_router_create(self, resource, event, trigger, payload=None):
224224

225225
# Ensure the shared anchor network first: it is idempotent and meant to
226226
# persist, so creating it before adoption never strands an adopted node.
227-
self._wiring._ensure_anchor_network()
227+
self._wiring.ensure_anchor_network()
228228
self._ironic.adopt_node_for_router(
229229
node,
230230
project_id=router.get("project_id"),
@@ -282,25 +282,22 @@ def _process_gateway_create(self, resource, event, trigger, payload=None):
282282
the parent is bound.
283283
"""
284284
router_id = payload.resource_id
285-
router = self._palo_alto_router(payload.context, router_id)
286-
if router is None:
285+
if self._palo_alto_router(payload.context, router_id) is None:
287286
return
288287

289-
gateway_port = self._wiring._gateway_port_for_router(router_id)
288+
gateway_port = self._wiring.gateway_port_for_router(router_id)
290289
if gateway_port is None:
291290
raise PaloAltoGatewayPortNotFound(router_id=router_id)
292291

293-
parent = self._wiring._ensure_parent_port(router)
294-
parent = self._wiring._ensure_parent_vif_attached(router, parent)
295-
trunk = self._wiring._ensure_trunk(router, parent)
296-
self._wiring._add_gateway_subport(router, trunk, gateway_port)
292+
stack = self._wiring.ensure_stack(router_id)
293+
self._wiring.add_gateway_subport(router_id, stack.trunk, gateway_port)
297294

298295
LOG.info(
299296
"Attached Palo Alto router %s gateway port %s via parent %s trunk %s",
300297
router_id,
301298
gateway_port["id"],
302-
parent["id"],
303-
trunk["id"],
299+
stack.parent["id"],
300+
stack.trunk["id"],
304301
)
305302

306303
def _process_gateway_delete(self, resource, event, trigger, payload=None):
@@ -310,19 +307,18 @@ def _process_gateway_delete(self, resource, event, trigger, payload=None):
310307
remove its subport before Neutron deletes it.
311308
"""
312309
router_id = payload.resource_id
313-
router = self._palo_alto_router(payload.context, router_id)
314-
if router is None:
310+
if self._palo_alto_router(payload.context, router_id) is None:
315311
return
316312

317-
gateway_port = self._wiring._gateway_port_for_router(router_id)
313+
gateway_port = self._wiring.gateway_port_for_router(router_id)
318314
if gateway_port is None:
319315
LOG.debug(
320316
"Palo Alto router %s gateway cleanup skipped; gateway port not found",
321317
router_id,
322318
)
323319
return
324320

325-
self._wiring._cleanup_gateway_attachment(router, gateway_port)
321+
self._wiring.cleanup_attachment(router_id, gateway_port["id"], "gateway")
326322
LOG.info(
327323
"Cleaned Palo Alto router %s gateway attachment (port %s)",
328324
router_id,
@@ -332,8 +328,7 @@ def _process_gateway_delete(self, resource, event, trigger, payload=None):
332328
def _process_router_interface_create(self, resource, event, trigger, payload=None):
333329
"""Wire the interface before Neutron commits its RouterPort association."""
334330
router_id = payload.resource_id
335-
router = self._palo_alto_router(payload.context, router_id)
336-
if router is None:
331+
if self._palo_alto_router(payload.context, router_id) is None:
337332
return
338333

339334
interface_port = payload.metadata.get("port")
@@ -342,14 +337,12 @@ def _process_router_interface_create(self, resource, event, trigger, payload=Non
342337
if interface_port.get("device_owner") not in const.ROUTER_INTERFACE_OWNERS:
343338
return
344339

345-
self._interface_snapshots[payload] = (
346-
self._wiring._snapshot_interface_attachment(router_id, interface_port["id"])
340+
self._interface_snapshots[payload] = self._wiring.snapshot_interface_attachment(
341+
router_id, interface_port["id"]
347342
)
348343
try:
349-
parent = self._wiring._ensure_parent_port(router)
350-
parent = self._wiring._ensure_parent_vif_attached(router, parent)
351-
trunk = self._wiring._ensure_trunk(router, parent)
352-
self._wiring._add_interface_subport(router, trunk, interface_port)
344+
stack = self._wiring.ensure_stack(router_id)
345+
self._wiring.add_interface_subport(router_id, stack.trunk, interface_port)
353346
except Exception:
354347
# Attach-by-port is reverted with a port update by Neutron, so it
355348
# cannot rely on PORT/BEFORE_DELETE to undo partial realization.
@@ -360,8 +353,8 @@ def _process_router_interface_create(self, resource, event, trigger, payload=Non
360353
"Attached Palo Alto router %s interface port %s via parent %s trunk %s",
361354
router_id,
362355
interface_port["id"],
363-
parent["id"],
364-
trunk["id"],
356+
stack.parent["id"],
357+
stack.trunk["id"],
365358
)
366359

367360
def _rollback_interface_attachment(self, payload) -> None:
@@ -370,7 +363,7 @@ def _rollback_interface_attachment(self, payload) -> None:
370363
if snapshot is None:
371364
return
372365
try:
373-
self._wiring._undo_interface_attachment(snapshot)
366+
self._wiring.undo_interface_attachment(snapshot)
374367
except Exception:
375368
# Preserve the original attach error. Keep the snapshot so the
376369
# subsequent ABORT_CREATE can retry compensation if it failed here.
@@ -432,7 +425,7 @@ def _process_router_interface_port_delete(
432425
):
433426
return
434427

435-
self._wiring._cleanup_interface_attachment(router, port)
428+
self._wiring.cleanup_attachment(router_id, port["id"], "interface")
436429
LOG.info(
437430
"Cleaned Palo Alto router %s interface attachment (port %s)",
438431
router_id,

‎python/neutron-understack/neutron_understack/l3_router/palo_alto_wiring.py‎

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ class AttachmentSnapshot:
6060
parent_vif_attached: bool
6161

6262

63+
@dataclass(frozen=True)
64+
class AttachmentStack:
65+
"""A router's VIF-bound anchor parent port and the trunk built on it."""
66+
67+
parent: dict
68+
trunk: dict
69+
70+
6371
# Conflict -> HTTP 409: every allowed VLAN on the router's trunk is in use.
6472
class NoPaloAltoSubportVlanAvailable(n_exc.Conflict):
6573
message = (
@@ -173,7 +181,7 @@ def _ironic(self) -> IronicClient:
173181
def _trunk_plugin(self):
174182
return utils.fetch_trunk_plugin()
175183

176-
def _ensure_anchor_network(self) -> dict:
184+
def ensure_anchor_network(self) -> dict:
177185
"""Create the shared sentinel anchor network if it does not exist."""
178186
core_plugin = directory.get_plugin()
179187
admin_context = n_context.get_admin_context()
@@ -206,7 +214,7 @@ def _ensure_anchor_network(self) -> dict:
206214
},
207215
)
208216

209-
def _gateway_port_for_router(self, router_id: str) -> dict | None:
217+
def gateway_port_for_router(self, router_id: str) -> dict | None:
210218
"""Return the router's Neutron external-gateway port, or None.
211219
212220
The gateway port is owned by the router (``device_id == router_id``) with
@@ -237,7 +245,7 @@ def _parent_port_for_router(self, router_id: str) -> dict | None:
237245
"""Return the router's existing anchor-network parent port, or None."""
238246
core_plugin = directory.get_plugin()
239247
admin_context = n_context.get_admin_context()
240-
anchor_network = self._ensure_anchor_network()
248+
anchor_network = self.ensure_anchor_network()
241249
ports = core_plugin.get_ports(
242250
admin_context,
243251
filters={
@@ -247,7 +255,7 @@ def _parent_port_for_router(self, router_id: str) -> dict | None:
247255
)
248256
return ports[0] if ports else None
249257

250-
def _create_parent_port(self, router: dict) -> dict:
258+
def _create_parent_port(self, router_id: str) -> dict:
251259
"""Create the router's parent port on the anchor network.
252260
253261
vnic_type=baremetal so Ironic can VIF-attach it to the adopted node.
@@ -256,12 +264,12 @@ def _create_parent_port(self, router: dict) -> dict:
256264
"""
257265
core_plugin = directory.get_plugin()
258266
admin_context = n_context.get_admin_context()
259-
anchor_network = self._ensure_anchor_network()
260-
port_name = _parent_port_name(router["id"])
267+
anchor_network = self.ensure_anchor_network()
268+
port_name = _parent_port_name(router_id)
261269
LOG.info(
262270
"Creating Palo Alto anchor parent port %s for router %s",
263271
port_name,
264-
router["id"],
272+
router_id,
265273
)
266274
return core_plugin.create_port(
267275
admin_context,
@@ -271,7 +279,7 @@ def _create_parent_port(self, router: dict) -> dict:
271279
"network_id": anchor_network["id"],
272280
"admin_state_up": True,
273281
"device_owner": "",
274-
"device_id": router["id"],
282+
"device_id": router_id,
275283
"mac_address": "",
276284
"fixed_ips": [],
277285
"project_id": admin_context.project_id or "",
@@ -280,17 +288,17 @@ def _create_parent_port(self, router: dict) -> dict:
280288
},
281289
)
282290

283-
def _ensure_parent_port(self, router: dict) -> dict:
291+
def _ensure_parent_port(self, router_id: str) -> dict:
284292
"""Find-or-create the router's anchor-network parent port (idempotent)."""
285-
existing = self._parent_port_for_router(router["id"])
293+
existing = self._parent_port_for_router(router_id)
286294
if existing is not None:
287295
LOG.debug(
288296
"Reusing Palo Alto anchor parent port %s for router %s",
289297
existing["id"],
290-
router["id"],
298+
router_id,
291299
)
292300
return existing
293-
return self._create_parent_port(router)
301+
return self._create_parent_port(router_id)
294302

295303
def _fresh_port(self, port_id: str) -> dict:
296304
"""Re-read a port so callers see its current binding profile."""
@@ -318,7 +326,7 @@ def _vif_landed_after_error(
318326
return False
319327
return parent_port_id in vif_ids
320328

321-
def _ensure_parent_vif_attached(self, router: dict, parent_port: dict) -> dict:
329+
def _ensure_parent_vif_attached(self, router_id: str, parent_port: dict) -> dict:
322330
"""VIF-attach the parent port to the router's node (idempotent).
323331
324332
Attaching a single VIF; Ironic binds it to a free baremetal port on the
@@ -327,7 +335,6 @@ def _ensure_parent_vif_attached(self, router: dict, parent_port: dict) -> dict:
327335
328336
Returns a fresh copy of the parent port reflecting the new binding.
329337
"""
330-
router_id = router["id"]
331338
node = self._ironic().node_by_instance_uuid(router_id)
332339
if node is None:
333340
raise PaloAltoNodeNotAdopted(router_id=router_id)
@@ -382,15 +389,15 @@ def _trunk_for_router(self, router_id: str) -> dict | None:
382389
)
383390
return trunks[0] if trunks else None
384391

385-
def _create_trunk(self, router: dict, parent_port: dict) -> dict:
392+
def _create_trunk(self, router_id: str, parent_port: dict) -> dict:
386393
"""Create the router's trunk with the parent port as its trunk parent."""
387394
admin_context = n_context.get_admin_context()
388-
trunk_name = _trunk_name(router["id"])
395+
trunk_name = _trunk_name(router_id)
389396
LOG.info(
390397
"Creating Palo Alto trunk %s on parent port %s for router %s",
391398
trunk_name,
392399
parent_port["id"],
393-
router["id"],
400+
router_id,
394401
)
395402
return self._trunk_plugin().create_trunk(
396403
admin_context,
@@ -405,17 +412,29 @@ def _create_trunk(self, router: dict, parent_port: dict) -> dict:
405412
},
406413
)
407414

408-
def _ensure_trunk(self, router: dict, parent_port: dict) -> dict:
415+
def _ensure_trunk(self, router_id: str, parent_port: dict) -> dict:
409416
"""Find-or-create the router's trunk (idempotent)."""
410-
existing = self._trunk_for_router(router["id"])
417+
existing = self._trunk_for_router(router_id)
411418
if existing is not None:
412419
LOG.debug(
413420
"Reusing Palo Alto trunk %s for router %s",
414421
existing["id"],
415-
router["id"],
422+
router_id,
416423
)
417424
return existing
418-
return self._create_trunk(router, parent_port)
425+
return self._create_trunk(router_id, parent_port)
426+
427+
def ensure_stack(self, router_id: str) -> AttachmentStack:
428+
"""Find or create the router's parent port, VIF attachment and trunk.
429+
430+
Builds them in this order so the parent is VIF-bound before any subport
431+
is added: the trunk driver only programs the switchport once the parent
432+
is bound.
433+
"""
434+
parent = self._ensure_parent_port(router_id)
435+
parent = self._ensure_parent_vif_attached(router_id, parent)
436+
trunk = self._ensure_trunk(router_id, parent)
437+
return AttachmentStack(parent=parent, trunk=trunk)
419438

420439
def _next_available_subport_vlan(
421440
self, router_id: str, trunk: dict, start_vlan: int
@@ -433,7 +452,7 @@ def _next_available_subport_vlan(
433452

434453
def _add_router_port_subport(
435454
self,
436-
router: dict,
455+
router_id: str,
437456
trunk: dict,
438457
port: dict,
439458
label: PortLabel,
@@ -466,7 +485,7 @@ def _add_router_port_subport(
466485
port_id,
467486
trunk["id"],
468487
segmentation_id,
469-
router["id"],
488+
router_id,
470489
)
471490
with _device_id_cleared(port):
472491
return self._trunk_plugin().add_subports(
@@ -483,25 +502,25 @@ def _add_router_port_subport(
483502
},
484503
)
485504

486-
def _add_gateway_subport(
487-
self, router: dict, trunk: dict, gateway_port: dict
505+
def add_gateway_subport(
506+
self, router_id: str, trunk: dict, gateway_port: dict
488507
) -> dict:
489508
"""Add the gateway port to the trunk as a VLAN subport (idempotent)."""
490509
return self._add_router_port_subport(
491-
router, trunk, gateway_port, "gateway", lambda: GATEWAY_SUBPORT_VLAN
510+
router_id, trunk, gateway_port, "gateway", lambda: GATEWAY_SUBPORT_VLAN
492511
)
493512

494-
def _add_interface_subport(
495-
self, router: dict, trunk: dict, interface_port: dict
513+
def add_interface_subport(
514+
self, router_id: str, trunk: dict, interface_port: dict
496515
) -> dict:
497516
"""Add a router-interface port to the trunk as a VLAN subport."""
498517
return self._add_router_port_subport(
499-
router,
518+
router_id,
500519
trunk,
501520
interface_port,
502521
"interface",
503522
lambda: self._next_available_subport_vlan(
504-
router["id"], trunk, INTERFACE_SUBPORT_VLAN_START
523+
router_id, trunk, INTERFACE_SUBPORT_VLAN_START
505524
),
506525
)
507526

@@ -575,9 +594,9 @@ def _delete_parent_stack_if_unused(self, router_id: str, trunk: dict) -> None:
575594
self._trunk_plugin().delete_trunk(admin_context, trunk["id"])
576595
self._detach_and_delete_parent(router_id, parent_id)
577596

578-
def _cleanup_router_port_attachment(
597+
def cleanup_attachment(
579598
self,
580-
router: dict,
599+
router_id: str,
581600
port_id: str,
582601
label: PortLabel,
583602
) -> None:
@@ -587,7 +606,6 @@ def _cleanup_router_port_attachment(
587606
parent port was created/VIF-attached but before the trunk existed, there
588607
is no trunk to key off, so find and tear down the orphan parent directly.
589608
"""
590-
router_id = router["id"]
591609
trunk = self._trunk_for_router(router_id)
592610
if trunk is None:
593611
parent = self._parent_port_for_router(router_id)
@@ -607,15 +625,7 @@ def _cleanup_router_port_attachment(
607625
self._remove_router_port_subport(trunk, port_id, label)
608626
self._delete_parent_stack_if_unused(router_id, trunk)
609627

610-
def _cleanup_gateway_attachment(self, router: dict, gateway_port: dict) -> None:
611-
"""Clean up a gateway port's Palo Alto trunk attachment."""
612-
self._cleanup_router_port_attachment(router, gateway_port["id"], "gateway")
613-
614-
def _cleanup_interface_attachment(self, router: dict, interface_port: dict) -> None:
615-
"""Clean up a router-interface port's Palo Alto trunk attachment."""
616-
self._cleanup_router_port_attachment(router, interface_port["id"], "interface")
617-
618-
def _snapshot_interface_attachment(
628+
def snapshot_interface_attachment(
619629
self, router_id: str, port_id: str
620630
) -> AttachmentSnapshot:
621631
"""Record which wiring already exists before an interface attach."""
@@ -635,7 +645,7 @@ def _snapshot_interface_attachment(
635645
parent_vif_attached=parent_vif_attached,
636646
)
637647

638-
def _undo_interface_attachment(self, snapshot: AttachmentSnapshot) -> None:
648+
def undo_interface_attachment(self, snapshot: AttachmentSnapshot) -> None:
639649
"""Return the router's wiring to the state recorded in the snapshot.
640650
641651
Re-reads current state rather than replaying the calls that succeeded,

0 commit comments

Comments
 (0)