Browse Source

Add support for global DHCP options with OVN DHCP.

An operator may wish to set certain DHCP options
globally within an environment. This patch adds
configuration options to allow an operator to
specify DHCP option default values that may be
overriden by more specific configuration at the
subnet or port level.

Change-Id: I626c2dcd4ba66466b342da27a2ab50c3cac8b040
Closes-bug: 1785847
changes/28/589528/21
Andrew Austin 4 years ago committed by Lucas Alvares Gomes
parent
commit
545d098bfa
  1. 34
      networking_ovn/common/config.py
  2. 8
      networking_ovn/common/constants.py
  3. 65
      networking_ovn/common/maintenance.py
  4. 27
      networking_ovn/common/ovn_client.py
  5. 135
      networking_ovn/tests/functional/test_maintenance.py
  6. 69
      networking_ovn/tests/unit/ml2/test_mech_driver.py
  7. 10
      releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml

34
networking_ovn/common/config.py

@ -154,6 +154,32 @@ ovn_opts = [
"field is empty. If both subnet's dns_nameservers and "
"this option is empty, then the DNS resolvers on the "
"host running the neutron server will be used.")),
cfg.DictOpt('ovn_dhcp4_global_options',
default={},
help=_("Dictionary of global DHCPv4 options which will be "
"automatically set on each subnet upon creation and "
"on all existing subnets when Neutron starts.\n"
"An empty value for a DHCP option will cause that "
"option to be unset globally.\n"
"EXAMPLES:\n"
"- ntp_server:1.2.3.4,wpad:1.2.3.5 - Set ntp_server "
"and wpad\n"
"- ntp_server:,wpad:1.2.3.5 - Unset ntp_server and "
"set wpad\n"
"See the ovn-nb(5) man page for available options.")),
cfg.DictOpt('ovn_dhcp6_global_options',
default={},
help=_("Dictionary of global DHCPv6 options which will be "
"automatically set on each subnet upon creation and "
"on all existing subnets when Neutron starts.\n"
"An empty value for a DHCP option will cause that "
"option to be unset globally.\n"
"EXAMPLES:\n"
"- ntp_server:1.2.3.4,wpad:1.2.3.5 - Set ntp_server "
"and wpad\n"
"- ntp_server:,wpad:1.2.3.5 - Unset ntp_server and "
"set wpad\n"
"See the ovn-nb(5) man page for available options.")),
]
cfg.CONF.register_opts(ovn_opts, group='ovn')
@ -241,6 +267,14 @@ def get_dns_servers():
return cfg.CONF.ovn.dns_servers
def get_global_dhcpv4_opts():
return cfg.CONF.ovn.ovn_dhcp4_global_options
def get_global_dhcpv6_opts():
return cfg.CONF.ovn.ovn_dhcp6_global_options
def setup_logging():
"""Sets up the logging options for a log with supplied name."""
product_name = "networking-ovn"

8
networking_ovn/common/constants.py

@ -91,6 +91,14 @@ SUPPORTED_DHCP_OPTS = {
6: ['server-id', 'dns-server', 'domain-search']}
DHCPV6_STATELESS_OPT = 'dhcpv6_stateless'
# When setting global DHCP options, these options will be ignored
# as they are required for basic network functions and will be
# set by Neutron.
GLOBAL_DHCP_OPTS_BLACKLIST = {
4: ['server_id', 'lease_time', 'mtu', 'router', 'server_mac',
'dns_server', 'classless_static_route'],
6: ['dhcpv6_stateless', 'dns_server', 'server_id']}
CHASSIS_DATAPATH_NETDEV = 'netdev'
CHASSIS_IFACE_DPDKVHOSTUSER = 'dpdkvhostuser'

65
networking_ovn/common/maintenance.py

@ -18,12 +18,14 @@ import threading
from futurist import periodics
from neutron.common import config as n_conf
from neutron_lib import constants as n_const
from neutron_lib import context as n_context
from neutron_lib import exceptions as n_exc
from neutron_lib import worker
from oslo_log import log
from oslo_utils import timeutils
from networking_ovn.common import config as ovn_conf
from networking_ovn.common import constants as ovn_const
from networking_ovn.db import maintenance as db_maint
from networking_ovn.db import revision as db_rev
@ -301,3 +303,66 @@ class DBInconsistenciesPeriodics(object):
router_id = port['device_id']
self._ovn_client._l3_plugin.add_router_interface(
admin_context, router_id, {'port_id': port['id']}, may_exist=True)
def _check_subnet_global_dhcp_opts(self):
inconsistent_subnets = []
admin_context = n_context.get_admin_context()
subnet_filter = {'enable_dhcp': [True]}
neutron_subnets = self._ovn_client._plugin.get_subnets(
admin_context, subnet_filter)
global_v4_opts = ovn_conf.get_global_dhcpv4_opts()
global_v6_opts = ovn_conf.get_global_dhcpv6_opts()
LOG.debug('Checking %s subnets for global DHCP option consistency',
len(neutron_subnets))
for subnet in neutron_subnets:
ovn_dhcp_opts = self._nb_idl.get_subnet_dhcp_options(
subnet['id'])['subnet']
inconsistent_opts = []
if ovn_dhcp_opts:
if subnet['ip_version'] == n_const.IP_VERSION_4:
for opt, value in global_v4_opts.items():
if value != ovn_dhcp_opts['options'].get(opt, None):
inconsistent_opts.append(opt)
if subnet['ip_version'] == n_const.IP_VERSION_6:
for opt, value in global_v6_opts.items():
if value != ovn_dhcp_opts['options'].get(opt, None):
inconsistent_opts.append(opt)
if inconsistent_opts:
LOG.debug('Subnet %s has inconsistent DHCP opts: %s',
subnet['id'], inconsistent_opts)
inconsistent_subnets.append(subnet)
return inconsistent_subnets
# 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_global_dhcp_opts(self):
# This periodic task is included in DBInconsistenciesPeriodics since
# it uses the lock to ensure only one worker is executing
if not self.has_lock:
return
if (not ovn_conf.get_global_dhcpv4_opts() and
not ovn_conf.get_global_dhcpv6_opts()):
# No need to scan the subnets if the settings are unset.
raise periodics.NeverAgain()
LOG.debug('Maintenance task: Checking DHCP options on subnets')
self._sync_timer.restart()
fix_subnets = self._check_subnet_global_dhcp_opts()
if fix_subnets:
admin_context = n_context.get_admin_context()
LOG.debug('Triggering update for %s subnets', len(fix_subnets))
for subnet in fix_subnets:
neutron_net = self._ovn_client._plugin.get_network(
admin_context, subnet['network_id'])
try:
self._ovn_client.update_subnet(subnet, neutron_net)
except Exception:
LOG.exception('Failed to update subnet %s',
subnet['id'])
self._sync_timer.stop()
LOG.info('Maintenance task: DHCP options check finished '
'(took %.2f seconds)', self._sync_timer.elapsed())
raise periodics.NeverAgain()

27
networking_ovn/common/ovn_client.py

@ -1422,6 +1422,29 @@ class OVNClient(object):
return dhcp_options
def _process_global_dhcp_opts(self, options, ip_version):
if ip_version == 4:
global_options = config.get_global_dhcpv4_opts()
else:
global_options = config.get_global_dhcpv6_opts()
for option, value in global_options.items():
if option in ovn_const.GLOBAL_DHCP_OPTS_BLACKLIST[ip_version]:
# This option is not allowed to be set with a global setting
LOG.debug('DHCP option %s is not permitted to be set in '
'global options. This option will be ignored.')
continue
# If the value is null (i.e. config ntp_server:), treat it as
# a request to remove the option
if value:
options[option] = value
else:
try:
del(options[option])
except KeyError:
# Option not present, job done
pass
def _get_ovn_dhcpv4_opts(self, subnet, network, server_mac=None):
metadata_port_ip = self._find_metadata_port_ip(
n_context.get_admin_context(), subnet)
@ -1479,6 +1502,8 @@ class OVNClient(object):
options['classless_static_route'] = '{' + ', '.join(routes) + '}'
self._process_global_dhcp_opts(options, ip_version=4)
return options
def _get_ovn_dhcpv6_opts(self, subnet, server_id=None):
@ -1496,6 +1521,8 @@ class OVNClient(object):
if subnet.get('ipv6_address_mode') == const.DHCPV6_STATELESS:
dhcpv6_opts[ovn_const.DHCPV6_STATELESS_OPT] = 'true'
self._process_global_dhcp_opts(dhcpv6_opts, ip_version=6)
return dhcpv6_opts
def _remove_subnet_dhcp_options(self, subnet_id, txn):

135
networking_ovn/tests/functional/test_maintenance.py

@ -15,10 +15,12 @@
import mock
from futurist import periodics
from neutron.tests.unit.api import test_extensions
from neutron.tests.unit.extensions import test_extraroute
from neutron.tests.unit.extensions import test_securitygroup
from networking_ovn.common import config as ovn_config
from networking_ovn.common import constants as ovn_const
from networking_ovn.common import maintenance
from networking_ovn.common import utils
@ -97,13 +99,38 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase):
ovn_const.OVN_PORT_NAME_EXT_ID_KEY) == name):
return row
def _create_subnet(self, name, net_id):
def _set_global_dhcp_opts(self, ip_version, opts):
opt_string = ','.join(['{0}:{1}'.format(key, value)
for key, value
in opts.items()])
if ip_version == 6:
ovn_config.cfg.CONF.set_override('ovn_dhcp6_global_options',
opt_string,
group='ovn')
if ip_version == 4:
ovn_config.cfg.CONF.set_override('ovn_dhcp4_global_options',
opt_string,
group='ovn')
def _unset_global_dhcp_opts(self, ip_version):
if ip_version == 6:
ovn_config.cfg.CONF.clear_override('ovn_dhcp6_global_options',
group='ovn')
if ip_version == 4:
ovn_config.cfg.CONF.clear_override('ovn_dhcp4_global_options',
group='ovn')
def _create_subnet(self, name, net_id, ip_version=4):
data = {'subnet': {'name': name,
'tenant_id': self._tenant_id,
'network_id': net_id,
'cidr': '10.0.0.0/24',
'ip_version': 4,
'ip_version': ip_version,
'enable_dhcp': True}}
if ip_version == 4:
data['subnet']['cidr'] = '10.0.0.0/24'
else:
data['subnet']['cidr'] = 'eef0::/64'
req = self.new_create_request('subnets', data, self.fmt)
res = req.get_response(self.api)
return self.deserialize(self.fmt, res)['subnet']
@ -325,6 +352,108 @@ class TestMaintenance(_TestMaintenanceHelper):
# Assert the revision number no longer exists
self.assertIsNone(db_rev.get_revision_row(neutron_obj['id']))
def test_subnet_global_dhcp4_opts(self):
obj_name = 'globaltestsubnet'
options = {'ntp_server': '1.2.3.4'}
neutron_net = self._create_network('network1')
# Create a subnet without global options
neutron_sub = self._create_subnet(obj_name, neutron_net['id'])
# Assert that the option is not set
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertIsNone(ovn_obj.options.get('ntp_server', None))
# Set some global DHCP Options
self._set_global_dhcp_opts(ip_version=4, opts=options)
# Run the maintenance task to add the new options
self.assertRaises(periodics.NeverAgain,
self.maint.check_global_dhcp_opts)
# Assert that the option was added
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'1.2.3.4')
# Change the global option
new_options = {'ntp_server': '4.3.2.1'}
self._set_global_dhcp_opts(ip_version=4, opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
self.maint.check_global_dhcp_opts)
# Assert that the option was changed
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'4.3.2.1')
# Change the global option to null
new_options = {'ntp_server': ''}
self._set_global_dhcp_opts(ip_version=4, opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
self.maint.check_global_dhcp_opts)
# Assert that the option was removed
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertIsNone(ovn_obj.options.get('ntp_server', None))
def test_subnet_global_dhcp6_opts(self):
obj_name = 'globaltestsubnet'
options = {'ntp_server': '1.2.3.4'}
neutron_net = self._create_network('network1')
# Create a subnet without global options
neutron_sub = self._create_subnet(obj_name, neutron_net['id'], 6)
# Assert that the option is not set
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertIsNone(ovn_obj.options.get('ntp_server', None))
# Set some global DHCP Options
self._set_global_dhcp_opts(ip_version=6, opts=options)
# Run the maintenance task to add the new options
self.assertRaises(periodics.NeverAgain,
self.maint.check_global_dhcp_opts)
# Assert that the option was added
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'1.2.3.4')
# Change the global option
new_options = {'ntp_server': '4.3.2.1'}
self._set_global_dhcp_opts(ip_version=6, opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
self.maint.check_global_dhcp_opts)
# Assert that the option was changed
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertEqual(
ovn_obj.options.get('ntp_server', None),
'4.3.2.1')
# Change the global option to null
new_options = {'ntp_server': ''}
self._set_global_dhcp_opts(ip_version=6, opts=new_options)
# Run the maintenance task to update the options
self.assertRaises(periodics.NeverAgain,
self.maint.check_global_dhcp_opts)
# Assert that the option was removed
ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
self.assertIsNone(ovn_obj.options.get('ntp_server', None))
def test_subnet(self):
obj_name = 'subnettest'
neutron_net = self._create_network('network1')

69
networking_ovn/tests/unit/ml2/test_mech_driver.py

@ -1725,6 +1725,75 @@ class TestOVNMechansimDriverDHCPOptions(OVNMechanismDriverTestCase):
self._test_get_ovn_dhcp_options_helper(subnet, network,
expected_dhcp_options)
def test_get_ovn_dhcp_options_with_global_options(self):
ovn_config.cfg.CONF.set_override('ovn_dhcp4_global_options',
'ntp_server:8.8.8.8,'
'mtu:9000,'
'wpad:',
group='ovn')
subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
'cidr': '10.0.0.0/24',
'ip_version': 4,
'enable_dhcp': True,
'gateway_ip': '10.0.0.1',
'dns_nameservers': ['7.7.7.7', '8.8.8.8'],
'host_routes': [{'destination': '20.0.0.4',
'nexthop': '10.0.0.100'}]}
network = {'id': 'network-id', 'mtu': 1400}
expected_dhcp_options = {'cidr': '10.0.0.0/24',
'external_ids': {
'subnet_id': 'foo-subnet',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}}
expected_dhcp_options['options'] = {
'server_id': subnet['gateway_ip'],
'server_mac': '01:02:03:04:05:06',
'lease_time': str(12 * 60 * 60),
'mtu': '1400',
'router': subnet['gateway_ip'],
'ntp_server': '8.8.8.8',
'dns_server': '{7.7.7.7, 8.8.8.8}',
'classless_static_route':
'{20.0.0.4,10.0.0.100, 0.0.0.0/0,10.0.0.1}'
}
self._test_get_ovn_dhcp_options_helper(subnet, network,
expected_dhcp_options)
expected_dhcp_options['options']['server_mac'] = '11:22:33:44:55:66'
self._test_get_ovn_dhcp_options_helper(subnet, network,
expected_dhcp_options,
service_mac='11:22:33:44:55:66')
def test_get_ovn_dhcp_options_with_global_options_ipv6(self):
ovn_config.cfg.CONF.set_override('ovn_dhcp6_global_options',
'ntp_server:8.8.8.8,'
'server_id:01:02:03:04:05:04,'
'wpad:',
group='ovn')
subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
'cidr': 'ae70::/24',
'ip_version': 6,
'enable_dhcp': True,
'dns_nameservers': ['7.7.7.7', '8.8.8.8']}
network = {'id': 'network-id', 'mtu': 1400}
ext_ids = {'subnet_id': 'foo-subnet',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}
expected_dhcp_options = {
'cidr': 'ae70::/24', 'external_ids': ext_ids,
'options': {'server_id': '01:02:03:04:05:06',
'ntp_server': '8.8.8.8',
'dns_server': '{7.7.7.7, 8.8.8.8}'}}
self._test_get_ovn_dhcp_options_helper(subnet, network,
expected_dhcp_options)
expected_dhcp_options['options']['server_id'] = '11:22:33:44:55:66'
self._test_get_ovn_dhcp_options_helper(subnet, network,
expected_dhcp_options,
service_mac='11:22:33:44:55:66')
def test_get_ovn_dhcp_options_ipv6_subnet(self):
subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
'cidr': 'ae70::/24',

10
releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml

@ -0,0 +1,10 @@
---
features:
- |
Added config options ovn_dhcp4_global_option and ovn_dhcp6_global_options.
These options allow configuring DHCP options that will be enforced on all
subnets controlled by networking_ovn.
upgrade:
- |
If ovn_dhcp4_global_option or ovn_dhcp6_global_options is set, all
existing subnets will be checked and updated when Neutron is started.
Loading…
Cancel
Save