From d4fa95168c2ef3cb6ffc8a449c2a3dc89946866b Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Sun, 24 Jul 2016 11:50:01 +0300 Subject: [PATCH] NSX|V add dhcp-mtu extension to subnet Add subnet extension dhcp-mtu and configure it in option26 of the dhcp binding. Also add this column to the nsxv_subnet_ext_attributes DB table. This option will be available only from NSX version 6.2.3 DocImpact: Added dhcp-mtu extension to subnets Change-Id: Id2a74a3c089beb61fde6b7c0fd02b207e444c3b7 --- .../alembic_migrations/versions/CONTRACT_HEAD | 2 +- ...e29d208ac6_nsxv_add_dhcp_mtu_to_subnets.py | 38 ++++ vmware_nsx/db/nsxv_db.py | 13 +- vmware_nsx/db/nsxv_models.py | 3 +- vmware_nsx/extensions/dhcp_mtu.py | 54 +++++ vmware_nsx/plugins/nsx_v/plugin.py | 50 +++-- .../plugins/nsx_v/vshield/edge_utils.py | 18 +- .../tests/unit/extensions/test_dhcp_mtu.py | 186 ++++++++++++++++++ 8 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 vmware_nsx/db/migration/alembic_migrations/versions/newton/contract/dbe29d208ac6_nsxv_add_dhcp_mtu_to_subnets.py create mode 100644 vmware_nsx/extensions/dhcp_mtu.py create mode 100644 vmware_nsx/tests/unit/extensions/test_dhcp_mtu.py diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD index 24479e836c..6f6323b410 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/CONTRACT_HEAD @@ -1 +1 @@ -081af0e396d7 +dbe29d208ac6 \ No newline at end of file diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/newton/contract/dbe29d208ac6_nsxv_add_dhcp_mtu_to_subnets.py b/vmware_nsx/db/migration/alembic_migrations/versions/newton/contract/dbe29d208ac6_nsxv_add_dhcp_mtu_to_subnets.py new file mode 100644 index 0000000000..027166f7ac --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/newton/contract/dbe29d208ac6_nsxv_add_dhcp_mtu_to_subnets.py @@ -0,0 +1,38 @@ +# Copyright 2016 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""NSXv add DHCP MTU to subnets + +Revision ID: dbe29d208ac6 +Revises: 081af0e396d7 +Create Date: 2016-07-21 05:03:35.369938 + +""" + +# revision identifiers, used by Alembic. +revision = 'dbe29d208ac6' +down_revision = '081af0e396d7' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # Add a new column and make the previous column nullable, + # because it is enough that one of them is non-null + op.add_column('nsxv_subnet_ext_attributes', + sa.Column('dhcp_mtu', sa.Integer, nullable=True)) + op.alter_column('nsxv_subnet_ext_attributes', 'dns_search_domain', + nullable=True, existing_type=sa.String(length=255), + existing_nullable=False) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index 45f85a75a8..f8e7d87e78 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -30,6 +30,7 @@ from vmware_nsx._i18n import _, _LE, _LW from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.common import nsxv_constants from vmware_nsx.db import nsxv_models +from vmware_nsx.extensions import dhcp_mtu as ext_dhcp_mtu from vmware_nsx.extensions import dns_search_domain as ext_dns_search_domain from vmware_nsx.plugins.nsx_v.vshield.common import constants @@ -773,11 +774,14 @@ def del_nsxv_lbaas_certificate_binding(session, cert_id, edge_id): edge_id=edge_id).delete()) -def add_nsxv_subnet_ext_attributes(session, subnet_id, dns_search_domain): +def add_nsxv_subnet_ext_attributes(session, subnet_id, + dns_search_domain=None, + dhcp_mtu=None): with session.begin(subtransactions=True): binding = nsxv_models.NsxvSubnetExtAttributes( subnet_id=subnet_id, - dns_search_domain=dns_search_domain) + dns_search_domain=dns_search_domain, + dhcp_mtu=dhcp_mtu) session.add(binding) return binding @@ -791,9 +795,12 @@ def get_nsxv_subnet_ext_attributes(session, subnet_id): return -def update_nsxv_subnet_ext_attributes(session, subnet_id, dns_search_domain): +def update_nsxv_subnet_ext_attributes(session, subnet_id, + dns_search_domain=None, + dhcp_mtu=None): with session.begin(subtransactions=True): binding = (session.query(nsxv_models.NsxvSubnetExtAttributes). filter_by(subnet_id=subnet_id).one()) binding[ext_dns_search_domain.DNS_SEARCH_DOMAIN] = dns_search_domain + binding[ext_dhcp_mtu.DHCP_MTU] = dhcp_mtu return binding diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index 4e177cba0f..c60c5ef844 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -342,7 +342,8 @@ class NsxvSubnetExtAttributes(model_base.BASEV2, models.TimestampMixin): subnet_id = sa.Column(sa.String(36), sa.ForeignKey('subnets.id', ondelete="CASCADE"), primary_key=True) - dns_search_domain = sa.Column(sa.String(255), nullable=False) + dns_search_domain = sa.Column(sa.String(255), nullable=True) + dhcp_mtu = sa.Column(sa.Integer, nullable=True) # Add a relationship to the Subnet model in order to instruct # SQLAlchemy to eagerly load this association subnet = orm.relationship( diff --git a/vmware_nsx/extensions/dhcp_mtu.py b/vmware_nsx/extensions/dhcp_mtu.py new file mode 100644 index 0000000000..14971a2747 --- /dev/null +++ b/vmware_nsx/extensions/dhcp_mtu.py @@ -0,0 +1,54 @@ +# Copyright 2016 VMware, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.api import extensions + +from neutron_lib import constants + +DHCP_MTU = 'dhcp_mtu' +EXTENDED_ATTRIBUTES_2_0 = { + 'subnets': { + DHCP_MTU: { + 'allow_post': True, 'allow_put': True, + 'default': constants.ATTR_NOT_SPECIFIED, + # This is the legal range for the backend MTU + 'validate': {'type:range': (68, 65535)}, + 'is_visible': True}, + } +} + + +class Dhcp_mtu(extensions.ExtensionDescriptor): + """Extension class supporting DHCP MTU for subnets.""" + + @classmethod + def get_name(cls): + return "DHCP MTU" + + @classmethod + def get_alias(cls): + return "dhcp-mtu" + + @classmethod + def get_description(cls): + return "Enable the ability to add DHCP MTU for Subnets" + + @classmethod + def get_updated(cls): + return "2016-7-21T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + return {} diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index e3b5d7bd11..7fce8de95a 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -89,6 +89,7 @@ from vmware_nsx.extensions import ( advancedserviceproviders as as_providers) from vmware_nsx.extensions import ( vnicindex as ext_vnic_idx) +from vmware_nsx.extensions import dhcp_mtu as ext_dhcp_mtu from vmware_nsx.extensions import dns_search_domain as ext_dns_search_domain from vmware_nsx.extensions import routersize from vmware_nsx.extensions import secgroup_rule_local_ip_prefix @@ -213,6 +214,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, else: self._dvs = None + if self.edge_manager.is_dhcp_opt_enabled: + # Only expose the extension if it is supported + self.supported_extension_aliases.append("dhcp-mtu") + # Bind QoS notifications callbacks_registry.subscribe(self._handle_qos_notification, callbacks_resources.QOS_POLICY) @@ -1847,38 +1852,50 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, def _process_subnet_ext_attr_create(self, session, subnet_db, subnet_req): - # Verify if dns search domain for subnet is configured + # Verify if dns search domain/dhcp mtu for subnet are configured dns_search_domain = subnet_req.get( ext_dns_search_domain.DNS_SEARCH_DOMAIN) - if not validators.is_attr_set(dns_search_domain): + dhcp_mtu = subnet_req.get( + ext_dhcp_mtu.DHCP_MTU) + if (not validators.is_attr_set(dns_search_domain) and + not validators.is_attr_set(dhcp_mtu)): return + if not validators.is_attr_set(dns_search_domain): + dns_search_domain = None + if not validators.is_attr_set(dhcp_mtu): + dhcp_mtu = None sub_binding = nsxv_db.get_nsxv_subnet_ext_attributes( session=session, subnet_id=subnet_db['id']) - # Create a DNS search domain entry for subnet if it does not exist + # Create a subnet extensions for subnet if it does not exist if not sub_binding: nsxv_db.add_nsxv_subnet_ext_attributes( session=session, subnet_id=subnet_db['id'], - dns_search_domain=dns_search_domain) - # Else update only if a new value for dns search domain is provided - elif sub_binding.dns_search_domain != dns_search_domain: + dns_search_domain=dns_search_domain, + dhcp_mtu=dhcp_mtu) + # Else update only if a new values for subnet extensions are provided + elif (sub_binding.dns_search_domain != dns_search_domain or + sub_binding.dhcp_mtu != dhcp_mtu): nsxv_db.update_nsxv_subnet_ext_attributes( session=session, subnet_id=subnet_db['id'], - dns_search_domain=dns_search_domain) + dns_search_domain=dns_search_domain, + dhcp_mtu=dhcp_mtu) subnet_db['dns_search_domain'] = dns_search_domain + subnet_db['dhcp_mtu'] = dhcp_mtu def _process_subnet_ext_attr_update(self, session, subnet_db, subnet_req): - update_dns_search_domain = False - # Update dns search domain attribute for subnet - if ext_dns_search_domain.DNS_SEARCH_DOMAIN in subnet_req: + update_dhcp_config = False + # Update extended attributes for subnet + if (ext_dns_search_domain.DNS_SEARCH_DOMAIN in subnet_req or + ext_dhcp_mtu.DHCP_MTU in subnet_req): self._process_subnet_ext_attr_create(session, subnet_db, subnet_req) - update_dns_search_domain = True - return update_dns_search_domain + update_dhcp_config = True + return update_dhcp_config def update_subnet(self, context, id, subnet): s = subnet['subnet'] @@ -1890,9 +1907,9 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, orig_enable_dhcp=enable_dhcp, orig_host_routes=orig_host_routes) subnet = super(NsxVPluginV2, self).update_subnet(context, id, subnet) - update_dns_search_domain = self._process_subnet_ext_attr_update( + update_dhcp_config = self._process_subnet_ext_attr_update( context.session, subnet, s) - if (gateway_ip != subnet['gateway_ip'] or update_dns_search_domain or + if (gateway_ip != subnet['gateway_ip'] or update_dhcp_config or set(orig['dns_nameservers']) != set(subnet['dns_nameservers']) or orig_host_routes != subnet['host_routes'] or enable_dhcp and not subnet['enable_dhcp']): @@ -1908,12 +1925,13 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, return subnet db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( - attr.SUBNETS, ['_extend_subnet_dict_dns_search_domain']) + attr.SUBNETS, ['_extend_subnet_dict_extended_attributes']) - def _extend_subnet_dict_dns_search_domain(self, subnet_res, subnet_db): + def _extend_subnet_dict_extended_attributes(self, subnet_res, subnet_db): subnet_attr = subnet_db.get('nsxv_subnet_attributes') if subnet_attr: subnet_res['dns_search_domain'] = subnet_attr.dns_search_domain + subnet_res['dhcp_mtu'] = subnet_attr.dhcp_mtu def _update_subnet_dhcp_status(self, subnet, context): network_id = subnet['network_id'] diff --git a/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py b/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py index ca0df15558..8a689cc639 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py +++ b/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py @@ -847,7 +847,12 @@ class EdgeManager(object): context.session, subnet_id) if sub_binding: - static_config['domainName'] = sub_binding.dns_search_domain + if sub_binding.dns_search_domain is not None: + static_config['domainName'] = sub_binding.dns_search_domain + if sub_binding.dhcp_mtu: + static_config = self.add_mtu_on_static_binding( + static_config, sub_binding.dhcp_mtu) + self.handle_meta_static_route( context, subnet_id, [static_config]) for host_route in subnet['routes']: @@ -879,6 +884,17 @@ class EdgeManager(object): 'router': nexthop}) return static_bindings + def add_mtu_on_static_binding(self, static_binding, mtu): + """Add the pre-configured MTU to a static binding config. + + We can add the MTU via dhcp option26. + This func can only works at NSXv version 6.2.3 or higher. + """ + if 'dhcpOptions' not in six.iterkeys(static_binding): + static_binding['dhcpOptions'] = {} + static_binding['dhcpOptions']['option26'] = mtu + return static_binding + def handle_meta_static_route(self, context, subnet_id, static_bindings): is_dhcp_option121 = ( self.is_dhcp_opt_enabled and diff --git a/vmware_nsx/tests/unit/extensions/test_dhcp_mtu.py b/vmware_nsx/tests/unit/extensions/test_dhcp_mtu.py new file mode 100644 index 0000000000..07ed31adf9 --- /dev/null +++ b/vmware_nsx/tests/unit/extensions/test_dhcp_mtu.py @@ -0,0 +1,186 @@ +# Copyright 2016 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +import neutron.db.api as db +from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db + +from vmware_nsx.db import nsxv_db +from vmware_nsx.extensions import dhcp_mtu as ext_dhcp_mtu +from vmware_nsx.plugins.nsx_v.vshield import edge_utils +from vmware_nsx.tests.unit.nsx_v import test_plugin +from vmware_nsx.tests.unit.nsx_v.vshield import fake_vcns + +PLUGIN_NAME = 'vmware_nsx.plugin.NsxVPlugin' + + +class DhcpMtuExtensionManager(object): + + def get_resources(self): + return [] + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + def get_extended_resources(self, version): + return ext_dhcp_mtu.get_extended_resources(version) + + +class DhcpMtuExtensionTestCase(test_plugin.NsxVPluginV2TestCase): + """Test API extension dhcp-mtu attribute of subnets.""" + + @mock.patch.object(edge_utils.EdgeManager, '_deploy_edge') + def setUp(self, plugin=PLUGIN_NAME): + ext_mgr = DhcpMtuExtensionManager() + # This feature is enabled only since 6.2.3 + with mock.patch.object(fake_vcns.FakeVcns, + 'get_version', + return_value="6.2.3"): + super(DhcpMtuExtensionTestCase, self).setUp(ext_mgr=ext_mgr) + + def _create_subnet_with_dhcp_mtu(self, dhcp_mtu): + with self.network() as net: + tenant_id = net['network']['tenant_id'] + net_id = net['network']['id'] + data = {'subnet': {'network_id': net_id, + 'cidr': '10.0.0.0/24', + 'ip_version': 4, + 'name': 'test-mtu-subnet', + 'tenant_id': tenant_id, + 'dhcp_mtu': dhcp_mtu}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + return res + + def test_subnet_create_with_dhcp_mtu(self): + for mtu in (68, 2000, 65535): + res = self._create_subnet_with_dhcp_mtu(mtu) + sub = self.deserialize(self.fmt, res) + self.assertEqual(mtu, sub['subnet']['dhcp_mtu']) + + def test_subnet_create_with_invalid_dhcp_mtu_fail(self): + res = self._create_subnet_with_dhcp_mtu(67) + self.assertEqual(400, res.status_int) + + res = self._create_subnet_with_dhcp_mtu(100000) + self.assertEqual(400, res.status_int) + + def test_subnet_update_with_dhcp_mtu(self): + res = self._create_subnet_with_dhcp_mtu(2000) + sub = self.deserialize(self.fmt, res) + data = {'subnet': {'dhcp_mtu': 3000}} + req = self.new_update_request('subnets', data, sub['subnet']['id']) + updated_sub = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(3000, updated_sub['subnet']['dhcp_mtu']) + + def _create_subnet_with_dhcp_mtu_and_dns(self, dhcp_mtu, + dns_search_domain): + with self.network() as net: + tenant_id = net['network']['tenant_id'] + net_id = net['network']['id'] + data = {'subnet': {'network_id': net_id, + 'cidr': '10.0.0.0/24', + 'ip_version': 4, + 'name': 'test-mtu-subnet', + 'tenant_id': tenant_id, + 'dhcp_mtu': dhcp_mtu, + 'dns_search_domain': dns_search_domain}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + return res + + def test_subnet_create_with_dhcp_mtu_and_dns(self): + res = self._create_subnet_with_dhcp_mtu_and_dns(2000, 'vmware.com') + sub = self.deserialize(self.fmt, res) + self.assertEqual(2000, sub['subnet']['dhcp_mtu']) + self.assertEqual('vmware.com', sub['subnet']['dns_search_domain']) + + def test_subnet_update_with_dhcp_mtu_and_dns(self): + res = self._create_subnet_with_dhcp_mtu_and_dns(2000, 'vmware.com') + sub = self.deserialize(self.fmt, res) + data = {'subnet': {'dhcp_mtu': 3000, + 'dns_search_domain': 'eng.vmware.com'}} + req = self.new_update_request('subnets', data, sub['subnet']['id']) + updated_sub = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(3000, updated_sub['subnet']['dhcp_mtu']) + self.assertEqual('eng.vmware.com', + updated_sub['subnet']['dns_search_domain']) + + +class DhcpMtuDBTestCase(test_db.NeutronDbPluginV2TestCase): + + def setUp(self): + super(DhcpMtuDBTestCase, self).setUp() + self.session = db.get_session() + + def test_get_nsxv_subnet_ext_attributes_no_dhcp_mtu(self): + with self.subnet() as sub: + sub_binding = nsxv_db.get_nsxv_subnet_ext_attributes( + session=self.session, subnet_id=sub['subnet']['id']) + self.assertIsNone(sub_binding) + + def test_add_nsxv_subnet_ext_attributes_dhcp_mtu(self): + with self.subnet() as sub: + nsxv_db.add_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dhcp_mtu=2000) + sub_binding = nsxv_db.get_nsxv_subnet_ext_attributes( + session=self.session, subnet_id=sub['subnet']['id']) + self.assertEqual(2000, sub_binding.dhcp_mtu) + self.assertEqual(sub['subnet']['id'], sub_binding.subnet_id) + + def test_update_nsxv_subnet_ext_attributes_dhcp_mtu(self): + with self.subnet() as sub: + nsxv_db.add_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dhcp_mtu=2000) + sub_binding = nsxv_db.update_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dhcp_mtu=3000) + self.assertEqual(3000, sub_binding.dhcp_mtu) + + def test_add_nsxv_subnet_ext_attributes_dhcp_mtu_and_dns(self): + with self.subnet() as sub: + nsxv_db.add_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dhcp_mtu=2000, + dns_search_domain='eng.vmware.com') + sub_binding = nsxv_db.get_nsxv_subnet_ext_attributes( + session=self.session, subnet_id=sub['subnet']['id']) + self.assertEqual(2000, sub_binding.dhcp_mtu) + self.assertEqual('eng.vmware.com', sub_binding.dns_search_domain) + self.assertEqual(sub['subnet']['id'], sub_binding.subnet_id) + + def test_update_nsxv_subnet_ext_attributes_dhcp_mtu_and_dns(self): + with self.subnet() as sub: + nsxv_db.add_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dhcp_mtu=2000, + dns_search_domain='eng.vmware.com') + sub_binding = nsxv_db.update_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dhcp_mtu=3000, + dns_search_domain='nsx.vmware.com') + self.assertEqual(3000, sub_binding.dhcp_mtu) + self.assertEqual('nsx.vmware.com', sub_binding.dns_search_domain)