diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index 74adfb566d..222149bbd5 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -2af850eb3970 +69fb78b33d41 diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/69fb78b33d41_nsxv_add_search_domain_to_subnets.py b/vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/69fb78b33d41_nsxv_add_search_domain_to_subnets.py new file mode 100644 index 0000000000..9dea6defd1 --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/69fb78b33d41_nsxv_add_search_domain_to_subnets.py @@ -0,0 +1,40 @@ +# 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 dns search domain to subnets + +Revision ID: 69fb78b33d41 +Revises: 2af850eb3970 +Create Date: 2016-01-27 07:28:35.369938 + +""" + +# revision identifiers, used by Alembic. +revision = '69fb78b33d41' +down_revision = '2af850eb3970' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'nsxv_subnet_ext_attributes', + sa.Column('subnet_id', sa.String(length=36), nullable=False), + sa.Column('dns_search_domain', sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint(['subnet_id'], + ['subnets.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('subnet_id') + ) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index f16473061c..6cf3769242 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -27,6 +27,7 @@ from vmware_nsx._i18n import _, _LE 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 dns_search_domain as ext_dns_search_domain from vmware_nsx.plugins.nsx_v.vshield.common import constants NsxvEdgeDhcpStaticBinding = nsxv_models.NsxvEdgeDhcpStaticBinding @@ -715,3 +716,29 @@ def del_nsxv_lbaas_certificate_binding(session, cert_id, edge_id): return (session.query(nsxv_models.NsxvLbaasCertificateBinding). filter_by(cert_id=cert_id, edge_id=edge_id).delete()) + + +def add_nsxv_subnet_ext_attributes(session, subnet_id, dns_search_domain): + with session.begin(subtransactions=True): + binding = nsxv_models.NsxvSubnetExtAttributes( + subnet_id=subnet_id, + dns_search_domain=dns_search_domain) + session.add(binding) + return binding + + +def get_nsxv_subnet_ext_attributes(session, subnet_id): + try: + return session.query( + nsxv_models.NsxvSubnetExtAttributes).filter_by( + subnet_id=subnet_id).one() + except exc.NoResultFound: + return + + +def update_nsxv_subnet_ext_attributes(session, subnet_id, dns_search_domain): + 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 + return binding diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index 07e83ffced..7a817ee7bf 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -310,3 +310,20 @@ class NsxvLbaasCertificateBinding(model_base.BASEV2): cert_id = sa.Column(sa.String(36), primary_key=True) edge_id = sa.Column(sa.String(36), primary_key=True) edge_cert_id = sa.Column(sa.String(36), nullable=False) + + +class NsxvSubnetExtAttributes(model_base.BASEV2): + """Subnet attributes managed by NSX plugin extensions.""" + + __tablename__ = 'nsxv_subnet_ext_attributes' + + 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) + # Add a relationship to the Subnet model in order to instruct + # SQLAlchemy to eagerly load this association + subnet = orm.relationship( + models_v2.Subnet, + backref=orm.backref("nsxv_subnet_attributes", lazy='joined', + uselist=False, cascade='delete')) diff --git a/vmware_nsx/extensions/dns_search_domain.py b/vmware_nsx/extensions/dns_search_domain.py new file mode 100644 index 0000000000..afaf10398c --- /dev/null +++ b/vmware_nsx/extensions/dns_search_domain.py @@ -0,0 +1,52 @@ +# 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.api.v2 import attributes + + +DNS_SEARCH_DOMAIN = 'dns_search_domain' +EXTENDED_ATTRIBUTES_2_0 = { + 'subnets': { + DNS_SEARCH_DOMAIN: {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:string': None}, + 'is_visible': True}, + } +} + + +class Dns_search_domain(extensions.ExtensionDescriptor): + """Extension class supporting dns search domains for subnets.""" + + @classmethod + def get_name(cls): + return "DNS search Domains" + + @classmethod + def get_alias(cls): + return "dns-search-domain" + + @classmethod + def get_description(cls): + return "Enable the ability to add DNS search domain name for Subnets" + + @classmethod + def get_updated(cls): + return "2016-1-22T10: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 cc885f29de..98279e3679 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -69,6 +69,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 dns_search_domain as ext_dns_search_domain from vmware_nsx.extensions import routersize from vmware_nsx.plugins.nsx_v import managers from vmware_nsx.plugins.nsx_v import md_proxy as nsx_v_md_proxy @@ -102,6 +103,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, supported_extension_aliases = ["agent", "allowed-address-pairs", "binding", + "dns-search-domain", "dvr", "ext-gw-mode", "multi-provider", @@ -1136,12 +1138,51 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, s = super(NsxVPluginV2, self).create_subnet(context, subnet) if s['enable_dhcp']: try: + self._process_subnet_ext_attr_create( + session=context.session, + subnet_db=s, + subnet_req=data) self._update_dhcp_service_with_subnet(context, s) except Exception: with excutils.save_and_reraise_exception(): self.delete_subnet(context, s['id']) return s + def _process_subnet_ext_attr_create(self, session, subnet_db, + subnet_req): + # Verify if dns search domain for subnet is configured + dns_search_domain = subnet_req.get( + ext_dns_search_domain.DNS_SEARCH_DOMAIN) + if not attr.is_attr_set(dns_search_domain): + return + 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 + 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: + nsxv_db.update_nsxv_subnet_ext_attributes( + session=session, + subnet_id=subnet_db['id'], + dns_search_domain=dns_search_domain) + subnet_db['dns_search_domain'] = dns_search_domain + + 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: + self._process_subnet_ext_attr_create(session, + subnet_db, + subnet_req) + update_dns_search_domain = True + return update_dns_search_domain + def update_subnet(self, context, id, subnet): s = subnet['subnet'] if "host_routes" in s and s["host_routes"]: @@ -1151,7 +1192,9 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, gateway_ip = orig['gateway_ip'] enable_dhcp = orig['enable_dhcp'] subnet = super(NsxVPluginV2, self).update_subnet(context, id, subnet) - if (gateway_ip != subnet['gateway_ip'] or + update_dns_search_domain = self._process_subnet_ext_attr_update( + context.session, subnet, s) + if (gateway_ip != subnet['gateway_ip'] or update_dns_search_domain or set(orig['dns_nameservers']) != set(subnet['dns_nameservers'])): # Need to ensure that all of the subnet attributes will be reloaded # when creating the edge bindings. Without adding this the original @@ -1164,6 +1207,14 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self._update_subnet_dhcp_status(subnet, context) return subnet + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + attr.SUBNETS, ['_extend_subnet_dict_dns_search_domain']) + + def _extend_subnet_dict_dns_search_domain(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 + def _update_subnet_dhcp_status(self, subnet, context): network_id = subnet['network_id'] if subnet['enable_dhcp']: diff --git a/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py b/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py index 97e40bb6df..9b40b3c846 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py +++ b/vmware_nsx/plugins/nsx_v/vshield/edge_utils.py @@ -679,6 +679,7 @@ class EdgeManager(object): + """ static_bindings = [] @@ -709,6 +710,12 @@ class EdgeManager(object): elif len(name_servers) >= 2: static_config['primaryNameServer'] = name_servers[0] static_config['secondaryNameServer'] = name_servers[1] + # Set search domain for static binding + sub_binding = nsxv_db.get_nsxv_subnet_ext_attributes( + context.session, + subnet_id) + if sub_binding: + static_config['domainName'] = sub_binding.dns_search_domain static_bindings.append(static_config) return static_bindings diff --git a/vmware_nsx/tests/unit/extensions/test_dns_search_domain.py b/vmware_nsx/tests/unit/extensions/test_dns_search_domain.py new file mode 100644 index 0000000000..f26f784030 --- /dev/null +++ b/vmware_nsx/tests/unit/extensions/test_dns_search_domain.py @@ -0,0 +1,111 @@ +# 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 dns_search_domain as ext_dns_search_domain +from vmware_nsx.plugins.nsx_v.vshield import edge_utils +from vmware_nsx.tests.unit.nsx_v import test_plugin + +PLUGIN_NAME = 'vmware_nsx.plugin.NsxVPlugin' + + +class DnsSearchDomainExtensionManager(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_dns_search_domain.get_extended_resources(version) + + +class DnsSearchDomainExtensionTestCase(test_plugin.NsxVPluginV2TestCase): + """Test API extension dns-search-domain attribute.""" + + @mock.patch.object(edge_utils.EdgeManager, '_deploy_edge') + def setUp(self, plugin=PLUGIN_NAME): + ext_mgr = DnsSearchDomainExtensionManager() + super(DnsSearchDomainExtensionTestCase, self).setUp(ext_mgr=ext_mgr) + + def _create_subnet_with_dns_search_domain(self, 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-dns-search-domain-subnet', + 'tenant_id': tenant_id, + 'dns_search_domain': dns_search_domain}} + subnet_req = self.new_create_request('subnets', data) + res = subnet_req.get_response(self.api) + return self.deserialize(self.fmt, res) + + def test_subnet_create_with_dns_search_domain(self): + sub = self._create_subnet_with_dns_search_domain('vmware.com') + self.assertEqual('vmware.com', sub['subnet']['dns_search_domain']) + + def test_subnet_update_with_dns_search_domain(self): + sub = self._create_subnet_with_dns_search_domain('vmware.com') + data = {'subnet': {'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('eng.vmware.com', + updated_sub['subnet']['dns_search_domain']) + + +class DnsSearchDomainDBTestCase(test_db.NeutronDbPluginV2TestCase): + + def setUp(self): + super(DnsSearchDomainDBTestCase, self).setUp() + self.session = db.get_session() + + def test_get_nsxv_subnet_ext_attributes_no_dns_search_domain(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_dns_search_domain(self): + with self.subnet() as sub: + nsxv_db.add_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + 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('eng.vmware.com', sub_binding.dns_search_domain) + self.assertEqual(sub['subnet']['id'], sub_binding.subnet_id) + + def test_update_nsxv_subnet_ext_attributes_dns_search_domain(self): + with self.subnet() as sub: + nsxv_db.add_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dns_search_domain='eng.vmware.com') + sub_binding = nsxv_db.update_nsxv_subnet_ext_attributes( + session=self.session, + subnet_id=sub['subnet']['id'], + dns_search_domain='nsx.vmware.com') + self.assertEqual('nsx.vmware.com', sub_binding.dns_search_domain)