[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:
parent
6514e37e47
commit
034fcb0f6d
@ -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
|
||||
|
@ -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.'))
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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. "
|
||||
|
@ -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()
|
||||
|
@ -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',
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Support for PXE baremetal provisioning using OVN's built-in DHCP server
|
||||
has been added for IPv6.
|
Loading…
Reference in New Issue
Block a user