|
| 1 | +From f7d06492fe30357ec3bc09d319f050adbf18aced Mon Sep 17 00:00:00 2001 |
| 2 | +From: Doug Goldstein < [email protected]> |
| 3 | +Date: Wed, 22 Oct 2025 12:58:41 -0500 |
| 4 | +Subject: [PATCH] pass along physical_network to neutron from the baremetal |
| 5 | + port |
| 6 | + |
| 7 | +When plugging a baremetal port in using the 'neutron' interface, send |
| 8 | +the 'physical_network' value of the baremetal port to Neutron as part of the |
| 9 | +binding_profile for the port. This can be useful for VXLAN underlay |
| 10 | +connected machines where the networks in Neutron are VXLAN networks |
| 11 | +which then have segments on them that are VLAN based segments which bind |
| 12 | +the VNI to a VLAN for attachment for the node to connect to the VNI. |
| 13 | + |
| 14 | +Ref: https://bugs.launchpad.net/ovn-bgp-agent/+bug/2017890 |
| 15 | +Ref: https://bugs.launchpad.net/neutron/+bug/2114451 |
| 16 | +Ref: https://review.opendev.org/c/openstack/neutron-specs/+/952166 |
| 17 | + |
| 18 | +Partial-Bug: #2105855 |
| 19 | +Assisted-by: Claude Code 2.0 |
| 20 | +Change-Id: I6e0185e203489676d530e6955929997f4871b8fa |
| 21 | +Signed-off-by: Doug Goldstein < [email protected]> |
| 22 | +--- |
| 23 | + ironic/common/neutron.py | 4 ++ |
| 24 | + ironic/drivers/modules/network/common.py | 11 +++ |
| 25 | + ironic/tests/unit/common/test_neutron.py | 38 ++++++++++ |
| 26 | + .../drivers/modules/network/test_common.py | 71 +++++++++++++++++++ |
| 27 | + ...ude-physical-network-8d8cbe17716d341a.yaml | 6 ++ |
| 28 | + 5 files changed, 130 insertions(+) |
| 29 | + create mode 100644 releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml |
| 30 | + |
| 31 | +diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py |
| 32 | +index 7d4ce2db9..9f95073b7 100644 |
| 33 | +--- a/ironic/common/neutron.py |
| 34 | ++++ b/ironic/common/neutron.py |
| 35 | +@@ -371,6 +371,10 @@ def add_ports_to_network(task, network_uuid, security_groups=None): |
| 36 | + binding_profile['vtep-logical-switch'] = vtep_logical_switch |
| 37 | + binding_profile['vtep-physical-switch'] = vtep_physical_switch |
| 38 | + |
| 39 | ++ # Include physical_network if available |
| 40 | ++ if ironic_port.physical_network: |
| 41 | ++ binding_profile['physical_network'] = ironic_port.physical_network |
| 42 | ++ |
| 43 | + update_port_attrs['binding:profile'] = binding_profile |
| 44 | + |
| 45 | + if not ironic_port.pxe_enabled: |
| 46 | +diff --git a/ironic/drivers/modules/network/common.py b/ironic/drivers/modules/network/common.py |
| 47 | +index 19c30a4dd..8d033984d 100644 |
| 48 | +--- a/ironic/drivers/modules/network/common.py |
| 49 | ++++ b/ironic/drivers/modules/network/common.py |
| 50 | +@@ -272,6 +272,17 @@ def plug_port_to_tenant_network(task, port_like_obj, client=None): |
| 51 | + binding_profile = {'local_link_information': local_link_info} |
| 52 | + if local_group_info: |
| 53 | + binding_profile['local_group_information'] = local_group_info |
| 54 | ++ |
| 55 | ++ # Include physical_network if available |
| 56 | ++ if isinstance(port_like_obj, objects.Portgroup): |
| 57 | ++ # For portgroups, get physical_network from the first port |
| 58 | ++ pg_ports = [p for p in task.ports |
| 59 | ++ if p.portgroup_id == port_like_obj.id] |
| 60 | ++ if pg_ports and pg_ports[0].physical_network: |
| 61 | ++ binding_profile['physical_network'] = pg_ports[0].physical_network |
| 62 | ++ elif port_like_obj.physical_network: |
| 63 | ++ binding_profile['physical_network'] = port_like_obj.physical_network |
| 64 | ++ |
| 65 | + port_attrs['binding:profile'] = binding_profile |
| 66 | + |
| 67 | + if client_id_opt: |
| 68 | +diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py |
| 69 | +index 406e42a7e..4bc0140b9 100644 |
| 70 | +--- a/ironic/tests/unit/common/test_neutron.py |
| 71 | ++++ b/ironic/tests/unit/common/test_neutron.py |
| 72 | +@@ -329,6 +329,44 @@ class TestNeutronNetworkActions(db_base.DbTestCase): |
| 73 | + self._test_add_ports_to_network(is_client_id=False, |
| 74 | + security_groups=sg_ids) |
| 75 | + |
| 76 | ++ @mock.patch.object(neutron, 'update_neutron_port', autospec=True) |
| 77 | ++ def test_add_ports_to_network_with_physical_network(self, update_mock): |
| 78 | ++ # Test that physical_network is included in binding:profile |
| 79 | ++ self.node.network_interface = 'neutron' |
| 80 | ++ self.node.save() |
| 81 | ++ port = self.ports[0] |
| 82 | ++ port.physical_network = 'physnet1' |
| 83 | ++ port.save() |
| 84 | ++ |
| 85 | ++ expected_create_attrs = { |
| 86 | ++ 'network_id': self.network_uuid, |
| 87 | ++ 'admin_state_up': True, |
| 88 | ++ 'binding:vnic_type': 'baremetal', |
| 89 | ++ 'device_id': self.node.uuid |
| 90 | ++ } |
| 91 | ++ expected_update_attrs = { |
| 92 | ++ 'device_owner': 'baremetal:none', |
| 93 | ++ 'binding:host_id': self.node.uuid, |
| 94 | ++ 'mac_address': port.address, |
| 95 | ++ 'binding:profile': { |
| 96 | ++ 'local_link_information': [port.local_link_connection], |
| 97 | ++ 'physical_network': 'physnet1' |
| 98 | ++ } |
| 99 | ++ } |
| 100 | ++ |
| 101 | ++ self.client_mock.create_port.return_value = self.neutron_port |
| 102 | ++ update_mock.return_value = self.neutron_port |
| 103 | ++ expected = {port.uuid: self.neutron_port['id']} |
| 104 | ++ |
| 105 | ++ with task_manager.acquire(self.context, self.node.uuid) as task: |
| 106 | ++ ports = neutron.add_ports_to_network(task, self.network_uuid) |
| 107 | ++ self.assertEqual(expected, ports) |
| 108 | ++ self.client_mock.create_port.assert_called_once_with( |
| 109 | ++ **expected_create_attrs) |
| 110 | ++ update_mock.assert_called_once_with( |
| 111 | ++ self.context, self.neutron_port['id'], |
| 112 | ++ expected_update_attrs) |
| 113 | ++ |
| 114 | + @mock.patch.object(neutron, 'update_neutron_port', autospec=True) |
| 115 | + def test__add_ip_addresses_for_ipv6_stateful(self, mock_update): |
| 116 | + subnet_id = uuidutils.generate_uuid() |
| 117 | +diff --git a/ironic/tests/unit/drivers/modules/network/test_common.py b/ironic/tests/unit/drivers/modules/network/test_common.py |
| 118 | +index 286ac35d3..0e01d43c7 100644 |
| 119 | +--- a/ironic/tests/unit/drivers/modules/network/test_common.py |
| 120 | ++++ b/ironic/tests/unit/drivers/modules/network/test_common.py |
| 121 | +@@ -489,6 +489,77 @@ class TestCommonFunctions(db_base.DbTestCase): |
| 122 | + nclient, self.vif_id, 'ACTIVE', fail_on_binding_failure=True) |
| 123 | + self.assertTrue(mock_update.called) |
| 124 | + |
| 125 | ++ @mock.patch.object(neutron_common, 'update_neutron_port', autospec=True) |
| 126 | ++ @mock.patch.object(neutron_common, 'wait_for_port_status', autospec=True) |
| 127 | ++ @mock.patch.object(neutron_common, 'get_client', autospec=True) |
| 128 | ++ def test_plug_port_to_tenant_network_with_physical_network( |
| 129 | ++ self, mock_gc, wait_mock_status, mock_update): |
| 130 | ++ # Test that physical_network is included in binding:profile for port |
| 131 | ++ nclient = mock.MagicMock() |
| 132 | ++ mock_gc.return_value = nclient |
| 133 | ++ self.port.internal_info = {common.TENANT_VIF_KEY: self.vif_id} |
| 134 | ++ self.port.physical_network = 'physnet1' |
| 135 | ++ self.port.save() |
| 136 | ++ |
| 137 | ++ expected_attrs = { |
| 138 | ++ 'binding:vnic_type': neutron_common.VNIC_BAREMETAL, |
| 139 | ++ 'binding:host_id': self.node.uuid, |
| 140 | ++ 'mac_address': self.port.address, |
| 141 | ++ 'binding:profile': { |
| 142 | ++ 'local_link_information': [self.port.local_link_connection], |
| 143 | ++ 'physical_network': 'physnet1' |
| 144 | ++ } |
| 145 | ++ } |
| 146 | ++ |
| 147 | ++ with task_manager.acquire(self.context, self.node.id) as task: |
| 148 | ++ common.plug_port_to_tenant_network(task, self.port) |
| 149 | ++ mock_update.assert_called_once_with( |
| 150 | ++ task.context, self.vif_id, expected_attrs) |
| 151 | ++ |
| 152 | ++ @mock.patch.object(neutron_common, 'update_neutron_port', autospec=True) |
| 153 | ++ @mock.patch.object(neutron_common, 'wait_for_port_status', autospec=True) |
| 154 | ++ @mock.patch.object(neutron_common, 'get_client', autospec=True) |
| 155 | ++ def test_plug_portgroup_to_tenant_network_with_physical_network( |
| 156 | ++ self, mock_gc, wait_mock_status, mock_update): |
| 157 | ++ # Test that physical_network is included in binding:profile for |
| 158 | ++ # a portgroup |
| 159 | ++ nclient = mock.MagicMock() |
| 160 | ++ mock_gc.return_value = nclient |
| 161 | ++ pg = obj_utils.create_test_portgroup( |
| 162 | ++ self.context, node_id=self.node.id, address='00:54:00:cf:2d:01') |
| 163 | ++ port1 = obj_utils.create_test_port( |
| 164 | ++ self.context, node_id=self.node.id, address='52:54:00:cf:2d:01', |
| 165 | ++ portgroup_id=pg.id, uuid=uuidutils.generate_uuid(), |
| 166 | ++ physical_network='physnet1') |
| 167 | ++ port2 = obj_utils.create_test_port( |
| 168 | ++ self.context, node_id=self.node.id, address='52:54:00:cf:2d:02', |
| 169 | ++ portgroup_id=pg.id, uuid=uuidutils.generate_uuid(), |
| 170 | ++ physical_network='physnet1') |
| 171 | ++ pg.internal_info = {common.TENANT_VIF_KEY: self.vif_id} |
| 172 | ++ pg.save() |
| 173 | ++ |
| 174 | ++ expected_attrs = { |
| 175 | ++ 'binding:vnic_type': neutron_common.VNIC_BAREMETAL, |
| 176 | ++ 'binding:host_id': self.node.uuid, |
| 177 | ++ 'mac_address': pg.address, |
| 178 | ++ 'binding:profile': { |
| 179 | ++ 'local_link_information': [port1.local_link_connection, |
| 180 | ++ port2.local_link_connection], |
| 181 | ++ 'local_group_information': { |
| 182 | ++ 'id': pg.uuid, |
| 183 | ++ 'name': pg.name, |
| 184 | ++ 'bond_mode': pg.mode, |
| 185 | ++ 'bond_properties': {} |
| 186 | ++ }, |
| 187 | ++ 'physical_network': 'physnet1' |
| 188 | ++ } |
| 189 | ++ } |
| 190 | ++ |
| 191 | ++ with task_manager.acquire(self.context, self.node.id) as task: |
| 192 | ++ common.plug_port_to_tenant_network(task, pg) |
| 193 | ++ mock_update.assert_called_once_with( |
| 194 | ++ task.context, self.vif_id, expected_attrs) |
| 195 | ++ |
| 196 | + |
| 197 | + class TestVifPortIDMixin(db_base.DbTestCase): |
| 198 | + |
| 199 | +diff --git a/releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml b/releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml |
| 200 | +new file mode 100644 |
| 201 | +index 000000000..1a556d812 |
| 202 | +--- /dev/null |
| 203 | ++++ b/releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml |
| 204 | +@@ -0,0 +1,6 @@ |
| 205 | ++--- |
| 206 | ++features: |
| 207 | ++ - | |
| 208 | ++ When plugging a baremetal port in using the 'neutron' interface, send |
| 209 | ++ the 'physical_network' value of the baremetal port to Neutron as part of the |
| 210 | ++ binding_profile for the port. |
| 211 | +-- |
| 212 | +2.50.1 (Apple Git-155) |
0 commit comments