From c6331c7cc01a9245437c83c6b4766e42ad880163 Mon Sep 17 00:00:00 2001 From: Gary Kotton Date: Mon, 20 Mar 2017 03:08:07 -0700 Subject: [PATCH] NSX|V: add in support for DHCP options The NSX|V supports the following options: 'interface-mtu': 'option26', 'tftp-server-name': 'option66', 'bootfile-name': 'option67', 'classless-static-route': 'option121', 'tftp-server-address': 'option150' All other options are generic. The code should be passed (number) and the value in in hex format, for example '80' and 'cafecafe' Change-Id: I5456c938c8edd36c9763e5df5be6b8fb62c595a4 --- vmware_nsx/plugins/nsx_v/plugin.py | 43 +++- .../plugins/nsx_v/vshield/common/constants.py | 8 + .../plugins/nsx_v/vshield/edge_utils.py | 35 ++++ vmware_nsx/tests/unit/nsx_v/test_plugin.py | 183 ++++++++++++++++++ 4 files changed, 267 insertions(+), 2 deletions(-) diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index e9efd0a947..7f95aa47ba 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -52,6 +52,7 @@ from neutron.db.availability_zone import router as router_az_db from neutron.db import db_base_plugin_v2 from neutron.db import dns_db from neutron.db import external_net_db +from neutron.db import extradhcpopt_db from neutron.db import extraroute_db from neutron.db import l3_attrs_db from neutron.db import l3_db @@ -67,6 +68,7 @@ from neutron.db import vlantransparent_db from neutron.extensions import allowedaddresspairs as addr_pair from neutron.extensions import availability_zone as az_ext from neutron.extensions import external_net as ext_net_extn +from neutron.extensions import extra_dhcp_opt as ext_edo from neutron.extensions import flavors from neutron.extensions import l3 from neutron.extensions import multiprovidernet as mpnet @@ -140,6 +142,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, rt_rtr.RouterType_mixin, external_net_db.External_net_db_mixin, extraroute_db.ExtraRoute_db_mixin, + extradhcpopt_db.ExtraDhcpOptMixin, router_az_db.RouterAvailabilityZoneMixin, l3_gwmode_db.L3_NAT_db_mixin, portbindings_db.PortBindingMixin, @@ -163,6 +166,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, "provider", "quotas", "external-net", + "extra_dhcp_opt", "extraroute", "router", "security-group", @@ -1630,8 +1634,37 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, port_id=port_data['id'], vnic_type=vnic_type) + def _validate_extra_dhcp_options(self, opts): + if not opts: + return + for opt in opts: + opt_name = opt['opt_name'] + opt_val = opt['opt_value'] + if opt_name == 'classless-static-route': + # separate validation for option121 + if opt_val is not None: + try: + net, ip = opt_val.split(',') + except Exception: + msg = (_("Bad value %(val)s for DHCP option " + "%(name)s") % {'name': opt_name, + 'val': opt_val}) + raise n_exc.InvalidInput(error_message=msg) + elif opt_name not in vcns_const.SUPPORTED_DHCP_OPTIONS: + try: + option = int(opt_name) + except ValueError: + option = 255 + if option >= 255: + msg = (_("DHCP option %s is not supported") % opt_name) + LOG.error(msg) + raise n_exc.InvalidInput(error_message=msg) + def create_port(self, context, port): port_data = port['port'] + dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS) + self._validate_extra_dhcp_options(dhcp_opts) + with db_api.context_manager.writer.using(context): # First we allocate port in neutron database neutron_db = super(NsxVPluginV2, self).create_port(context, port) @@ -1692,6 +1725,8 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self._process_vnic_type(context, port_data, attrs, has_security_groups, port_security) + self._process_port_create_extra_dhcp_opts( + context, port_data, dhcp_opts) # Invoking the manager callback under transaction fails so here # we do it outside. If this fails we will blow away the port @@ -1710,7 +1745,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, try: # Configure NSX - this should not be done in the DB transaction # Configure the DHCP Edge service - self._create_dhcp_static_binding(context, neutron_db) + self._create_dhcp_static_binding(context, port_data) except Exception: with excutils.save_and_reraise_exception(): LOG.exception('Failed to create port') @@ -1825,6 +1860,8 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, device_id): attrs = port[attr.PORT] port_data = port['port'] + dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS) + self._validate_extra_dhcp_options(dhcp_opts) if addr_pair.ADDRESS_PAIRS in attrs: self._validate_address_pairs(attrs, original_port) orig_has_port_security = (cfg.CONF.nsxv.spoofguard_enabled and @@ -1938,11 +1975,13 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self._process_vnic_type(context, ret_port, attrs, has_security_groups, has_port_security) + self._update_extra_dhcp_opts_on_port(context, id, port, + ret_port) if comp_owner_update: # Create dhcp bindings, the port is now owned by an instance self._create_dhcp_static_binding(context, ret_port) - elif port_ip_change: + elif port_ip_change or dhcp_opts: owner = original_port['device_owner'] # If port IP has changed we should update according to device # owner diff --git a/vmware_nsx/plugins/nsx_v/vshield/common/constants.py b/vmware_nsx/plugins/nsx_v/vshield/common/constants.py index 91ae3f7dc2..a4faf564cd 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/common/constants.py +++ b/vmware_nsx/plugins/nsx_v/vshield/common/constants.py @@ -72,6 +72,14 @@ ALLOWED_EDGE_SIZES = (nsxv_constants.COMPACT, ALLOWED_EDGE_TYPES = (nsxv_constants.SERVICE_EDGE, nsxv_constants.VDR_EDGE) +SUPPORTED_DHCP_OPTIONS = { + 'interface-mtu': 'option26', + 'tftp-server-name': 'option66', + 'bootfile-name': 'option67', + 'classless-static-route': 'option121', + 'tftp-server-address': 'option150', +} + # router status by number class RouterStatus(object): diff --git a/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py b/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py index 102aee377f..d107d8518e 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py +++ b/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py @@ -32,6 +32,7 @@ from oslo_utils import timeutils from oslo_utils import uuidutils from six import moves +from neutron.extensions import extra_dhcp_opt as ext_edo from neutron.extensions import l3 from neutron.plugins.common import constants as plugin_const @@ -926,6 +927,35 @@ class EdgeManager(object): with locking.LockManager.get_lock(str(edge_binding['edge_id'])): self.update_dhcp_service_config(context, edge_binding['edge_id']) + def _add_dhcp_option(self, static_config, opt): + if 'dhcpOptions' not in static_config: + static_config['dhcpOptions'] = {} + opt_name = opt['opt_name'] + opt_val = opt['opt_value'] + if opt_name in vcns_const.SUPPORTED_DHCP_OPTIONS: + key = vcns_const.SUPPORTED_DHCP_OPTIONS[opt_name] + if opt_name == 'classless-static-route': + if 'option121' not in static_config['dhcpOptions']: + static_config['dhcpOptions']['option121'] = { + 'staticRoutes': []} + opt121 = static_config['dhcpOptions']['option121'] + net, ip = opt_val.split(',') + opt121['staticRoutes'].append({'destinationSubnet': net, + 'router': ip}) + elif opt_name == 'tftp-server-address': + if 'option150' not in static_config['dhcpOptions']: + static_config['dhcpOptions']['option150'] = { + 'tftpServers': []} + opt150 = static_config['dhcpOptions']['option150'] + opt150['tftpServers'].append(opt_val) + else: + static_config['dhcpOptions'][key] = opt_val + else: + if 'other' not in static_config['dhcpOptions']: + static_config['dhcpOptions']['others'] = [] + static_config['dhcpOptions']['others'].append( + {'code': opt_name, 'value': opt_val}) + def create_static_binding(self, context, port): """Create the DHCP Edge static binding configuration @@ -993,6 +1023,11 @@ class EdgeManager(object): host_route['destination'], host_route['nexthop']) + dhcp_opts = port.get(ext_edo.EXTRADHCPOPTS) + if dhcp_opts is not None: + for opt in dhcp_opts: + self._add_dhcp_option(static_config, opt) + static_bindings.append(static_config) return static_bindings diff --git a/vmware_nsx/tests/unit/nsx_v/test_plugin.py b/vmware_nsx/tests/unit/nsx_v/test_plugin.py index 72e7a1946a..7cd4c016cd 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v/test_plugin.py @@ -14,6 +14,7 @@ # limitations under the License. import contextlib +import copy from eventlet import greenthread import mock import netaddr @@ -23,6 +24,7 @@ from neutron.api.v2 import attributes from neutron.extensions import allowedaddresspairs as addr_pair from neutron.extensions import dvr as dist_router from neutron.extensions import external_net +from neutron.extensions import extra_dhcp_opt as edo_ext from neutron.extensions import l3 from neutron.extensions import l3_ext_gw_mode from neutron.extensions import l3_flavors @@ -35,6 +37,7 @@ from neutron.tests.unit import _test_extension_portbindings as test_bindings import neutron.tests.unit.db.test_allowedaddresspairs_db as test_addr_pair import neutron.tests.unit.db.test_db_base_plugin_v2 as test_plugin from neutron.tests.unit.extensions import base as extension +from neutron.tests.unit.extensions import test_extra_dhcp_opt as test_dhcpopts import neutron.tests.unit.extensions.test_l3 as test_l3_plugin import neutron.tests.unit.extensions.test_l3_ext_gw_mode as test_ext_gw_mode import neutron.tests.unit.extensions.test_portsecurity as test_psec @@ -5304,3 +5307,183 @@ class TestRouterFlavorTestCase(extension.ExtensionTestCase, self._test_router_create_with_flavor_error( metainfo, n_exc.BadRequest, create_az=['az2']) + + +class DHCPOptsTestCase(test_dhcpopts.TestExtraDhcpOpt, + NsxVPluginV2TestCase): + + def setUp(self, plugin=None): + super(test_dhcpopts.ExtraDhcpOptDBTestCase, self).setUp( + plugin=PLUGIN_NAME) + + def test_create_port_with_extradhcpopts(self): + opt_list = [{'opt_name': 'bootfile-name', + 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + + params = {edo_ext.EXTRADHCPOPTS: opt_list, + 'arg_list': (edo_ext.EXTRADHCPOPTS,)} + + with self.port(**params) as port: + self._check_opts(opt_list, + port['port'][edo_ext.EXTRADHCPOPTS]) + + def test_create_port_with_extradhcpopts_ipv6_opt_version(self): + self.skipTest('No DHCP v6 Support yet') + + def test_create_port_with_extradhcpopts_ipv4_opt_version(self): + opt_list = [{'opt_name': 'bootfile-name', + 'opt_value': 'pxelinux.0', + 'ip_version': 4}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123', + 'ip_version': 4}] + + params = {edo_ext.EXTRADHCPOPTS: opt_list, + 'arg_list': (edo_ext.EXTRADHCPOPTS,)} + + with self.port(**params) as port: + self._check_opts(opt_list, + port['port'][edo_ext.EXTRADHCPOPTS]) + + def test_update_port_with_extradhcpopts_with_same(self): + opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'changeme.0'}] + expected_opts = opt_list[:] + for i in expected_opts: + if i['opt_name'] == upd_opts[0]['opt_name']: + i['opt_value'] = upd_opts[0]['opt_value'] + break + self._test_update_port_with_extradhcpopts(opt_list, upd_opts, + expected_opts) + + def test_update_port_with_additional_extradhcpopt(self): + opt_list = [{'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'changeme.0'}] + expected_opts = copy.deepcopy(opt_list) + expected_opts.append(upd_opts[0]) + self._test_update_port_with_extradhcpopts(opt_list, upd_opts, + expected_opts) + + def test_update_port_with_extradhcpopts(self): + opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'changeme.0'}] + expected_opts = copy.deepcopy(opt_list) + for i in expected_opts: + if i['opt_name'] == upd_opts[0]['opt_name']: + i['opt_value'] = upd_opts[0]['opt_value'] + break + self._test_update_port_with_extradhcpopts(opt_list, upd_opts, + expected_opts) + + def test_update_port_with_extradhcpopt_delete(self): + opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': None}] + expected_opts = [] + + expected_opts = [opt for opt in opt_list + if opt['opt_name'] != 'bootfile-name'] + self._test_update_port_with_extradhcpopts(opt_list, upd_opts, + expected_opts) + + def test_update_port_adding_extradhcpopts(self): + opt_list = [] + upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + expected_opts = copy.deepcopy(upd_opts) + self._test_update_port_with_extradhcpopts(opt_list, upd_opts, + expected_opts) + + def test_update_port_with_blank_name_extradhcpopt(self): + opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + upd_opts = [{'opt_name': ' ', 'opt_value': 'pxelinux.0'}] + + params = {edo_ext.EXTRADHCPOPTS: opt_list, + 'arg_list': (edo_ext.EXTRADHCPOPTS,)} + + with self.port(**params) as port: + update_port = {'port': {edo_ext.EXTRADHCPOPTS: upd_opts}} + + req = self.new_update_request('ports', update_port, + port['port']['id']) + res = req.get_response(self.api) + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_create_port_with_empty_router_extradhcpopts(self): + self.skipTest('No DHCP support option for router') + + def test_update_port_with_blank_router_extradhcpopt(self): + self.skipTest('No DHCP support option for router') + + def test_update_port_with_extradhcpopts_ipv6_change_value(self): + self.skipTest('No DHCP v6 Support yet') + + def test_update_port_with_extradhcpopts_add_another_ver_opt(self): + self.skipTest('No DHCP v6 Support yet') + + def test_update_port_with_blank_string_extradhcpopt(self): + opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': ' '}] + + params = {edo_ext.EXTRADHCPOPTS: opt_list, + 'arg_list': (edo_ext.EXTRADHCPOPTS,)} + + with self.port(**params) as port: + update_port = {'port': {edo_ext.EXTRADHCPOPTS: upd_opts}} + + req = self.new_update_request('ports', update_port, + port['port']['id']) + res = req.get_response(self.api) + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_create_port_with_none_extradhcpopts(self): + opt_list = [{'opt_name': 'bootfile-name', + 'opt_value': None}, + {'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + expected = [{'opt_name': 'tftp-server-address', + 'opt_value': '123.123.123.123'}] + + params = {edo_ext.EXTRADHCPOPTS: opt_list, + 'arg_list': (edo_ext.EXTRADHCPOPTS,)} + + with self.port(**params) as port: + self._check_opts(expected, + port['port'][edo_ext.EXTRADHCPOPTS]) + + def test_create_port_with_extradhcpopts_codes(self): + opt_list = [{'opt_name': '85', + 'opt_value': 'cafecafe'}] + + params = {edo_ext.EXTRADHCPOPTS: opt_list, + 'arg_list': (edo_ext.EXTRADHCPOPTS,)} + + with self.port(**params) as port: + self._check_opts(opt_list, + port['port'][edo_ext.EXTRADHCPOPTS]) + + def test_update_port_with_extradhcpopts_codes(self): + opt_list = [{'opt_name': '85', + 'opt_value': 'cafecafe'}] + upd_opts = [{'opt_name': '85', + 'opt_value': '01010101'}] + expected_opts = copy.deepcopy(opt_list) + for i in expected_opts: + if i['opt_name'] == upd_opts[0]['opt_name']: + i['opt_value'] = upd_opts[0]['opt_value'] + break + self._test_update_port_with_extradhcpopts(opt_list, upd_opts, + expected_opts)