From e721c56e681a2f20a0f7edd2605339c3e6b275f3 Mon Sep 17 00:00:00 2001 From: Doug Goldstein Date: Wed, 22 Oct 2025 12:58:41 -0500 Subject: [PATCH] pass along physical_network to neutron from the baremetal port When plugging a baremetal port in using the 'neutron' interface, send the 'physical_network' value of the baremetal port to Neutron as part of the binding_profile for the port. This can be useful for VXLAN underlay connected machines where the networks in Neutron are VXLAN networks which then have segments on them that are VLAN based segments which bind the VNI to a VLAN for attachment for the node to connect to the VNI. Ref: https://bugs.launchpad.net/ovn-bgp-agent/+bug/2017890 Ref: https://bugs.launchpad.net/neutron/+bug/2114451 Ref: https://review.opendev.org/c/openstack/neutron-specs/+/952166 Partial-Bug: #2105855 Assisted-by: Claude Code 2.0 Change-Id: I6e0185e203489676d530e6955929997f4871b8fa Signed-off-by: Doug Goldstein --- ironic/common/neutron.py | 4 ++ ironic/drivers/modules/network/common.py | 5 ++ ironic/tests/unit/common/test_neutron.py | 38 ++++++++++ .../drivers/modules/network/test_common.py | 70 +++++++++++++++++++ ...ude-physical-network-8d8cbe17716d341a.yaml | 6 ++ 5 files changed, 123 insertions(+) create mode 100644 releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py index 7d4ce2db98..9f95073b70 100644 --- a/ironic/common/neutron.py +++ b/ironic/common/neutron.py @@ -371,6 +371,10 @@ def add_ports_to_network(task, network_uuid, security_groups=None): binding_profile['vtep-logical-switch'] = vtep_logical_switch binding_profile['vtep-physical-switch'] = vtep_physical_switch + # Include physical_network if available + if ironic_port.physical_network: + binding_profile['physical_network'] = ironic_port.physical_network + update_port_attrs['binding:profile'] = binding_profile if not ironic_port.pxe_enabled: diff --git a/ironic/drivers/modules/network/common.py b/ironic/drivers/modules/network/common.py index 19c30a4dd6..474717162a 100644 --- a/ironic/drivers/modules/network/common.py +++ b/ironic/drivers/modules/network/common.py @@ -272,6 +272,11 @@ def plug_port_to_tenant_network(task, port_like_obj, client=None): binding_profile = {'local_link_information': local_link_info} if local_group_info: binding_profile['local_group_information'] = local_group_info + + # Include physical_network if available + if port_like_obj.physical_network: + binding_profile['physical_network'] = port_like_obj.physical_network + port_attrs['binding:profile'] = binding_profile if client_id_opt: diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py index 406e42a7ef..4bc0140b9d 100644 --- a/ironic/tests/unit/common/test_neutron.py +++ b/ironic/tests/unit/common/test_neutron.py @@ -329,6 +329,44 @@ class TestNeutronNetworkActions(db_base.DbTestCase): self._test_add_ports_to_network(is_client_id=False, security_groups=sg_ids) + @mock.patch.object(neutron, 'update_neutron_port', autospec=True) + def test_add_ports_to_network_with_physical_network(self, update_mock): + # Test that physical_network is included in binding:profile + self.node.network_interface = 'neutron' + self.node.save() + port = self.ports[0] + port.physical_network = 'physnet1' + port.save() + + expected_create_attrs = { + 'network_id': self.network_uuid, + 'admin_state_up': True, + 'binding:vnic_type': 'baremetal', + 'device_id': self.node.uuid + } + expected_update_attrs = { + 'device_owner': 'baremetal:none', + 'binding:host_id': self.node.uuid, + 'mac_address': port.address, + 'binding:profile': { + 'local_link_information': [port.local_link_connection], + 'physical_network': 'physnet1' + } + } + + self.client_mock.create_port.return_value = self.neutron_port + update_mock.return_value = self.neutron_port + expected = {port.uuid: self.neutron_port['id']} + + with task_manager.acquire(self.context, self.node.uuid) as task: + ports = neutron.add_ports_to_network(task, self.network_uuid) + self.assertEqual(expected, ports) + self.client_mock.create_port.assert_called_once_with( + **expected_create_attrs) + update_mock.assert_called_once_with( + self.context, self.neutron_port['id'], + expected_update_attrs) + @mock.patch.object(neutron, 'update_neutron_port', autospec=True) def test__add_ip_addresses_for_ipv6_stateful(self, mock_update): subnet_id = uuidutils.generate_uuid() diff --git a/ironic/tests/unit/drivers/modules/network/test_common.py b/ironic/tests/unit/drivers/modules/network/test_common.py index 05b4adec18..6e27b04921 100644 --- a/ironic/tests/unit/drivers/modules/network/test_common.py +++ b/ironic/tests/unit/drivers/modules/network/test_common.py @@ -492,6 +492,76 @@ class TestCommonFunctions(db_base.DbTestCase): nclient, self.vif_id, 'ACTIVE', fail_on_binding_failure=True) self.assertTrue(mock_update.called) + @mock.patch.object(neutron_common, 'update_neutron_port', autospec=True) + @mock.patch.object(neutron_common, 'wait_for_port_status', autospec=True) + @mock.patch.object(neutron_common, 'get_client', autospec=True) + def test_plug_port_to_tenant_network_with_physical_network( + self, mock_gc, wait_mock_status, mock_update): + # Test that physical_network is included in binding:profile for port + nclient = mock.MagicMock() + mock_gc.return_value = nclient + self.port.internal_info = {common.TENANT_VIF_KEY: self.vif_id} + self.port.physical_network = 'physnet1' + self.port.save() + + expected_attrs = { + 'binding:vnic_type': neutron_common.VNIC_BAREMETAL, + 'binding:host_id': self.node.uuid, + 'mac_address': self.port.address, + 'binding:profile': { + 'local_link_information': [self.port.local_link_connection], + 'physical_network': 'physnet1' + } + } + + with task_manager.acquire(self.context, self.node.id) as task: + common.plug_port_to_tenant_network(task, self.port) + mock_update.assert_called_once_with( + task.context, self.vif_id, expected_attrs) + + @mock.patch.object(neutron_common, 'update_neutron_port', autospec=True) + @mock.patch.object(neutron_common, 'wait_for_port_status', autospec=True) + @mock.patch.object(neutron_common, 'get_client', autospec=True) + def test_plug_portgroup_to_tenant_network_with_physical_network( + self, mock_gc, wait_mock_status, mock_update): + # Test that physical_network is included in binding:profile for + # a portgroup + nclient = mock.MagicMock() + mock_gc.return_value = nclient + pg = obj_utils.create_test_portgroup( + self.context, node_id=self.node.id, address='00:54:00:cf:2d:01', + physical_network='physnet1') + port1 = obj_utils.create_test_port( + self.context, node_id=self.node.id, address='52:54:00:cf:2d:01', + portgroup_id=pg.id, uuid=uuidutils.generate_uuid()) + port2 = obj_utils.create_test_port( + self.context, node_id=self.node.id, address='52:54:00:cf:2d:02', + portgroup_id=pg.id, uuid=uuidutils.generate_uuid()) + pg.internal_info = {common.TENANT_VIF_KEY: self.vif_id} + pg.save() + + expected_attrs = { + 'binding:vnic_type': neutron_common.VNIC_BAREMETAL, + 'binding:host_id': self.node.uuid, + 'mac_address': pg.address, + 'binding:profile': { + 'local_link_information': [port1.local_link_connection, + port2.local_link_connection], + 'local_group_information': { + 'id': pg.uuid, + 'name': pg.name, + 'bond_mode': pg.mode, + 'bond_properties': {} + }, + 'physical_network': 'physnet1' + } + } + + with task_manager.acquire(self.context, self.node.id) as task: + common.plug_port_to_tenant_network(task, pg) + mock_update.assert_called_once_with( + task.context, self.vif_id, expected_attrs) + class TestVifPortIDMixin(db_base.DbTestCase): diff --git a/releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml b/releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml new file mode 100644 index 0000000000..1a556d812e --- /dev/null +++ b/releasenotes/notes/neutron-port-binding-include-physical-network-8d8cbe17716d341a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + When plugging a baremetal port in using the 'neutron' interface, send + the 'physical_network' value of the baremetal port to Neutron as part of the + binding_profile for the port.