@@ -66,6 +66,16 @@ def _snapshot(**changes):
6666 return palo_alto_wiring .AttachmentSnapshot (** {** fields , ** changes })
6767
6868
69+ def _ironic_port (physical_network = "physnet-1" , local_link_connection = None ):
70+ """An Ironic baremetal port; bindable unless a field is overridden empty."""
71+ if local_link_connection is None :
72+ local_link_connection = {"switch_id" : "aa" , "port_id" : "Eth1/1" }
73+ return SimpleNamespace (
74+ physical_network = physical_network ,
75+ local_link_connection = local_link_connection ,
76+ )
77+
78+
6979@pytest .fixture
7080def core_plugin (mocker ):
7181 core = mocker .Mock ()
@@ -131,6 +141,7 @@ def test_no_subport_vlan_available_is_conflict(self):
131141 [
132142 palo_alto_wiring .PaloAltoNodeNotAdopted ,
133143 palo_alto_wiring .PaloAltoParentNotAnnotated ,
144+ palo_alto_wiring .PaloAltoNodeHasNoUsablePort ,
134145 ],
135146 )
136147 def test_wiring_errors_are_bad_request (self , exc ):
@@ -399,6 +410,52 @@ def test_reuses_existing_trunk(self, wiring, trunk_plugin):
399410 trunk_plugin .create_trunk .assert_not_called ()
400411
401412
413+ class TestCheckCanAttach :
414+ """The read-only check run before Neutron creates a gateway."""
415+
416+ def test_passes_when_node_has_a_bindable_port (
417+ self , wiring , ironic , node , core_plugin , trunk_plugin
418+ ):
419+ ironic .node_ports .return_value = [
420+ _ironic_port (physical_network = None ),
421+ _ironic_port (),
422+ ]
423+
424+ wiring .check_can_attach ("r1" )
425+
426+ ironic .node_ports .assert_called_once_with (node )
427+ # Nothing is created or attached: the check must have no side effects.
428+ ironic .attach_vif_to_node .assert_not_called ()
429+ core_plugin .create_network .assert_not_called ()
430+ core_plugin .create_port .assert_not_called ()
431+ trunk_plugin .create_trunk .assert_not_called ()
432+
433+ def test_rejects_router_without_adopted_node (self , wiring , ironic ):
434+ ironic .node_by_instance_uuid .return_value = None
435+
436+ with pytest .raises (palo_alto_wiring .PaloAltoNodeNotAdopted , match = "router r1 " ):
437+ wiring .check_can_attach ("r1" )
438+ ironic .node_ports .assert_not_called ()
439+
440+ @pytest .mark .parametrize (
441+ "ports" ,
442+ [
443+ [],
444+ [_ironic_port (physical_network = None )],
445+ [_ironic_port (local_link_connection = {})],
446+ ],
447+ ids = ["no-ports" , "no-physical-network" , "no-local-link-connection" ],
448+ )
449+ def test_rejects_node_without_bindable_port (self , wiring , ironic , node , ports ):
450+ ironic .node_ports .return_value = ports
451+
452+ with pytest .raises (
453+ palo_alto_wiring .PaloAltoNodeHasNoUsablePort ,
454+ match = "node node-1 for Palo Alto router r1" ,
455+ ):
456+ wiring .check_can_attach ("r1" )
457+
458+
402459class TestEnsureStack :
403460 def test_binds_parent_before_building_trunk (self , mocker , wiring ):
404461 parent = {"id" : "parent-1" }
0 commit comments