[OVN] Add baremetal support without Neutron DHCP agent for IPv6

Support for the required DHCPv6 options was recently added in core
OVN with [1].
This patch adds support for that in ML2/OVN backend also and by that
closing one of the gaps between ML2/OVN and ML2/OVS backends.

This patch also adds upgrade check to check used ovn version and warn
operators if native OVN DHCP is used for BM provisioning and OVN version
is older than 23.06.0.
Unfortunately there is no easy way to check used version of OVN so check
relies on the ovnnb schema version.

[1] c5fd51bd15

Closes-Bug: #2030520
Change-Id: Iaa3ff8e97021e44f352e5a9a370714bf5f1d77b8
This commit is contained in:
Slawek Kaplonski 2023-08-07 17:02:26 +02:00
parent 6514e37e47
commit 034fcb0f6d
9 changed files with 211 additions and 27 deletions

View File

@ -8,15 +8,6 @@ It is not a complete list, but is enough to be used as a starting point for
implementors working on closing these gaps. A TODO list for OVN is located
at [1]_.
* Baremetal provisioning with iPXE without Neutron DHCP agent for IPv6
The core OVN built-in DHCP server implementation does not
yet support PXE booting for IPv6. This can be achieved at
the moment if used with the Neutron DHCP agent by deploying it
on OVN gateway nodes and disabling the OVN DHCP by setting the
``[ovn]/disable_ovn_dhcp_for_baremetal_ports`` configuration option
to True.
* QoS minimum bandwidth allocation in Placement API
ML2/OVN integration with the Nova placement API to provide guaranteed

View File

@ -28,6 +28,8 @@ from sqlalchemy import or_
from neutron._i18n import _
from neutron.cmd.upgrade_checks import base
from neutron.conf.plugins.ml2 import config as ml2_conf
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
from neutron.conf import service as conf_service
from neutron.db.extra_dhcp_opt import models as extra_dhcp_opt_models
from neutron.db.models import agent as agent_model
@ -39,12 +41,17 @@ from neutron.db.models import segment
from neutron.db import models_v2
from neutron.db.qos import models as qos_models
from neutron.objects import ports as port_obj
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import impl_idl_ovn
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import worker
OVN_ALEMBIC_TABLE_NAME = "ovn_alembic_version"
LAST_NETWORKING_OVN_EXPAND_HEAD = "e55d09277410"
LAST_NETWORKING_OVN_CONTRACT_HEAD = "1d271ead4eb6"
_OVN_CLIENT = None
def get_agents(agt_type):
"""Get agent information from Database
@ -175,8 +182,23 @@ def get_duplicated_ha_networks_per_project():
return query.all()
def get_ovn_client():
global _OVN_CLIENT
if _OVN_CLIENT is None:
mech_worker = worker.MaintenanceWorker
ovn_api = impl_idl_ovn.OvsdbNbOvnIdl.from_worker(mech_worker)
ovn_sb_api = impl_idl_ovn.OvsdbSbOvnIdl.from_worker(mech_worker)
_OVN_CLIENT = ovn_client.OVNClient(ovn_api, ovn_sb_api)
return _OVN_CLIENT
class CoreChecks(base.BaseChecks):
def __init__(self):
super().__init__()
ml2_conf.register_ml2_plugin_opts()
ovn_conf.register_opts()
def get_checks(self):
return [
(_("Gateway external network"),
@ -205,6 +227,8 @@ class CoreChecks(base.BaseChecks):
self.extra_dhcp_options_check),
(_('Duplicated HA network per project check'),
self.extra_dhcp_options_check),
(_('OVN support for BM provisioning over IPv6 check'),
self.ovn_for_bm_provisioning_over_ipv6_check),
]
@staticmethod
@ -570,3 +594,48 @@ class CoreChecks(base.BaseChecks):
return upgradecheck.Result(
upgradecheck.Code.SUCCESS,
_('There are no duplicated HA networks in the system.'))
@staticmethod
def ovn_for_bm_provisioning_over_ipv6_check(checker):
"""Check if OVN version is new enough to handle IPv6 provisioning
Support for the required DHCPv6 options was recently added in core
OVN with c5fd51bd154147a567097eaf61fbebc0b5b39e28 in OVN.
This check function will raise warning if user is using older OVN
version, withouth this patch and will have
``disable_ovn_dhcp_for_baremetal_ports`` option set to False.
"""
if cfg.CONF.ovn.disable_ovn_dhcp_for_baremetal_ports:
return upgradecheck.Result(
upgradecheck.Code.SUCCESS,
_("Native OVN DHCP is disabed for baremetal ports."))
try:
ovn_client = get_ovn_client()
except RuntimeError:
return upgradecheck.Result(
upgradecheck.Code.WARNING,
_("Invalid OVN connection parameters provided."))
except Exception as err:
err_msg = "Failed to connect to OVN. Error: %s" % err
return upgradecheck.Result(
upgradecheck.Code.WARNING,
_(err_msg))
if ovn_client.is_ipxe_over_ipv6_supported:
return upgradecheck.Result(
upgradecheck.Code.SUCCESS,
_('Version of OVN supports iPXE over IPv6.'))
else:
return upgradecheck.Result(
upgradecheck.Code.WARNING,
_('Version of OVN does not support iPXE over IPv6 but '
'``disable_ovn_dhcp_for_baremetal_ports`` is set to '
'``False``. In case if provisioning of baremetal nodes '
'is required, please make sure that either '
'``disable_ovn_dhcp_for_baremetal_ports`` option is set to '
'``True`` and Neutron DHCP agent is available or use '
'OVN with patch https://github.com/ovn-org/ovn/commit/'
'c5fd51bd154147a567097eaf61fbebc0b5b39e28 which added '
'support for iPXE over IPv6. It is available in '
'OVN >= 23.06.0.'))

View File

@ -202,6 +202,12 @@ SUPPORTED_BM_DHCP_OPTS_MAPPING[4].update({
'tag:ipxe,67': 'bootfile_name',
'tag:!ipxe,bootfile-name': 'bootfile_name_alt',
'tag:!ipxe,67': 'bootfile_name_alt'})
SUPPORTED_BM_DHCP_OPTS_MAPPING[6].update({
'tag:ipxe6,bootfile-name': 'bootfile_name',
'tag:ipxe6,59': 'bootfile_name',
'tag:!ipxe6,bootfile-name': 'bootfile_name_alt',
'tag:!ipxe6,59': 'bootfile_name_alt'})
# OVN string type DHCP options
OVN_STR_TYPE_DHCP_OPTS = [

View File

@ -207,9 +207,14 @@ ovn_opts = [
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 '
'(VNIC type "baremetal"). This allows operators to '
'plug their own DHCP server of choice for PXE booting '
'baremetal nodes. Defaults to False.')),
'baremetal nodes. OVN 23.06.0 and newer also supports '
'baremetal ``PXE`` based provisioning over IPv6. '
'If an older version of OVN is used for baremetal '
'provisioning over IPv6 this option should be set '
'to "True" and neutron-dhcp-agent should be used '
'instead. Defaults to "False".')),
cfg.BoolOpt('allow_stateless_action_supported',
default=True,
deprecated_for_removal=True,

View File

@ -97,6 +97,7 @@ class OVNClient(object):
self._plugin_property = None
self._l3_plugin_property = None
self._is_mcast_flood_broken = None
self._is_ipxe_over_ipv6_supported = None
# TODO(ralonsoh): handle the OVN client extensions with an ext. manager
self._qos_driver = qos_extension.OVNClientQosExtension(driver=self)
@ -313,6 +314,19 @@ class OVNClient(object):
(6, 3, 0))
return self._is_mcast_flood_broken
# TODO(slaweq): Remove this method when min supported OVN version will be
# >= v23.06.0 which is the one which have support for IPv6 iPXE booting
# added:
# https://github.com/ovn-org/ovn/commit/c5fd51bd154147a567097eaf61fbebc0b5b39e28
@property
def is_ipxe_over_ipv6_supported(self):
if self._is_ipxe_over_ipv6_supported is None:
schema_version = self._nb_idl.get_schema_version()
self._is_ipxe_over_ipv6_supported = (
versionutils.convert_version_to_tuple(schema_version) >=
(7, 0, 4))
return self._is_ipxe_over_ipv6_supported
def _get_port_options(self, port):
context = n_context.get_admin_context()
bp_info = utils.validate_and_get_data_from_binding_profile(port)

View File

@ -25,7 +25,7 @@ class StatusTest(base.BaseLoggingTestCase):
def test_neutron_status_cli(self):
"""This test runs "neutron-status upgrade check" command and check if
stdout contains header "Upgrade Check Results". It also checks if
stderr is empty.
stderr contains only expected message.
Example output from this CLI tool looks like:
+----------------------------------------------------------------+
@ -49,6 +49,14 @@ class StatusTest(base.BaseLoggingTestCase):
"""
expected_result_title = "Upgrade Check Results"
# NOTE(slaweq): it seems that ovsdbapp raises Exception() and prints
# it's message to the stderr when it can't connect to the OVSDBs.
# This upgrade check's test is just testing that tool is working fine
# and don't really need to connect to the ovn databases so lets simply
# expect that error message in the test
expected_stderr = (
'Unable to open stream to tcp:127.0.0.1:6641 to retrieve schema: '
'Connection refused')
try:
stdout, stderr = utils.execute(
cmd=["neutron-status", "upgrade", "check"],
@ -57,7 +65,9 @@ class StatusTest(base.BaseLoggingTestCase):
upgradecheck.Code.WARNING,
upgradecheck.Code.FAILURE],
return_stderr=True)
self.assertEqual('', stderr)
self.assertEqual(
expected_stderr,
stderr.replace('\n', ''))
self.assertTrue(expected_result_title in stdout)
except exceptions.ProcessExecutionError as error:
self.fail("neutron-status upgrade check command failed to run. "

View File

@ -297,3 +297,56 @@ class TestChecks(base.BaseTestCase):
result = checks.CoreChecks.duplicated_ha_network_per_project_check(
mock.ANY)
self.assertEqual(Code.WARNING, result.code)
@mock.patch.object(checks, 'get_ovn_client')
def test_ovn_for_bm_provisioning_over_ipv6_check_native_dhcp_disabled(
self, mock_get_ovn_client):
cfg.CONF.set_override(
'disable_ovn_dhcp_for_baremetal_ports', True, group='ovn')
result = checks.CoreChecks.ovn_for_bm_provisioning_over_ipv6_check(
mock.ANY)
self.assertEqual(Code.SUCCESS, result.code)
mock_get_ovn_client.assert_not_called()
@mock.patch.object(checks, 'get_ovn_client')
def test_ovn_for_bm_provisioning_over_ipv6_check_success(
self, mock_get_ovn_client):
ovn_client_mock = mock.Mock(is_ipxe_over_ipv6_supported=True)
mock_get_ovn_client.return_value = ovn_client_mock
cfg.CONF.set_override(
'disable_ovn_dhcp_for_baremetal_ports', False, group='ovn')
result = checks.CoreChecks.ovn_for_bm_provisioning_over_ipv6_check(
mock.ANY)
self.assertEqual(Code.SUCCESS, result.code)
mock_get_ovn_client.assert_called_once_with()
@mock.patch.object(checks, 'get_ovn_client')
def test_ovn_for_bm_provisioning_over_ipv6_check_warning(
self, mock_get_ovn_client):
ovn_client_mock = mock.Mock(is_ipxe_over_ipv6_supported=False)
mock_get_ovn_client.return_value = ovn_client_mock
cfg.CONF.set_override(
'disable_ovn_dhcp_for_baremetal_ports', False, group='ovn')
result = checks.CoreChecks.ovn_for_bm_provisioning_over_ipv6_check(
mock.ANY)
self.assertEqual(Code.WARNING, result.code)
mock_get_ovn_client.assert_called_once_with()
@mock.patch.object(checks, 'get_ovn_client')
def test_ovn_for_bm_provisioning_over_ipv6_check_failed_to_get_ovn_client(
self, mock_get_ovn_client):
mock_get_ovn_client.side_effect = RuntimeError
cfg.CONF.set_override(
'disable_ovn_dhcp_for_baremetal_ports', False, group='ovn')
result = checks.CoreChecks.ovn_for_bm_provisioning_over_ipv6_check(
mock.ANY)
self.assertEqual(Code.WARNING, result.code)
mock_get_ovn_client.assert_called_once_with()

View File

@ -438,17 +438,29 @@ class TestDHCPUtils(base.BaseTestCase):
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}
opts = [{
'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': 'http://172.7.27.29/ipxe',
'ip_version': 4
}, {
'opt_name': 'tag:!ipxe,bootfile-name',
'opt_value': 'undionly.kpxe',
'ip_version': 4
}, {
'opt_name': 'tftp-server',
'opt_value': '"172.7.27.29"',
'ip_version': 4
}, {
'opt_name': 'tag:ipxe6,bootfile-name',
'opt_value': 'http://[2001:db8::1]/ipxe',
'ip_version': 6
}, {
'opt_name': 'tag:!ipxe6,bootfile-name',
'opt_value': 'undionly.kpxe',
'ip_version': 6
}]
port = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL,
edo_ext.EXTRADHCPOPTS: [opt0, opt1, opt2]}
edo_ext.EXTRADHCPOPTS: opts}
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
self.assertFalse(dhcp_disabled)
@ -458,16 +470,28 @@ class TestDHCPUtils(base.BaseTestCase):
'bootfile_name': '"http://172.7.27.29/ipxe"',
'bootfile_name_alt': '"undionly.kpxe"'}
self.assertEqual(expected_options, options)
# Now the same for IPv6 options
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 6)
self.assertFalse(dhcp_disabled)
expected_options = {'bootfile_name': '"http://[2001:db8::1]/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}
opts = [{
'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': 'http://172.7.27.29/ipxe',
'ip_version': 4
}, {
'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': 'http://[2001:db8::1]/ipxe',
'ip_version': 6
}]
port = {portbindings.VNIC_TYPE: portbindings.VNIC_BAREMETAL,
edo_ext.EXTRADHCPOPTS: [opt]}
edo_ext.EXTRADHCPOPTS: [opts]}
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
# Assert DHCP is disabled for this port
@ -475,6 +499,13 @@ class TestDHCPUtils(base.BaseTestCase):
# Assert no options were passed
self.assertEqual({}, options)
# and the same for dhcpv6
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 6)
# Assert DHCP is disabled for this port
self.assertTrue(dhcp_disabled)
# Assert no options were passed
self.assertEqual({}, options)
def test_get_lsp_dhcp_opts_for_domain_search(self):
opt = {'opt_name': 'domain-search',
'opt_value': 'openstack.org,ovn.org',

View File

@ -0,0 +1,5 @@
---
features:
- |
Support for PXE baremetal provisioning using OVN's built-in DHCP server
has been added for IPv6.