summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-11-21 22:45:02 +0000
committerGerrit Code Review <review@openstack.org>2018-11-21 22:45:02 +0000
commita4e69319ad61dffe0482d6fe7a9b421b55145f0a (patch)
treeb51fe8d1b206365bad4dce522fd55a7d48de31da
parent687889d92706c9120b3fdd0efcc22a72e0c64637 (diff)
parent545d098bfaf089b8ce89b07bb05e66ed4125852b (diff)
Merge "Add support for global DHCP options with OVN DHCP."
-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.