Browse Source

Merge "Add support for global DHCP options with OVN DHCP."

Zuul 5 months ago
parent
commit
a4e69319ad

+ 34
- 0
networking_ovn/common/config.py View File

@@ -154,6 +154,32 @@ ovn_opts = [
154 154
                        "field is empty. If both subnet's dns_nameservers and "
155 155
                        "this option is empty, then the DNS resolvers on the "
156 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
 
159 185
 cfg.CONF.register_opts(ovn_opts, group='ovn')
@@ -241,6 +267,14 @@ def get_dns_servers():
241 267
     return cfg.CONF.ovn.dns_servers
242 268
 
243 269
 
270
+def get_global_dhcpv4_opts():
271
+    return cfg.CONF.ovn.ovn_dhcp4_global_options
272
+
273
+
274
+def get_global_dhcpv6_opts():
275
+    return cfg.CONF.ovn.ovn_dhcp6_global_options
276
+
277
+
244 278
 def setup_logging():
245 279
     """Sets up the logging options for a log with supplied name."""
246 280
     product_name = "networking-ovn"

+ 8
- 0
networking_ovn/common/constants.py View File

@@ -91,6 +91,14 @@ SUPPORTED_DHCP_OPTS = {
91 91
     6: ['server-id', 'dns-server', 'domain-search']}
92 92
 DHCPV6_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.
97
+GLOBAL_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
+
94 102
 CHASSIS_DATAPATH_NETDEV = 'netdev'
95 103
 CHASSIS_IFACE_DPDKVHOSTUSER = 'dpdkvhostuser'
96 104
 

+ 65
- 0
networking_ovn/common/maintenance.py View File

@@ -18,12 +18,14 @@ import threading
18 18
 
19 19
 from futurist import periodics
20 20
 from neutron.common import config as n_conf
21
+from neutron_lib import constants as n_const
21 22
 from neutron_lib import context as n_context
22 23
 from neutron_lib import exceptions as n_exc
23 24
 from neutron_lib import worker
24 25
 from oslo_log import log
25 26
 from oslo_utils import timeutils
26 27
 
28
+from networking_ovn.common import config as ovn_conf
27 29
 from networking_ovn.common import constants as ovn_const
28 30
 from networking_ovn.db import maintenance as db_maint
29 31
 from networking_ovn.db import revision as db_rev
@@ -301,3 +303,66 @@ class DBInconsistenciesPeriodics(object):
301 303
         router_id = port['device_id']
302 304
         self._ovn_client._l3_plugin.add_router_interface(
303 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()

+ 27
- 0
networking_ovn/common/ovn_client.py View File

@@ -1422,6 +1422,29 @@ class OVNClient(object):
1422 1422
 
1423 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 1448
     def _get_ovn_dhcpv4_opts(self, subnet, network, server_mac=None):
1426 1449
         metadata_port_ip = self._find_metadata_port_ip(
1427 1450
             n_context.get_admin_context(), subnet)
@@ -1479,6 +1502,8 @@ class OVNClient(object):
1479 1502
 
1480 1503
             options['classless_static_route'] = '{' + ', '.join(routes) + '}'
1481 1504
 
1505
+        self._process_global_dhcp_opts(options, ip_version=4)
1506
+
1482 1507
         return options
1483 1508
 
1484 1509
     def _get_ovn_dhcpv6_opts(self, subnet, server_id=None):
@@ -1496,6 +1521,8 @@ class OVNClient(object):
1496 1521
         if subnet.get('ipv6_address_mode') == const.DHCPV6_STATELESS:
1497 1522
             dhcpv6_opts[ovn_const.DHCPV6_STATELESS_OPT] = 'true'
1498 1523
 
1524
+        self._process_global_dhcp_opts(dhcpv6_opts, ip_version=6)
1525
+
1499 1526
         return dhcpv6_opts
1500 1527
 
1501 1528
     def _remove_subnet_dhcp_options(self, subnet_id, txn):

+ 132
- 3
networking_ovn/tests/functional/test_maintenance.py View File

@@ -15,10 +15,12 @@
15 15
 
16 16
 import mock
17 17
 
18
+from futurist import periodics
18 19
 from neutron.tests.unit.api import test_extensions
19 20
 from neutron.tests.unit.extensions import test_extraroute
20 21
 from neutron.tests.unit.extensions import test_securitygroup
21 22
 
23
+from networking_ovn.common import config as ovn_config
22 24
 from networking_ovn.common import constants as ovn_const
23 25
 from networking_ovn.common import maintenance
24 26
 from networking_ovn.common import utils
@@ -97,13 +99,38 @@ class _TestMaintenanceHelper(base.TestOVNFunctionalBase):
97 99
                     ovn_const.OVN_PORT_NAME_EXT_ID_KEY) == name):
98 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 124
         data = {'subnet': {'name': name,
102 125
                            'tenant_id': self._tenant_id,
103 126
                            'network_id': net_id,
104
-                           'cidr': '10.0.0.0/24',
105
-                           'ip_version': 4,
127
+                           'ip_version': ip_version,
106 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 134
         req = self.new_create_request('subnets', data, self.fmt)
108 135
         res = req.get_response(self.api)
109 136
         return self.deserialize(self.fmt, res)['subnet']
@@ -325,6 +352,108 @@ class TestMaintenance(_TestMaintenanceHelper):
325 352
         # Assert the revision number no longer exists
326 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 457
     def test_subnet(self):
329 458
         obj_name = 'subnettest'
330 459
         neutron_net = self._create_network('network1')

+ 69
- 0
networking_ovn/tests/unit/ml2/test_mech_driver.py View File

@@ -1725,6 +1725,75 @@ class TestOVNMechansimDriverDHCPOptions(OVNMechanismDriverTestCase):
1725 1725
             self._test_get_ovn_dhcp_options_helper(subnet, network,
1726 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 1797
     def test_get_ovn_dhcp_options_ipv6_subnet(self):
1729 1798
         subnet = {'id': 'foo-subnet', 'network_id': 'network-id',
1730 1799
                   'cidr': 'ae70::/24',

+ 10
- 0
releasenotes/notes/ovn-global-dhcp-options-6a23e6a3619bba78.yaml View File

@@ -0,0 +1,10 @@
1
+---
2
+features:
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.
7
+upgrade:
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.

Loading…
Cancel
Save