Merge "[OVN] Add baremetal support without Neutron DHCP agent for IPv4"

This commit is contained in:
Zuul 2022-06-02 13:55:11 +00:00 committed by Gerrit Code Review
commit 75b95ad1c4
9 changed files with 259 additions and 53 deletions
doc/source/ovn
neutron
common/ovn
conf/plugins/ml2/drivers/ovn
plugins/ml2/drivers/ovn/mech_driver/ovsdb
tests/unit
common/ovn
plugins/ml2/drivers/ovn/mech_driver/ovsdb
releasenotes/notes

@ -20,24 +20,25 @@ at [1]_.
can announce host routes for both floating and fixed IP addresses. These can announce host routes for both floating and fixed IP addresses. These
functions are not supported in OVN. functions are not supported in OVN.
* Baremetal provisioning with iPXE * Baremetal provisioning with iPXE without Neutron DHCP agent for IPv6
The core OVN DHCP server implementation does not have support for The core OVN built-in DHCP server implementation does not
sending different boot options based on the ``gpxe`` DHCP Option yet support PXE booting for IPv6. This can be achieved at
(no. 175). Also, Ironic uses dnsmasq syntax when configuring the DHCP the moment if used with the Neutron DHCP agent by deploying it
options for Neutron [4]_ which is not understood by the OVN driver. on OVN gateway nodes and disabling the OVN DHCP by setting the
Work on that is in progress currently, see [5]_ and [6]_. ``[ovn]/disable_ovn_dhcp_for_baremetal_ports`` configuration option
to True.
* QoS minimum bandwidth allocation in Placement API * QoS minimum bandwidth allocation in Placement API
ML2/OVN integration with the Nova placement API to provide guaranteed ML2/OVN integration with the Nova placement API to provide guaranteed
minimum bandwidth for ports [7]_. Work in progress, see [8]_ minimum bandwidth for ports [4]_. Work in progress, see [5]_
* IPv6 Prefix Delegation * IPv6 Prefix Delegation
Currently ML2/OVN doesn't implement IPv6 prefix delegation. OVN logical Currently ML2/OVN doesn't implement IPv6 prefix delegation. OVN logical
routers have this capability implemented in [9]_ and we have an open RFE to routers have this capability implemented in [6]_ and we have an open RFE to
fill this gap [10]_. fill this gap [7]_.
* East/West Fragmentation * East/West Fragmentation
@ -51,12 +52,12 @@ at [1]_.
from instances to reach the DHCP agent. For OVN this traffic has to be explicitly from instances to reach the DHCP agent. For OVN this traffic has to be explicitly
allowed by security group rules attached to the instance. Note that the default allowed by security group rules attached to the instance. Note that the default
security group does allow all outgoing traffic, so this only becomes relevant security group does allow all outgoing traffic, so this only becomes relevant
when using custom security groups [11]_. Proposed patch is [12]_ but it when using custom security groups [8]_. Proposed patch is [9]_ but it
needs to be revived and updated. needs to be revived and updated.
* DNS resolution for instances * DNS resolution for instances
OVN cannot use the host's networking for DNS resolution, so Case 2b in [13]_ can OVN cannot use the host's networking for DNS resolution, so Case 2b in [10]_ can
only be used when additional DHCP agents are deployed. For Case 2a a different only be used when additional DHCP agents are deployed. For Case 2a a different
configuration option has to be used in ``ml2_conf.ini``:: configuration option has to be used in ``ml2_conf.ini``::
@ -69,13 +70,10 @@ References
.. [1] https://github.com/ovn-org/ovn/blob/master/TODO.rst .. [1] https://github.com/ovn-org/ovn/blob/master/TODO.rst
.. [2] https://bugzilla.redhat.com/show_bug.cgi?id=2060310 .. [2] https://bugzilla.redhat.com/show_bug.cgi?id=2060310
.. [3] https://review.opendev.org/c/openstack/neutron/+/842292 .. [3] https://review.opendev.org/c/openstack/neutron/+/842292
.. [4] https://github.com/openstack/ironic/blob/123cb22c731f93d0c608d791b41e05884fe18c04/ironic/common/pxe_utils.py#L447-L462> .. [4] https://specs.openstack.org/openstack/neutron-specs/specs/rocky/minimum-bandwidth-allocation-placement-api.html
.. [5] https://review.opendev.org/c/openstack/neutron/+/840287 .. [5] https://review.opendev.org/c/openstack/neutron/+/786478
.. [6] https://review.opendev.org/c/openstack/neutron/+/840316 .. [6] https://patchwork.ozlabs.org/project/openvswitch/patch/6aec0fb280f610a2083fbb6c61e251b1d237b21f.1576840560.git.lorenzo.bianconi@redhat.com/
.. [7] https://specs.openstack.org/openstack/neutron-specs/specs/rocky/minimum-bandwidth-allocation-placement-api.html .. [7] https://bugs.launchpad.net/neutron/+bug/1895972
.. [8] https://review.opendev.org/c/openstack/neutron/+/786478 .. [8] https://bugs.launchpad.net/neutron/+bug/1926515
.. [9] https://patchwork.ozlabs.org/project/openvswitch/patch/6aec0fb280f610a2083fbb6c61e251b1d237b21f.1576840560.git.lorenzo.bianconi@redhat.com/ .. [9] https://review.opendev.org/c/openstack/neutron/+/788594
.. [10] https://bugs.launchpad.net/neutron/+bug/1895972 .. [10] https://docs.openstack.org/neutron/latest/admin/config-dns-res.html
.. [11] https://bugs.launchpad.net/neutron/+bug/1926515
.. [12] https://review.opendev.org/c/openstack/neutron/+/788594
.. [13] https://docs.openstack.org/neutron/latest/admin/config-dns-res.html

@ -11,6 +11,7 @@
# under the License. # under the License.
import collections import collections
import copy
import re import re
import uuid import uuid
@ -131,10 +132,11 @@ SUPPORTED_DHCP_OPTS_MAPPING = {
'T1': 'T1', 'T1': 'T1',
'T2': 'T2', 'T2': 'T2',
'bootfile-name': 'bootfile_name', 'bootfile-name': 'bootfile_name',
'bootfile-name-alt': 'bootfile_name_alt',
'wpad': 'wpad', 'wpad': 'wpad',
'path-prefix': 'path_prefix', 'path-prefix': 'path_prefix',
'tftp-server-address': 'tftp_server_address', 'tftp-server-address': 'tftp_server_address',
'server-ip-address': 'tftp_server_address', 'server-ip-address': 'next_server',
'1': 'netmask', '1': 'netmask',
'3': 'router', '3': 'router',
'6': 'dns_server', '6': 'dns_server',
@ -175,10 +177,20 @@ SUPPORTED_DHCP_OPTS_MAPPING = {
'23': 'dns_server'}, '23': 'dns_server'},
} }
# Baremetal specific DHCP options for VNIC_BAREMETAL ports
SUPPORTED_BM_DHCP_OPTS_MAPPING = copy.deepcopy(
SUPPORTED_DHCP_OPTS_MAPPING)
SUPPORTED_BM_DHCP_OPTS_MAPPING[4].update({
'tag:ipxe,bootfile-name': 'bootfile_name',
'tag:ipxe,67': 'bootfile_name',
'tag:!ipxe,bootfile-name': 'bootfile_name_alt',
'tag:!ipxe,67': 'bootfile_name_alt'})
# OVN string type DHCP options # OVN string type DHCP options
OVN_STR_TYPE_DHCP_OPTS = [ OVN_STR_TYPE_DHCP_OPTS = [
'domain_name', 'domain_name',
'bootfile_name', 'bootfile_name',
'bootfile_name_alt',
'path_prefix', 'path_prefix',
'wpad', 'wpad',
'tftp_server'] 'tftp_server']

@ -139,6 +139,12 @@ def validate_port_extra_dhcp_opts(port):
:param port: A neutron port. :param port: A neutron port.
:returns: A PortExtraDHCPValidation object. :returns: A PortExtraDHCPValidation object.
""" """
# Get the right option mappings according to the port's vnic_type
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
mapping = constants.SUPPORTED_DHCP_OPTS_MAPPING
if vnic_type == portbindings.VNIC_BAREMETAL:
mapping = constants.SUPPORTED_BM_DHCP_OPTS_MAPPING
invalid = {const.IP_VERSION_4: [], const.IP_VERSION_6: []} invalid = {const.IP_VERSION_4: [], const.IP_VERSION_6: []}
failed = False failed = False
for edo in port.get(edo_ext.EXTRADHCPOPTS, []): for edo in port.get(edo_ext.EXTRADHCPOPTS, []):
@ -151,7 +157,7 @@ def validate_port_extra_dhcp_opts(port):
failed = False failed = False
break break
if opt_name not in constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version]: if opt_name not in mapping[ip_version]:
invalid[ip_version].append(opt_name) invalid[ip_version].append(opt_name)
failed = True failed = True
@ -171,14 +177,16 @@ def get_lsp_dhcp_opts(port, ip_version):
lsp_dhcp_disabled = False lsp_dhcp_disabled = False
lsp_dhcp_opts = {} lsp_dhcp_opts = {}
vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL)
is_baremetal = vnic_type == portbindings.VNIC_BAREMETAL
# NOTE(lucasagomes): Baremetal does not yet work with OVN's built-in if is_network_device_port(port):
# DHCP server, disable it for now lsp_dhcp_disabled = True
if (is_network_device_port(port) or elif is_baremetal and ovn_conf.is_ovn_dhcp_disabled_for_baremetal():
vnic_type == portbindings.VNIC_BAREMETAL):
lsp_dhcp_disabled = True lsp_dhcp_disabled = True
else: else:
mapping = constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version] mapping = (constants.SUPPORTED_BM_DHCP_OPTS_MAPPING[ip_version]
if is_baremetal else
constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version])
for edo in port.get(edo_ext.EXTRADHCPOPTS, []): for edo in port.get(edo_ext.EXTRADHCPOPTS, []):
if edo['ip_version'] != ip_version: if edo['ip_version'] != ip_version:
continue continue

@ -207,6 +207,12 @@ ovn_opts = [
'or by checking the output of the following command: \n' 'or by checking the output of the following command: \n'
'ovs-appctl -t ovs-vswitchd dpif/show-dp-features ' 'ovs-appctl -t ovs-vswitchd dpif/show-dp-features '
'br-int | grep "Check pkt length action".')), 'br-int | grep "Check pkt length action".')),
cfg.BoolOpt('disable_ovn_dhcp_for_baremetal_ports',
default=False,
help=_('Disable OVN\'s built-in DHCP for baremetal ports '
'(VNIC type "baremetal"). This alllow operators to '
'plug their own DHCP server of choice for PXE booting '
'baremetal nodes. Defaults to False.')),
] ]
@ -316,3 +322,7 @@ def is_ovn_emit_need_to_frag_enabled():
def is_igmp_snooping_enabled(): def is_igmp_snooping_enabled():
return cfg.CONF.OVS.igmp_snooping_enable return cfg.CONF.OVS.igmp_snooping_enable
def is_ovn_dhcp_disabled_for_baremetal():
return cfg.CONF.ovn.disable_ovn_dhcp_for_baremetal_ports

@ -20,6 +20,7 @@ import re
import threading import threading
from futurist import periodics from futurist import periodics
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.api.definitions import provider_net as pnet
from neutron_lib import constants as n_const from neutron_lib import constants as n_const
from neutron_lib import context as n_context from neutron_lib import context as n_context
@ -730,6 +731,58 @@ class DBInconsistenciesPeriodics(SchemaAwarePeriodicsBase):
txn.add(cmd) txn.add(cmd)
raise periodics.NeverAgain() raise periodics.NeverAgain()
# A static spacing value is used here, but this method will only run
# once per lock due to the use of periodics.NeverAgain().
@periodics.periodic(spacing=600, run_immediately=True)
def check_baremetal_ports_dhcp_options(self):
"""Update baremetal ports DHCP options
Update baremetal ports DHCP options based on the
"disable_ovn_dhcp_for_baremetal_ports" configuration option.
"""
# If external ports is not supported stop running
# this periodic task
if not self._ovn_client.is_external_ports_supported():
raise periodics.NeverAgain()
if not self.has_lock:
return
context = n_context.get_admin_context()
ports = self._ovn_client._plugin.get_ports(
context,
filters={portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL})
if not ports:
raise periodics.NeverAgain()
with self._nb_idl.transaction(check_error=True) as txn:
for port in ports:
lsp = self._nb_idl.lsp_get(port['id']).execute(
check_error=True)
if not lsp:
continue
update_dhcp = False
if ovn_conf.is_ovn_dhcp_disabled_for_baremetal():
if lsp.dhcpv4_options or lsp.dhcpv6_options:
update_dhcp = True
else:
if not lsp.dhcpv4_options and not lsp.dhcpv6_options:
update_dhcp = True
if update_dhcp:
port_info = self._ovn_client._get_port_options(port)
dhcpv4_options, dhcpv6_options = (
self._ovn_client.update_port_dhcp_options(
port_info, txn))
txn.add(self._nb_idl.set_lswitch_port(
lport_name=port['id'],
dhcpv4_options=dhcpv4_options,
dhcpv6_options=dhcpv6_options,
if_exists=False))
raise periodics.NeverAgain()
class HashRingHealthCheckPeriodics(object): class HashRingHealthCheckPeriodics(object):

@ -446,6 +446,24 @@ class OVNClient(object):
return ha_ch_grp.uuid return ha_ch_grp.uuid
def update_port_dhcp_options(self, port_info, txn):
dhcpv4_options = []
dhcpv6_options = []
if not port_info.dhcpv4_options:
dhcpv4_options = []
elif 'cmd' in port_info.dhcpv4_options:
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
else:
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
if not port_info.dhcpv6_options:
dhcpv6_options = []
elif 'cmd' in port_info.dhcpv6_options:
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
else:
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
return (dhcpv4_options, dhcpv6_options)
def create_port(self, context, port): def create_port(self, context, port):
if utils.is_lsp_ignored(port): if utils.is_lsp_ignored(port):
return return
@ -476,18 +494,8 @@ class OVNClient(object):
'Logical_Switch', 'name', lswitch_name) 'Logical_Switch', 'name', lswitch_name)
with self._nb_idl.transaction(check_error=True) as txn: with self._nb_idl.transaction(check_error=True) as txn:
if not port_info.dhcpv4_options: dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options(
dhcpv4_options = [] port_info, txn=txn)
elif 'cmd' in port_info.dhcpv4_options:
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
else:
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
if not port_info.dhcpv6_options:
dhcpv6_options = []
elif 'cmd' in port_info.dhcpv6_options:
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
else:
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
# The lport_name *must* be neutron port['id']. It must match the # The lport_name *must* be neutron port['id']. It must match the
# iface-id set in the Interfaces table of the Open_vSwitch # iface-id set in the Interfaces table of the Open_vSwitch
# database which nova sets to be the port ID. # database which nova sets to be the port ID.
@ -606,18 +614,9 @@ class OVNClient(object):
else: else:
columns_dict['type'] = port_info.type columns_dict['type'] = port_info.type
columns_dict['addresses'] = port_info.addresses columns_dict['addresses'] = port_info.addresses
if not port_info.dhcpv4_options:
dhcpv4_options = [] dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options(
elif 'cmd' in port_info.dhcpv4_options: port_info, txn=txn)
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
else:
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
if not port_info.dhcpv6_options:
dhcpv6_options = []
elif 'cmd' in port_info.dhcpv6_options:
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
else:
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
if self.is_metadata_port(port): if self.is_metadata_port(port):
context = n_context.get_admin_context() context = n_context.get_admin_context()

@ -281,6 +281,10 @@ class TestGateWayChassisValidity(base.BaseTestCase):
class TestDHCPUtils(base.BaseTestCase): class TestDHCPUtils(base.BaseTestCase):
def setUp(self):
ovn_conf.register_opts()
super(TestDHCPUtils, self).setUp()
def test_validate_port_extra_dhcp_opts_empty(self): def test_validate_port_extra_dhcp_opts_empty(self):
port = {edo_ext.EXTRADHCPOPTS: []} port = {edo_ext.EXTRADHCPOPTS: []}
result = utils.validate_port_extra_dhcp_opts(port) result = utils.validate_port_extra_dhcp_opts(port)
@ -384,11 +388,49 @@ class TestDHCPUtils(base.BaseTestCase):
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4) dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
self.assertFalse(dhcp_disabled) self.assertFalse(dhcp_disabled)
# Assert the names got translated to their OVN names # Assert the names got translated to their OVN names
expected_options = {'tftp_server_address': '10.0.0.1', expected_options = {'next_server': '10.0.0.1',
'ntp_server': '10.0.2.1', 'ntp_server': '10.0.2.1',
'bootfile_name': '"homer_simpson.bin"'} 'bootfile_name': '"homer_simpson.bin"'}
self.assertEqual(expected_options, options) self.assertEqual(expected_options, options)
def test_get_lsp_dhcp_opts_for_baremetal(self):
opt0 = {'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': 'http://172.7.27.29/ipxe',
'ip_version': 4}
opt1 = {'opt_name': 'tag:!ipxe,bootfile-name',
'opt_value': 'undionly.kpxe',
'ip_version': 4}
opt2 = {'opt_name': 'tftp-server',
'opt_value': '"172.7.27.29"',
'ip_version': 4}
port = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL,
edo_ext.EXTRADHCPOPTS: [opt0, opt1, opt2]}
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
self.assertFalse(dhcp_disabled)
# Assert the names got translated to their OVN names and the
# options that weren't double-quoted are now double-quoted
expected_options = {'tftp_server': '"172.7.27.29"',
'bootfile_name': '"http://172.7.27.29/ipxe"',
'bootfile_name_alt': '"undionly.kpxe"'}
self.assertEqual(expected_options, options)
def test_get_lsp_dhcp_opts_dhcp_disabled_for_baremetal(self):
cfg.CONF.set_override(
'disable_ovn_dhcp_for_baremetal_ports', True, group='ovn')
opt = {'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': 'http://172.7.27.29/ipxe',
'ip_version': 4}
port = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL,
edo_ext.EXTRADHCPOPTS: [opt]}
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
# Assert DHCP is disabled for this port
self.assertTrue(dhcp_disabled)
# Assert no options were passed
self.assertEqual({}, options)
class TestConnectionConfigToTargetString(base.BaseTestCase): class TestConnectionConfigToTargetString(base.BaseTestCase):

@ -616,3 +616,73 @@ class TestDBInconsistenciesPeriodics(testlib_api.SqlTestCaseLight,
expected_calls = [mock.call('Logical_Router', lr0.uuid, expected_calls = [mock.call('Logical_Router', lr0.uuid,
('external_ids', ext_ids))] ('external_ids', ext_ids))]
nb_idl.db_set.assert_has_calls(expected_calls) nb_idl.db_set.assert_has_calls(expected_calls)
def _test_check_baremetal_ports_dhcp_options(self, dhcp_disabled=False):
cfg.CONF.set_override('disable_ovn_dhcp_for_baremetal_ports',
dhcp_disabled, group='ovn')
self.fake_ovn_client.is_external_ports_supported.return_value = True
nb_idl = self.fake_ovn_client._nb_idl
self.fake_ovn_client._get_port_options.return_value = 'fake-port-opts'
port0 = {'id': 'port0'}
port1 = {'id': 'port1'}
port2 = {'id': 'port2'}
port3 = {'id': 'port3'}
self.fake_ovn_client._plugin.get_ports.return_value = [
port0, port1, port2, port3]
lsp0 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'type': constants.LSP_TYPE_EXTERNAL,
'name': 'lsp0',
'dhcpv4_options': ['fake-uuid'],
'dhcpv6_options': []})
lsp1 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'type': constants.LSP_TYPE_EXTERNAL,
'name': 'lsp1',
'dhcpv4_options': [],
'dhcpv6_options': []})
lsp2 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'type': constants.LSP_TYPE_EXTERNAL,
'name': 'lsp2',
'dhcpv4_options': [],
'dhcpv6_options': ['fake-uuid']})
lsp3 = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'type': constants.LSP_TYPE_EXTERNAL,
'name': 'lsp3',
'dhcpv4_options': ['fake-uuid'],
'dhcpv6_options': ['fake-uuid']})
nb_idl.lsp_get.return_value.execute.side_effect = [
lsp0, lsp1, lsp2, lsp3]
self.fake_ovn_client.update_port_dhcp_options.side_effect = [
(lsp0.dhcpv4_options, lsp0.dhcpv6_options),
(lsp1.dhcpv4_options, lsp1.dhcpv6_options),
(lsp2.dhcpv4_options, lsp2.dhcpv6_options),
(lsp3.dhcpv4_options, lsp3.dhcpv6_options)]
self.assertRaises(periodics.NeverAgain,
self.periodic.check_baremetal_ports_dhcp_options)
def test_check_baremetal_ports_dhcp_options(self):
self._test_check_baremetal_ports_dhcp_options()
self.fake_ovn_client._nb_idl.set_lswitch_port.assert_called_once_with(
lport_name='port1', dhcpv4_options=['fake-uuid'],
dhcpv6_options=[], if_exists=False)
def test_check_baremetal_ports_dhcp_options_dhcp_disabled(self):
self._test_check_baremetal_ports_dhcp_options(dhcp_disabled=True)
expected_calls = [
mock.call(lport_name='port0',
dhcpv4_options=['fake-uuid'],
dhcpv6_options=[], if_exists=False),
mock.call(lport_name='port2',
dhcpv4_options=[],
dhcpv6_options=[], if_exists=False),
mock.call(lport_name='port3',
dhcpv4_options=[],
dhcpv6_options=['fake-uuid'], if_exists=False)]
self.fake_ovn_client._nb_idl.set_lswitch_port.assert_has_calls(
expected_calls)

@ -0,0 +1,14 @@
---
features:
- |
Support for baremetal provisioning using OVN's built-in DHCP server
has been added for IPv4.
upgrade:
- |
A new configuration option called
``[ovn]/disable_ovn_dhcp_for_baremetal_ports`` has been added to
ML2/OVN for IPv4. Since PXE booting nodes can be very sensitive
depending on the hardware and some operators may prefer to use a
fully-fledged DHCP server instead of OVN's DHCP server this option
allows for disabling OVN's built-in DHCP server for baremetal ports
(vnic type "baremetal") when set to True. It defaults to False.