summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Austin <aaustin@redhat.com>2018-08-07 10:57:43 -0500
committerLucas Alvares Gomes <lucasagomes@gmail.com>2018-11-21 09:52:41 +0000
commit545d098bfaf089b8ce89b07bb05e66ed4125852b (patch)
treeba53d0d0f84d23e656622de566f9aadb8c8d8cd2
parent0a6a14de3cc7c0ebc283dbdb98940e59415aaef3 (diff)
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
Notes
Notes (review): Code-Review+2: Lucas Alvares Gomes <lucasagomes@gmail.com> Code-Review+2: Daniel Alvarez <dalvarez@redhat.com> Workflow+1: Lucas Alvares Gomes <lucasagomes@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Wed, 21 Nov 2018 22:45:02 +0000 Reviewed-on: https://review.openstack.org/589528 Project: openstack/networking-ovn Branch: refs/heads/master
-rw-r--r--networking_ovn/common/config.py34
-rw-r--r--networking_ovn/common/constants.py8
-rw-r--r--networking_ovn/common/maintenance.py65
-rw-r--r--networking_ovn/common/ovn_client.py27
-rw-r--r--networking_ovn/tests/functional/test_maintenance.py135
-rw-r--r--networking_ovn/tests/unit/ml2/test_mech_driver.py69
-rw-r--r--releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml10
7 files changed, 345 insertions, 3 deletions
diff --git a/networking_ovn/common/config.py b/networking_ovn/common/config.py
index cd8387d..c0803bc 100644
--- a/networking_ovn/common/config.py
+++ b/networking_ovn/common/config.py
@@ -154,6 +154,32 @@ ovn_opts = [
154 "field is empty. If both subnet's dns_nameservers and " 154 "field is empty. If both subnet's dns_nameservers and "
155 "this option is empty, then the DNS resolvers on the " 155 "this option is empty, then the DNS resolvers on the "
156 "host running the neutron server will be used.")), 156 "host running the neutron server will be used.")),
157 cfg.DictOpt('ovn_dhcp4_global_options',
158 default={},
159 help=_("Dictionary of global DHCPv4 options which will be "
160 "automatically set on each subnet upon creation and "
161 "on all existing subnets when Neutron starts.\n"
162 "An empty value for a DHCP option will cause that "
163 "option to be unset globally.\n"
164 "EXAMPLES:\n"
165 "- ntp_server:1.2.3.4,wpad:1.2.3.5 - Set ntp_server "
166 "and wpad\n"
167 "- ntp_server:,wpad:1.2.3.5 - Unset ntp_server and "
168 "set wpad\n"
169 "See the ovn-nb(5) man page for available options.")),
170 cfg.DictOpt('ovn_dhcp6_global_options',
171 default={},
172 help=_("Dictionary of global DHCPv6 options which will be "
173 "automatically set on each subnet upon creation and "
174 "on all existing subnets when Neutron starts.\n"
175 "An empty value for a DHCP option will cause that "
176 "option to be unset globally.\n"
177 "EXAMPLES:\n"
178 "- ntp_server:1.2.3.4,wpad:1.2.3.5 - Set ntp_server "
179 "and wpad\n"
180 "- ntp_server:,wpad:1.2.3.5 - Unset ntp_server and "
181 "set wpad\n"
182 "See the ovn-nb(5) man page for available options.")),
157] 183]
158 184
159cfg.CONF.register_opts(ovn_opts, group='ovn') 185cfg.CONF.register_opts(ovn_opts, group='ovn')
@@ -241,6 +267,14 @@ def get_dns_servers():
241 return cfg.CONF.ovn.dns_servers 267 return cfg.CONF.ovn.dns_servers
242 268
243 269
270def get_global_dhcpv4_opts():
271 return cfg.CONF.ovn.ovn_dhcp4_global_options
272
273
274def get_global_dhcpv6_opts():
275 return cfg.CONF.ovn.ovn_dhcp6_global_options
276
277
244def setup_logging(): 278def setup_logging():
245 """Sets up the logging options for a log with supplied name.""" 279 """Sets up the logging options for a log with supplied name."""
246 product_name = "networking-ovn" 280 product_name = "networking-ovn"
diff --git a/networking_ovn/common/constants.py b/networking_ovn/common/constants.py
index 9a4c17a..2a89e03 100644
--- a/networking_ovn/common/constants.py
+++ b/networking_ovn/common/constants.py
@@ -91,6 +91,14 @@ SUPPORTED_DHCP_OPTS = {
91 6: ['server-id', 'dns-server', 'domain-search']} 91 6: ['server-id', 'dns-server', 'domain-search']}
92DHCPV6_STATELESS_OPT = 'dhcpv6_stateless' 92DHCPV6_STATELESS_OPT = 'dhcpv6_stateless'
93 93
94# When setting global DHCP options, these options will be ignored
95# as they are required for basic network functions and will be
96# set by Neutron.
97GLOBAL_DHCP_OPTS_BLACKLIST = {
98 4: ['server_id', 'lease_time', 'mtu', 'router', 'server_mac',
99 'dns_server', 'classless_static_route'],
100 6: ['dhcpv6_stateless', 'dns_server', 'server_id']}
101
94CHASSIS_DATAPATH_NETDEV = 'netdev' 102CHASSIS_DATAPATH_NETDEV = 'netdev'
95CHASSIS_IFACE_DPDKVHOSTUSER = 'dpdkvhostuser' 103CHASSIS_IFACE_DPDKVHOSTUSER = 'dpdkvhostuser'
96 104
diff --git a/networking_ovn/common/maintenance.py b/networking_ovn/common/maintenance.py
index c43b2f0..1520f78 100644
--- a/networking_ovn/common/maintenance.py
+++ b/networking_ovn/common/maintenance.py
@@ -18,12 +18,14 @@ import threading
18 18
19from futurist import periodics 19from futurist import periodics
20from neutron.common import config as n_conf 20from neutron.common import config as n_conf
21from neutron_lib import constants as n_const
21from neutron_lib import context as n_context 22from neutron_lib import context as n_context
22from neutron_lib import exceptions as n_exc 23from neutron_lib import exceptions as n_exc
23from neutron_lib import worker 24from neutron_lib import worker
24from oslo_log import log 25from oslo_log import log
25from oslo_utils import timeutils 26from oslo_utils import timeutils
26 27
28from networking_ovn.common import config as ovn_conf
27from networking_ovn.common import constants as ovn_const 29from networking_ovn.common import constants as ovn_const
28from networking_ovn.db import maintenance as db_maint 30from networking_ovn.db import maintenance as db_maint
29from networking_ovn.db import revision as db_rev 31from networking_ovn.db import revision as db_rev
@@ -301,3 +303,66 @@ class DBInconsistenciesPeriodics(object):
301 router_id = port['device_id'] 303 router_id = port['device_id']
302 self._ovn_client._l3_plugin.add_router_interface( 304 self._ovn_client._l3_plugin.add_router_interface(
303 admin_context, router_id, {'port_id': port['id']}, may_exist=True) 305 admin_context, router_id, {'port_id': port['id']}, may_exist=True)
306
307 def _check_subnet_global_dhcp_opts(self):
308 inconsistent_subnets = []
309 admin_context = n_context.get_admin_context()
310 subnet_filter = {'enable_dhcp': [True]}
311 neutron_subnets = self._ovn_client._plugin.get_subnets(
312 admin_context, subnet_filter)
313 global_v4_opts = ovn_conf.get_global_dhcpv4_opts()
314 global_v6_opts = ovn_conf.get_global_dhcpv6_opts()
315 LOG.debug('Checking %s subnets for global DHCP option consistency',
316 len(neutron_subnets))
317 for subnet in neutron_subnets:
318 ovn_dhcp_opts = self._nb_idl.get_subnet_dhcp_options(
319 subnet['id'])['subnet']
320 inconsistent_opts = []
321 if ovn_dhcp_opts:
322 if subnet['ip_version'] == n_const.IP_VERSION_4:
323 for opt, value in global_v4_opts.items():
324 if value != ovn_dhcp_opts['options'].get(opt, None):
325 inconsistent_opts.append(opt)
326 if subnet['ip_version'] == n_const.IP_VERSION_6:
327 for opt, value in global_v6_opts.items():
328 if value != ovn_dhcp_opts['options'].get(opt, None):
329 inconsistent_opts.append(opt)
330 if inconsistent_opts:
331 LOG.debug('Subnet %s has inconsistent DHCP opts: %s',
332 subnet['id'], inconsistent_opts)
333 inconsistent_subnets.append(subnet)
334 return inconsistent_subnets
335
336 # A static spacing value is used here, but this method will only run
337 # once per lock due to the use of periodics.NeverAgain().
338 @periodics.periodic(spacing=600,
339 run_immediately=True)
340 def check_global_dhcp_opts(self):
341 # This periodic task is included in DBInconsistenciesPeriodics since
342 # it uses the lock to ensure only one worker is executing
343 if not self.has_lock:
344 return
345 if (not ovn_conf.get_global_dhcpv4_opts() and
346 not ovn_conf.get_global_dhcpv6_opts()):
347 # No need to scan the subnets if the settings are unset.
348 raise periodics.NeverAgain()
349 LOG.debug('Maintenance task: Checking DHCP options on subnets')
350 self._sync_timer.restart()
351 fix_subnets = self._check_subnet_global_dhcp_opts()
352 if fix_subnets:
353 admin_context = n_context.get_admin_context()
354 LOG.debug('Triggering update for %s subnets', len(fix_subnets))
355 for subnet in fix_subnets:
356 neutron_net = self._ovn_client._plugin.get_network(
357 admin_context, subnet['network_id'])
358 try:
359 self._ovn_client.update_subnet(subnet, neutron_net)
360 except Exception:
361 LOG.exception('Failed to update subnet %s',
362 subnet['id'])
363
364 self._sync_timer.stop()
365 LOG.info('Maintenance task: DHCP options check finished '
366 '(took %.2f seconds)', self._sync_timer.elapsed())
367
368 raise periodics.NeverAgain()
diff --git a/networking_ovn/common/ovn_client.py b/networking_ovn/common/ovn_client.py
index 3f1f0b6..2036d9d 100644
--- a/networking_ovn/common/ovn_client.py
+++ b/networking_ovn/common/ovn_client.py
@@ -1422,6 +1422,29 @@ class OVNClient(object):
1422 1422
1423 return dhcp_options 1423 return dhcp_options
1424 1424
1425 def _process_global_dhcp_opts(self, options, ip_version):
1426 if ip_version == 4:
1427 global_options = config.get_global_dhcpv4_opts()
1428 else:
1429 global_options = config.get_global_dhcpv6_opts()
1430
1431 for option, value in global_options.items():
1432 if option in ovn_const.GLOBAL_DHCP_OPTS_BLACKLIST[ip_version]:
1433 # This option is not allowed to be set with a global setting
1434 LOG.debug('DHCP option %s is not permitted to be set in '
1435 'global options. This option will be ignored.')
1436 continue
1437 # If the value is null (i.e. config ntp_server:), treat it as
1438 # a request to remove the option
1439 if value:
1440 options[option] = value
1441 else:
1442 try:
1443 del(options[option])
1444 except KeyError:
1445 # Option not present, job done
1446 pass
1447
1425 def _get_ovn_dhcpv4_opts(self, subnet, network, server_mac=None): 1448 def _get_ovn_dhcpv4_opts(self, subnet, network, server_mac=None):
1426 metadata_port_ip = self._find_metadata_port_ip( 1449 metadata_port_ip = self._find_metadata_port_ip(
1427 n_context.get_admin_context(), subnet) 1450 n_context.get_admin_context(), subnet)
@@ -1479,6 +1502,8 @@ class OVNClient(object):
1479 1502
1480 options['classless_static_route'] = '{' + ', '.join(routes) + '}' 1503 options['classless_static_route'] = '{' + ', '.join(routes) + '}'
1481 1504
1505 self._process_global_dhcp_opts(options, ip_version=4)
1506
1482 return options 1507 return options
1483 1508
1484 def _get_ovn_dhcpv6_opts(self, subnet, server_id=None): 1509 def _get_ovn_dhcpv6_opts(self, subnet, server_id=None):
@@ -1496,6 +1521,8 @@ class OVNClient(object):
1496 if subnet.get('ipv6_address_mode') == const.DHCPV6_STATELESS: 1521 if subnet.get('ipv6_address_mode') == const.DHCPV6_STATELESS:
1497 dhcpv6_opts[ovn_const.DHCPV6_STATELESS_OPT] = 'true' 1522 dhcpv6_opts[ovn_const.DHCPV6_STATELESS_OPT] = 'true'
1498 1523
1524 self._process_global_dhcp_opts(dhcpv6_opts, ip_version=6)
1525
1499 return dhcpv6_opts 1526 return dhcpv6_opts
1500 1527
1501 def _remove_subnet_dhcp_options(self, subnet_id, txn): 1528 def _remove_subnet_dhcp_options(self, subnet_id, txn):
diff --git a/networking_ovn/tests/functional/test_maintenance.py b/networking_ovn/tests/functional/test_maintenance.py
index e8a3e05..09b94c1 100644
--- a/networking_ovn/tests/functional/test_maintenance.py
+++ b/networking_ovn/tests/functional/test_maintenance.py
@@ -15,10 +15,12 @@
15 15
16import mock 16import mock
17 17
18from futurist import periodics
18from neutron.tests.unit.api import test_extensions 19from neutron.tests.unit.api import test_extensions
19from neutron.tests.unit.extensions import test_extraroute 20from neutron.tests.unit.extensions import test_extraroute
20from neutron.tests.unit.extensions import test_securitygroup 21from neutron.tests.unit.extensions import test_securitygroup
21 22
23from networking_ovn.common import config as ovn_config
22from networking_ovn.common import constants as ovn_const 24from networking_ovn.common import constants as ovn_const
23from networking_ovn.common import maintenance 25from networking_ovn.common import maintenance
24from networking_ovn.common import utils 26from networking_ovn.common import utils
@@ -97,13 +99,38 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase):
97 ovn_const.OVN_PORT_NAME_EXT_ID_KEY) == name): 99 ovn_const.OVN_PORT_NAME_EXT_ID_KEY) == name):
98 return row 100 return row
99 101
100 def _create_subnet(self, name, net_id): 102 def _set_global_dhcp_opts(self, ip_version, opts):
103 opt_string = ','.join(['{0}:{1}'.format(key, value)
104 for key, value
105 in opts.items()])
106 if ip_version == 6:
107 ovn_config.cfg.CONF.set_override('ovn_dhcp6_global_options',
108 opt_string,
109 group='ovn')
110 if ip_version == 4:
111 ovn_config.cfg.CONF.set_override('ovn_dhcp4_global_options',
112 opt_string,
113 group='ovn')
114
115 def _unset_global_dhcp_opts(self, ip_version):
116 if ip_version == 6:
117 ovn_config.cfg.CONF.clear_override('ovn_dhcp6_global_options',
118 group='ovn')
119 if ip_version == 4:
120 ovn_config.cfg.CONF.clear_override('ovn_dhcp4_global_options',
121 group='ovn')
122
123 def _create_subnet(self, name, net_id, ip_version=4):
101 data = {'subnet': {'name': name, 124 data = {'subnet': {'name': name,
102 'tenant_id': self._tenant_id, 125 'tenant_id': self._tenant_id,
103 'network_id': net_id, 126 'network_id': net_id,
104 'cidr': '10.0.0.0/24', 127 'ip_version': ip_version,
105 'ip_version': 4,
106 'enable_dhcp': True}} 128 'enable_dhcp': True}}
129 if ip_version == 4:
130 data['subnet']['cidr'] = '10.0.0.0/24'
131 else:
132 data['subnet']['cidr'] = 'eef0::/64'
133
107 req = self.new_create_request('subnets', data, self.fmt) 134 req = self.new_create_request('subnets', data, self.fmt)
108 res = req.get_response(self.api) 135 res = req.get_response(self.api)
109 return self.deserialize(self.fmt, res)['subnet'] 136 return self.deserialize(self.fmt, res)['subnet']
@@ -325,6 +352,108 @@ class TestMaintenance(_TestMaintenanceHelper):
325 # Assert the revision number no longer exists 352 # Assert the revision number no longer exists
326 self.assertIsNone(db_rev.get_revision_row(neutron_obj['id'])) 353 self.assertIsNone(db_rev.get_revision_row(neutron_obj['id']))
327 354
355 def test_subnet_global_dhcp4_opts(self):
356 obj_name = 'globaltestsubnet'
357 options = {'ntp_server': '1.2.3.4'}
358 neutron_net = self._create_network('network1')
359
360 # Create a subnet without global options
361 neutron_sub = self._create_subnet(obj_name, neutron_net['id'])
362
363 # Assert that the option is not set
364 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
365 self.assertIsNone(ovn_obj.options.get('ntp_server', None))
366
367 # Set some global DHCP Options
368 self._set_global_dhcp_opts(ip_version=4, opts=options)
369
370 # Run the maintenance task to add the new options
371 self.assertRaises(periodics.NeverAgain,
372 self.maint.check_global_dhcp_opts)
373
374 # Assert that the option was added
375 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
376 self.assertEqual(
377 ovn_obj.options.get('ntp_server', None),
378 '1.2.3.4')
379
380 # Change the global option
381 new_options = {'ntp_server': '4.3.2.1'}
382 self._set_global_dhcp_opts(ip_version=4, opts=new_options)
383
384 # Run the maintenance task to update the options
385 self.assertRaises(periodics.NeverAgain,
386 self.maint.check_global_dhcp_opts)
387
388 # Assert that the option was changed
389 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
390 self.assertEqual(
391 ovn_obj.options.get('ntp_server', None),
392 '4.3.2.1')
393
394 # Change the global option to null
395 new_options = {'ntp_server': ''}
396 self._set_global_dhcp_opts(ip_version=4, opts=new_options)
397
398 # Run the maintenance task to update the options
399 self.assertRaises(periodics.NeverAgain,
400 self.maint.check_global_dhcp_opts)
401
402 # Assert that the option was removed
403 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
404 self.assertIsNone(ovn_obj.options.get('ntp_server', None))
405
406 def test_subnet_global_dhcp6_opts(self):
407 obj_name = 'globaltestsubnet'
408 options = {'ntp_server': '1.2.3.4'}
409 neutron_net = self._create_network('network1')
410
411 # Create a subnet without global options
412 neutron_sub = self._create_subnet(obj_name, neutron_net['id'], 6)
413
414 # Assert that the option is not set
415 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
416 self.assertIsNone(ovn_obj.options.get('ntp_server', None))
417
418 # Set some global DHCP Options
419 self._set_global_dhcp_opts(ip_version=6, opts=options)
420
421 # Run the maintenance task to add the new options
422 self.assertRaises(periodics.NeverAgain,
423 self.maint.check_global_dhcp_opts)
424
425 # Assert that the option was added
426 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
427 self.assertEqual(
428 ovn_obj.options.get('ntp_server', None),
429 '1.2.3.4')
430
431 # Change the global option
432 new_options = {'ntp_server': '4.3.2.1'}
433 self._set_global_dhcp_opts(ip_version=6, opts=new_options)
434
435 # Run the maintenance task to update the options
436 self.assertRaises(periodics.NeverAgain,
437 self.maint.check_global_dhcp_opts)
438
439 # Assert that the option was changed
440 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
441 self.assertEqual(
442 ovn_obj.options.get('ntp_server', None),
443 '4.3.2.1')
444
445 # Change the global option to null
446 new_options = {'ntp_server': ''}
447 self._set_global_dhcp_opts(ip_version=6, opts=new_options)
448
449 # Run the maintenance task to update the options
450 self.assertRaises(periodics.NeverAgain,
451 self.maint.check_global_dhcp_opts)
452
453 # Assert that the option was removed
454 ovn_obj = self._find_subnet_row_by_id(neutron_sub['id'])
455 self.assertIsNone(ovn_obj.options.get('ntp_server', None))
456
328 def test_subnet(self): 457 def test_subnet(self):
329 obj_name = 'subnettest' 458 obj_name = 'subnettest'
330 neutron_net = self._create_network('network1') 459 neutron_net = self._create_network('network1')
diff --git a/networking_ovn/tests/unit/ml2/test_mech_driver.py b/networking_ovn/tests/unit/ml2/test_mech_driver.py
index 9ee6380..d09efb6 100644
--- a/networking_ovn/tests/unit/ml2/test_mech_driver.py
+++ b/networking_ovn/tests/unit/ml2/test_mech_driver.py
@@ -1725,6 +1725,75 @@ class TestOVNMechansimDriverDHCPOptions(OVNMechanismDriverTestCase):
1725 self._test_get_ovn_dhcp_options_helper(subnet, network, 1725 self._test_get_ovn_dhcp_options_helper(subnet, network,
1726 expected_dhcp_options) 1726 expected_dhcp_options)
1727 1727
1728 def test_get_ovn_dhcp_options_with_global_options(self):
1729 ovn_config.cfg.CONF.set_override('ovn_dhcp4_global_options',
1730 'ntp_server:8.8.8.8,'
1731 'mtu:9000,'
1732 'wpad:',
1733 group='ovn')
1734
1735 subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
1736 'cidr': '10.0.0.0/24',
1737 'ip_version': 4,
1738 'enable_dhcp': True,
1739 'gateway_ip': '10.0.0.1',
1740 'dns_nameservers': ['7.7.7.7', '8.8.8.8'],
1741 'host_routes': [{'destination': '20.0.0.4',
1742 'nexthop': '10.0.0.100'}]}
1743 network = {'id': 'network-id', 'mtu': 1400}
1744
1745 expected_dhcp_options = {'cidr': '10.0.0.0/24',
1746 'external_ids': {
1747 'subnet_id': 'foo-subnet',
1748 ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}}
1749 expected_dhcp_options['options'] = {
1750 'server_id': subnet['gateway_ip'],
1751 'server_mac': '01:02:03:04:05:06',
1752 'lease_time': str(12 * 60 * 60),
1753 'mtu': '1400',
1754 'router': subnet['gateway_ip'],
1755 'ntp_server': '8.8.8.8',
1756 'dns_server': '{7.7.7.7, 8.8.8.8}',
1757 'classless_static_route':
1758 '{20.0.0.4,10.0.0.100, 0.0.0.0/0,10.0.0.1}'
1759 }
1760
1761 self._test_get_ovn_dhcp_options_helper(subnet, network,
1762 expected_dhcp_options)
1763 expected_dhcp_options['options']['server_mac'] = '11:22:33:44:55:66'
1764 self._test_get_ovn_dhcp_options_helper(subnet, network,
1765 expected_dhcp_options,
1766 service_mac='11:22:33:44:55:66')
1767
1768 def test_get_ovn_dhcp_options_with_global_options_ipv6(self):
1769 ovn_config.cfg.CONF.set_override('ovn_dhcp6_global_options',
1770 'ntp_server:8.8.8.8,'
1771 'server_id:01:02:03:04:05:04,'
1772 'wpad:',
1773 group='ovn')
1774
1775 subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
1776 'cidr': 'ae70::/24',
1777 'ip_version': 6,
1778 'enable_dhcp': True,
1779 'dns_nameservers': ['7.7.7.7', '8.8.8.8']}
1780 network = {'id': 'network-id', 'mtu': 1400}
1781
1782 ext_ids = {'subnet_id': 'foo-subnet',
1783 ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}
1784 expected_dhcp_options = {
1785 'cidr': 'ae70::/24', 'external_ids': ext_ids,
1786 'options': {'server_id': '01:02:03:04:05:06',
1787 'ntp_server': '8.8.8.8',
1788 'dns_server': '{7.7.7.7, 8.8.8.8}'}}
1789
1790 self._test_get_ovn_dhcp_options_helper(subnet, network,
1791 expected_dhcp_options)
1792 expected_dhcp_options['options']['server_id'] = '11:22:33:44:55:66'
1793 self._test_get_ovn_dhcp_options_helper(subnet, network,
1794 expected_dhcp_options,
1795 service_mac='11:22:33:44:55:66')
1796
1728 def test_get_ovn_dhcp_options_ipv6_subnet(self): 1797 def test_get_ovn_dhcp_options_ipv6_subnet(self):
1729 subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 1798 subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
1730 'cidr': 'ae70::/24', 1799 'cidr': 'ae70::/24',
diff --git a/releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml b/releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml
new file mode 100644
index 0000000..eea6b60
--- /dev/null
+++ b/releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml
@@ -0,0 +1,10 @@
1---
2features:
3 - |
4 Added config options ovn_dhcp4_global_option and ovn_dhcp6_global_options.
5 These options allow configuring DHCP options that will be enforced on all
6 subnets controlled by networking_ovn.
7upgrade:
8 - |
9 If ovn_dhcp4_global_option or ovn_dhcp6_global_options is set, all
10 existing subnets will be checked and updated when Neutron is started.