@@ -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.
6472class 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