Merge "[OVN] Add baremetal support without Neutron DHCP agent for IPv6"
This commit is contained in:
@@ -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.'))
|
||||
|
@@ -203,6 +203,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 = [
|
||||
|
@@ -199,9 +199,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.
|
Reference in New Issue
Block a user