Skip to content

Commit 62f8d28

Browse files
umagobbezak
authored andcommitted
[OVN] Add baremetal support without Neutron DHCP agent for IPv4
This patch adds support for deploying baremetal nodes with OVN's built-in DHCP server for IPv4. Since Neutron API's for setting DHCP options is mostly a pass-thru, Ironic uses a dnsmasq syntax for setting the baremetal options [0]. Since this syntax is unlikely to change and it's only a tiny subset of what dnsmasq can offer this patch does translate that syntax used by Ironic and convert it to OVN's equivalent options. In this way we do not need to re-design Neutron's DHCP options API nor change Ironic to use it with ML2/OVN. This option also adds a new configuration option called "disable_ovn_dhcp_for_baremetal_ports". PXE booting nodes can be very sensitive and operators may prefer to use a fully-fledged DHCP server to do it (even Ironic makes DHCP pluggable). So if operators wish to disable OVN's built-in DHCP server for baremetal provisioning they can do so by setting this new option to True. It defaults to False. This change has been tested with real hardware and it does work. That said, we found a problem in core OVN itself [1] while testing it that can affect PXE from reaching the TFTP server, we already communicated this with the core OVN folks and we hope it can be fixed soon. The change in core OVN should not affect the Neutron change tho. Not that the "server-ip-address" DHCP Option now points to the "next_server" option in OVN instead of the "tftp_server_address". The previous behavior was wrong, the "server-ip-address" should set the "siaddr" in the DHCP header and this has been introduced in OVN [2] as an option called "next_server". [0] https://github.com/openstack/ironic/blob/49113385e89c52b56152418d3a0c8c69ddaf8b6e/ironic/common/pxe_utils.py#L523-L538 [1] https://mail.openvswitch.org/pipermail/ovs-discuss/2022-May/051821.html [2] https://patchwork.ozlabs.org/project/ovn/patch/[email protected]/ Partial-Bug: #1971431 Change-Id: Ia041f640293ba26abf9f70af915817e9861e8ffc Signed-off-by: Lucas Alvares Gomes <[email protected]> (cherry picked from commit e73a85f)
1 parent 5684138 commit 62f8d28

File tree

9 files changed

+262
-50
lines changed

9 files changed

+262
-50
lines changed

doc/source/ovn/gaps.rst

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,25 @@ at [1]_.
3232
can announce host routes for both floating and fixed IP addresses. These
3333
functions are not supported in OVN.
3434

35-
* Baremetal provisioning with iPXE
35+
* Baremetal provisioning with iPXE without Neutron DHCP agent for IPv6
3636

37-
The core OVN DHCP server implementation does not have support for
38-
sending different boot options based on the ``gpxe`` DHCP Option
39-
(no. 175). Also, Ironic uses dnsmasq syntax when configuring the DHCP
40-
options for Neutron [2]_ which is not understood by the OVN driver.
37+
The core OVN built-in DHCP server implementation does not
38+
yet support PXE booting for IPv6. This can be achieved at
39+
the moment if used with the Neutron DHCP agent by deploying it
40+
on OVN gateway nodes and disabling the OVN DHCP by setting the
41+
``[ovn]/disable_ovn_dhcp_for_baremetal_ports`` configuration option
42+
to True.
4143

4244
* QoS minimum bandwidth allocation in Placement API
4345

4446
ML2/OVN integration with the Nova placement API to provide guaranteed
45-
minimum bandwidth for ports [3]_.
47+
minimum bandwidth for ports [4]_. Work in progress, see [5]_
4648

4749
* IPv6 Prefix Delegation
4850

4951
Currently ML2/OVN doesn't implement IPv6 prefix delegation. OVN logical
50-
routers have this capability implemented in [4]_ and we have an open RFE to
51-
fill this gap [5]_.
52+
routers have this capability implemented in [6]_ and we have an open RFE to
53+
fill this gap [7]_.
5254

5355
* East/West Fragmentation
5456

@@ -62,11 +64,12 @@ at [1]_.
6264
from instances to reach the DHCP agent. For OVN this traffic has to be explicitly
6365
allowed by security group rules attached to the instance. Note that the default
6466
security group does allow all outgoing traffic, so this only becomes relevant
65-
when using custom security groups [6]_.
67+
when using custom security groups [8]_. Proposed patch is [9]_ but it
68+
needs to be revived and updated.
6669

6770
* DNS resolution for instances
6871

69-
OVN cannot use the host's networking for DNS resolution, so Case 2b in [7]_ can
72+
OVN cannot use the host's networking for DNS resolution, so Case 2b in [10]_ can
7073
only be used when additional DHCP agents are deployed. For Case 2a a different
7174
configuration option has to be used in ``ml2_conf.ini``::
7275

@@ -82,11 +85,12 @@ References
8285
----------
8386

8487
.. [1] https://github.com/ovn-org/ovn/blob/master/TODO.rst
85-
.. [2] https://github.com/openstack/ironic/blob/123cb22c731f93d0c608d791b41e05884fe18c04/ironic/common/pxe_utils.py#L447-L462>
86-
.. [3] https://specs.openstack.org/openstack/neutron-specs/specs/rocky/minimum-bandwidth-allocation-placement-api.html
87-
.. [4] https://patchwork.ozlabs.org/project/openvswitch/patch/6aec0fb280f610a2083fbb6c61e251b1d237b21f.1576840560.git.lorenzo.bianconi@redhat.com/
88-
.. [5] https://bugs.launchpad.net/neutron/+bug/1895972
89-
.. [6] https://bugs.launchpad.net/neutron/+bug/1926515
90-
.. [7] https://docs.openstack.org/neutron/latest/admin/config-dns-res.html
91-
.. [8] https://bugs.launchpad.net/neutron/+bug/1951816
92-
.. [9] https://bugs.launchpad.net/neutron/+bug/1950686
88+
.. [2] https://bugzilla.redhat.com/show_bug.cgi?id=2060310
89+
.. [3] https://review.opendev.org/c/openstack/neutron/+/842292
90+
.. [4] https://specs.openstack.org/openstack/neutron-specs/specs/rocky/minimum-bandwidth-allocation-placement-api.html
91+
.. [5] https://review.opendev.org/c/openstack/neutron/+/786478
92+
.. [6] https://patchwork.ozlabs.org/project/openvswitch/patch/6aec0fb280f610a2083fbb6c61e251b1d237b21f.1576840560.git.lorenzo.bianconi@redhat.com/
93+
.. [7] https://bugs.launchpad.net/neutron/+bug/1895972
94+
.. [8] https://bugs.launchpad.net/neutron/+bug/1926515
95+
.. [9] https://review.opendev.org/c/openstack/neutron/+/788594
96+
.. [10] https://docs.openstack.org/neutron/latest/admin/config-dns-res.html

neutron/common/ovn/constants.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# under the License.
1212

1313
import collections
14+
import copy
1415
import re
1516
import uuid
1617

@@ -129,10 +130,11 @@
129130
'T1': 'T1',
130131
'T2': 'T2',
131132
'bootfile-name': 'bootfile_name',
133+
'bootfile-name-alt': 'bootfile_name_alt',
132134
'wpad': 'wpad',
133135
'path-prefix': 'path_prefix',
134136
'tftp-server-address': 'tftp_server_address',
135-
'server-ip-address': 'tftp_server_address',
137+
'server-ip-address': 'next_server',
136138
'1': 'netmask',
137139
'3': 'router',
138140
'6': 'dns_server',
@@ -173,10 +175,20 @@
173175
'23': 'dns_server'},
174176
}
175177

178+
# Baremetal specific DHCP options for VNIC_BAREMETAL ports
179+
SUPPORTED_BM_DHCP_OPTS_MAPPING = copy.deepcopy(
180+
SUPPORTED_DHCP_OPTS_MAPPING)
181+
SUPPORTED_BM_DHCP_OPTS_MAPPING[4].update({
182+
'tag:ipxe,bootfile-name': 'bootfile_name',
183+
'tag:ipxe,67': 'bootfile_name',
184+
'tag:!ipxe,bootfile-name': 'bootfile_name_alt',
185+
'tag:!ipxe,67': 'bootfile_name_alt'})
186+
176187
# OVN string type DHCP options
177188
OVN_STR_TYPE_DHCP_OPTS = [
178189
'domain_name',
179190
'bootfile_name',
191+
'bootfile_name_alt',
180192
'path_prefix',
181193
'wpad',
182194
'tftp_server']

neutron/common/ovn/utils.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ def validate_port_extra_dhcp_opts(port):
137137
:param port: A neutron port.
138138
:returns: A PortExtraDHCPValidation object.
139139
"""
140+
# Get the right option mappings according to the port's vnic_type
141+
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
142+
mapping = constants.SUPPORTED_DHCP_OPTS_MAPPING
143+
if vnic_type == portbindings.VNIC_BAREMETAL:
144+
mapping = constants.SUPPORTED_BM_DHCP_OPTS_MAPPING
145+
140146
invalid = {const.IP_VERSION_4: [], const.IP_VERSION_6: []}
141147
failed = False
142148
for edo in port.get(edo_ext.EXTRADHCPOPTS, []):
@@ -149,7 +155,7 @@ def validate_port_extra_dhcp_opts(port):
149155
failed = False
150156
break
151157

152-
if opt_name not in constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version]:
158+
if opt_name not in mapping[ip_version]:
153159
invalid[ip_version].append(opt_name)
154160
failed = True
155161

@@ -169,14 +175,16 @@ def get_lsp_dhcp_opts(port, ip_version):
169175
lsp_dhcp_disabled = False
170176
lsp_dhcp_opts = {}
171177
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
178+
is_baremetal = vnic_type == portbindings.VNIC_BAREMETAL
172179

173-
# NOTE(lucasagomes): Baremetal does not yet work with OVN's built-in
174-
# DHCP server, disable it for now
175-
if (is_network_device_port(port) or
176-
vnic_type == portbindings.VNIC_BAREMETAL):
180+
if is_network_device_port(port):
181+
lsp_dhcp_disabled = True
182+
elif is_baremetal and ovn_conf.is_ovn_dhcp_disabled_for_baremetal():
177183
lsp_dhcp_disabled = True
178184
else:
179-
mapping = constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version]
185+
mapping = (constants.SUPPORTED_BM_DHCP_OPTS_MAPPING[ip_version]
186+
if is_baremetal else
187+
constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version])
180188
for edo in port.get(edo_ext.EXTRADHCPOPTS, []):
181189
if edo['ip_version'] != ip_version:
182190
continue

neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@
197197
'or by checking the output of the following command: \n'
198198
'ovs-appctl -t ovs-vswitchd dpif/show-dp-features '
199199
'br-int | grep "Check pkt length action".')),
200+
cfg.BoolOpt('disable_ovn_dhcp_for_baremetal_ports',
201+
default=False,
202+
help=_('Disable OVN\'s built-in DHCP for baremetal ports '
203+
'(VNIC type "baremetal"). This alllow operators to '
204+
'plug their own DHCP server of choice for PXE booting '
205+
'baremetal nodes. Defaults to False.')),
200206
]
201207

202208
cfg.CONF.register_opts(ovn_opts, group='ovn')
@@ -304,3 +310,7 @@ def is_ovn_emit_need_to_frag_enabled():
304310

305311
def is_igmp_snooping_enabled():
306312
return cfg.CONF.OVS.igmp_snooping_enable
313+
314+
315+
def is_ovn_dhcp_disabled_for_baremetal():
316+
return cfg.CONF.ovn.disable_ovn_dhcp_for_baremetal_ports

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/maintenance.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from futurist import periodics
2323
from neutron_lib.api.definitions import external_net
24+
from neutron_lib.api.definitions import portbindings
2425
from neutron_lib.api.definitions import provider_net as pnet
2526
from neutron_lib.api.definitions import segment as segment_def
2627
from neutron_lib import constants as n_const
@@ -840,6 +841,58 @@ def update_port_virtual_type(self):
840841
txn.add(cmd)
841842
raise periodics.NeverAgain()
842843

844+
# A static spacing value is used here, but this method will only run
845+
# once per lock due to the use of periodics.NeverAgain().
846+
@periodics.periodic(spacing=600, run_immediately=True)
847+
def check_baremetal_ports_dhcp_options(self):
848+
"""Update baremetal ports DHCP options
849+
850+
Update baremetal ports DHCP options based on the
851+
"disable_ovn_dhcp_for_baremetal_ports" configuration option.
852+
"""
853+
# If external ports is not supported stop running
854+
# this periodic task
855+
if not self._ovn_client.is_external_ports_supported():
856+
raise periodics.NeverAgain()
857+
858+
if not self.has_lock:
859+
return
860+
861+
context = n_context.get_admin_context()
862+
ports = self._ovn_client._plugin.get_ports(
863+
context,
864+
filters={portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL})
865+
if not ports:
866+
raise periodics.NeverAgain()
867+
868+
with self._nb_idl.transaction(check_error=True) as txn:
869+
for port in ports:
870+
lsp = self._nb_idl.lsp_get(port['id']).execute(
871+
check_error=True)
872+
if not lsp:
873+
continue
874+
875+
update_dhcp = False
876+
if ovn_conf.is_ovn_dhcp_disabled_for_baremetal():
877+
if lsp.dhcpv4_options or lsp.dhcpv6_options:
878+
update_dhcp = True
879+
else:
880+
if not lsp.dhcpv4_options and not lsp.dhcpv6_options:
881+
update_dhcp = True
882+
883+
if update_dhcp:
884+
port_info = self._ovn_client._get_port_options(port)
885+
dhcpv4_options, dhcpv6_options = (
886+
self._ovn_client.update_port_dhcp_options(
887+
port_info, txn))
888+
txn.add(self._nb_idl.set_lswitch_port(
889+
lport_name=port['id'],
890+
dhcpv4_options=dhcpv4_options,
891+
dhcpv6_options=dhcpv6_options,
892+
if_exists=False))
893+
894+
raise periodics.NeverAgain()
895+
843896

844897
class HashRingHealthCheckPeriodics(object):
845898

neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,24 @@ def sync_ha_chassis_group(self, context, network_id, txn):
475475

476476
return ha_ch_grp.uuid
477477

478+
def update_port_dhcp_options(self, port_info, txn):
479+
dhcpv4_options = []
480+
dhcpv6_options = []
481+
if not port_info.dhcpv4_options:
482+
dhcpv4_options = []
483+
elif 'cmd' in port_info.dhcpv4_options:
484+
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
485+
else:
486+
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
487+
if not port_info.dhcpv6_options:
488+
dhcpv6_options = []
489+
elif 'cmd' in port_info.dhcpv6_options:
490+
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
491+
else:
492+
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
493+
494+
return (dhcpv4_options, dhcpv6_options)
495+
478496
def create_port(self, context, port):
479497
if utils.is_lsp_ignored(port):
480498
return
@@ -505,18 +523,8 @@ def create_port(self, context, port):
505523
'Logical_Switch', 'name', lswitch_name)
506524

507525
with self._nb_idl.transaction(check_error=True) as txn:
508-
if not port_info.dhcpv4_options:
509-
dhcpv4_options = []
510-
elif 'cmd' in port_info.dhcpv4_options:
511-
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
512-
else:
513-
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
514-
if not port_info.dhcpv6_options:
515-
dhcpv6_options = []
516-
elif 'cmd' in port_info.dhcpv6_options:
517-
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
518-
else:
519-
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
526+
dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options(
527+
port_info, txn=txn)
520528
# The lport_name *must* be neutron port['id']. It must match the
521529
# iface-id set in the Interfaces table of the Open_vSwitch
522530
# database which nova sets to be the port ID.
@@ -642,18 +650,9 @@ def update_port(self, context, port, port_object=None):
642650
else:
643651
columns_dict['type'] = port_info.type
644652
columns_dict['addresses'] = port_info.addresses
645-
if not port_info.dhcpv4_options:
646-
dhcpv4_options = []
647-
elif 'cmd' in port_info.dhcpv4_options:
648-
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
649-
else:
650-
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
651-
if not port_info.dhcpv6_options:
652-
dhcpv6_options = []
653-
elif 'cmd' in port_info.dhcpv6_options:
654-
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
655-
else:
656-
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
653+
654+
dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options(
655+
port_info, txn=txn)
657656

658657
if self.is_metadata_port(port):
659658
context = n_context.get_admin_context()

neutron/tests/unit/common/ovn/test_utils.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ def test_gateway_chassis_for_chassis_not_in_gw_chassis_list(self):
264264

265265
class TestDHCPUtils(base.BaseTestCase):
266266

267+
def setUp(self):
268+
ovn_conf.register_opts()
269+
super(TestDHCPUtils, self).setUp()
270+
267271
def test_validate_port_extra_dhcp_opts_empty(self):
268272
port = {edo_ext.EXTRADHCPOPTS: []}
269273
result = utils.validate_port_extra_dhcp_opts(port)
@@ -367,11 +371,49 @@ def test_get_lsp_dhcp_opts(self):
367371
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
368372
self.assertFalse(dhcp_disabled)
369373
# Assert the names got translated to their OVN names
370-
expected_options = {'tftp_server_address': '10.0.0.1',
374+
expected_options = {'next_server': '10.0.0.1',
371375
'ntp_server': '10.0.2.1',
372376
'bootfile_name': '"homer_simpson.bin"'}
373377
self.assertEqual(expected_options, options)
374378

379+
def test_get_lsp_dhcp_opts_for_baremetal(self):
380+
opt0 = {'opt_name': 'tag:ipxe,bootfile-name',
381+
'opt_value': 'http://172.7.27.29/ipxe',
382+
'ip_version': 4}
383+
opt1 = {'opt_name': 'tag:!ipxe,bootfile-name',
384+
'opt_value': 'undionly.kpxe',
385+
'ip_version': 4}
386+
opt2 = {'opt_name': 'tftp-server',
387+
'opt_value': '"172.7.27.29"',
388+
'ip_version': 4}
389+
port = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL,
390+
edo_ext.EXTRADHCPOPTS: [opt0, opt1, opt2]}
391+
392+
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
393+
self.assertFalse(dhcp_disabled)
394+
# Assert the names got translated to their OVN names and the
395+
# options that weren't double-quoted are now double-quoted
396+
expected_options = {'tftp_server': '"172.7.27.29"',
397+
'bootfile_name': '"http://172.7.27.29/ipxe"',
398+
'bootfile_name_alt': '"undionly.kpxe"'}
399+
self.assertEqual(expected_options, options)
400+
401+
def test_get_lsp_dhcp_opts_dhcp_disabled_for_baremetal(self):
402+
cfg.CONF.set_override(
403+
'disable_ovn_dhcp_for_baremetal_ports', True, group='ovn')
404+
405+
opt = {'opt_name': 'tag:ipxe,bootfile-name',
406+
'opt_value': 'http://172.7.27.29/ipxe',
407+
'ip_version': 4}
408+
port = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL,
409+
edo_ext.EXTRADHCPOPTS: [opt]}
410+
411+
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
412+
# Assert DHCP is disabled for this port
413+
self.assertTrue(dhcp_disabled)
414+
# Assert no options were passed
415+
self.assertEqual({}, options)
416+
375417

376418
class TestGetDhcpDnsServers(base.BaseTestCase):
377419

0 commit comments

Comments
 (0)