Skip to content

Commit 152ce68

Browse files
authored
Merge pull request #1374 from rackerlabs/ironic-physical-network
feat(ironic): pass along the physical_network for port binding
2 parents f971dc0 + 5180d30 commit 152ce68

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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)

containers/ironic/patches/series

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
0002_skip_reboot_firmware_update.patch
22
0001-storage-controller_mode-update.patch
3+
0001-pass-along-physical_network-to-neutron-from-the-bare.patch

0 commit comments

Comments
 (0)