From 84be0ea6a5e9ad58ca1eda855f0af9da9527e86c Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Mon, 6 Mar 2017 10:36:52 +0200 Subject: [PATCH] NSX-V3| network availability zones support Adding availability zones for nsx-v3 for native dhcp parameters configuration: [nsx_v3] availability_zones = zone1,zone2,zone3 [az:zone1] metadata_proxy = a87d92f3-0106-47dc-a494-de68345fecc8 dhcp_profile = 8a4fb2ca-60aa-4291-aab8-d0d6b7790292 profile-name-or-uuid native_metadata_route = 179.254.169.254/31 dns_domain = aaa.com nameservers = 1.1.1.1, 2.2.2.2 Change-Id: I006d922908d5a061480f43eeb92d373fcb4db616 --- ...nsxv3-availability-zones-8decf892df62.yaml | 9 + vmware_nsx/common/availability_zones.py | 6 +- vmware_nsx/common/config.py | 63 ++++++- .../extension_drivers/dns_integration.py | 43 +++-- .../plugins/nsx_v3/availability_zones.py | 92 +++++++++++ vmware_nsx/plugins/nsx_v3/plugin.py | 153 ++++++++++++----- vmware_nsx/plugins/nsx_v3/utils.py | 3 +- .../plugins/nsxv3/resources/dhcp_binding.py | 3 + .../unit/nsx_v3/test_availability_zones.py | 112 +++++++++++++ .../tests/unit/nsx_v3/test_dhcp_metadata.py | 156 +++++++++++++++++- vmware_nsx/tests/unit/nsx_v3/test_plugin.py | 5 +- 11 files changed, 569 insertions(+), 76 deletions(-) create mode 100644 releasenotes/notes/nsxv3-availability-zones-8decf892df62.yaml create mode 100644 vmware_nsx/plugins/nsx_v3/availability_zones.py create mode 100644 vmware_nsx/tests/unit/nsx_v3/test_availability_zones.py diff --git a/releasenotes/notes/nsxv3-availability-zones-8decf892df62.yaml b/releasenotes/notes/nsxv3-availability-zones-8decf892df62.yaml new file mode 100644 index 0000000000..b7744c6d13 --- /dev/null +++ b/releasenotes/notes/nsxv3-availability-zones-8decf892df62.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + The NSX-v3 plugin supports availability zones hints on networks + creation in order to separate the native dhcp configuration. +features: + - The NSX-v3 plugin supports availability zones hints on networks + creation in order to separate the native dhcp configuration. + The availability zones configuration includes the metadata_proxy, + dhcp_profile, native_metadata_route and dns related parameters. diff --git a/vmware_nsx/common/availability_zones.py b/vmware_nsx/common/availability_zones.py index 017721e500..d5afe2f740 100644 --- a/vmware_nsx/common/availability_zones.py +++ b/vmware_nsx/common/availability_zones.py @@ -153,11 +153,7 @@ class NSXAvailabilityZonesPluginCommon(object): def get_obj_az_by_hints(self, obj): if az_ext.AZ_HINTS in obj: - hints = obj[az_ext.AZ_HINTS] - # if this is a string and not a list - need to convert it - if not isinstance(hints, list): - hints = az_ext.convert_az_string_to_list(hints) - for hint in hints: + for hint in obj[az_ext.AZ_HINTS]: # For now we use only the first hint return self.get_az_by_hint(hint) diff --git a/vmware_nsx/common/config.py b/vmware_nsx/common/config.py index e4dae9ce8e..ad0d78cad5 100644 --- a/vmware_nsx/common/config.py +++ b/vmware_nsx/common/config.py @@ -416,6 +416,12 @@ nsx_v3_opts = [ default=False, help=_("(Optional) Indicates whether distributed-firewall " "security-groups rules are logged.")), + cfg.ListOpt('availability_zones', + default=[], + help=_('Optional parameter defining the networks availability ' + 'zones names for the native dhcp configuration. The ' + 'configuration of each zone will be under a group ' + 'names [az:]')), ] DEFAULT_STATUS_CHECK_INTERVAL = 2000 @@ -668,8 +674,8 @@ nsxv_opts = [ help=_("(Optional) Have exclusive DHCP edge per network.")), ] -# define the configuration of each availability zone. -# the list of expected zones in under nsxv group: availability_zones +# define the configuration of each NSX-V availability zone. +# the list of expected zones is under nsxv group: availability_zones # Note: if any of the optional arguments is missing - the global one will be # used instead. nsxv_az_opts = [ @@ -726,6 +732,32 @@ nsxv_az_opts = [ help=_("(Optional) Have exclusive DHCP edge per network.")), ] +# define the configuration of each NSX-V3 availability zone. +# the list of expected zones is under nsx_v3 group: availability_zones +# Note: if any of the optional arguments is missing - the global one will be +# used instead. +nsxv3_az_opts = [ + cfg.StrOpt('metadata_proxy', + help=_("The name or UUID of the NSX Metadata Proxy " + "that will be used to enable native metadata service. " + "It needs to be created in NSX before starting Neutron " + "with the NSX plugin.")), + cfg.StrOpt('dhcp_profile', + help=_("The name or UUID of the NSX DHCP Profile " + "that will be used to enable native DHCP service. It " + "needs to be created in NSX before starting Neutron " + "with the NSX plugin")), + cfg.StrOpt('native_metadata_route', + help=_("(Optional) The metadata route used for native metadata " + "proxy service.")), + cfg.StrOpt('dns_domain', + help=_("(Optional) Domain to use for building the hostnames.")), + cfg.ListOpt('nameservers', + help=_("(Optional) List of nameservers to configure for the " + "DHCP binding entries. These will be used if there are " + "no nameservers defined on the subnet.")), +] + # Register the configuration options cfg.CONF.register_opts(connection_opts) cfg.CONF.register_opts(cluster_opts) @@ -740,8 +772,7 @@ cfg.CONF.register_opts(sync_opts, group="NSX_SYNC") cfg.CONF.register_opts(l3_hamode_db.L3_HA_OPTS) -# register a group for each nsxv availability zones -def register_nsxv_azs(conf, availability_zones): +def _register_nsx_azs(conf, availability_zones, az_opts): # first verify that the availability zones are in the format of a # list of names. The old format was a list of values for each az, # separated with ':' @@ -753,13 +784,23 @@ def register_nsxv_azs(conf, availability_zones): conf.register_group(cfg.OptGroup( name=az_group, title="Configuration for availability zone %s" % az)) - conf.register_opts(nsxv_az_opts, group=az_group) + conf.register_opts(az_opts, group=az_group) + + +# register a group for each nsxv/v3 availability zones +def register_nsxv_azs(conf, availability_zones): + _register_nsx_azs(conf, availability_zones, nsxv_az_opts) + + +def register_nsxv3_azs(conf, availability_zones): + _register_nsx_azs(conf, availability_zones, nsxv3_az_opts) register_nsxv_azs(cfg.CONF, cfg.CONF.nsxv.availability_zones) +register_nsxv3_azs(cfg.CONF, cfg.CONF.nsx_v3.availability_zones) -def get_nsxv_az_opts(az): +def _get_nsx_az_opts(az, opts): az_info = dict() group = 'az:%s' % az if group not in cfg.CONF: @@ -767,11 +808,19 @@ def get_nsxv_az_opts(az): opt_name=group, opt_value='None', reason=(_("Configuration group \'%s\' must be defined") % group)) - for opt in nsxv_az_opts: + for opt in opts: az_info[opt.name] = cfg.CONF[group][opt.name] return az_info +def get_nsxv_az_opts(az): + return _get_nsx_az_opts(az, nsxv_az_opts) + + +def get_nsxv3_az_opts(az): + return _get_nsx_az_opts(az, nsxv3_az_opts) + + def validate_nsxv_config_options(): if (cfg.CONF.nsxv.manager_uri is None or cfg.CONF.nsxv.user is None or diff --git a/vmware_nsx/extension_drivers/dns_integration.py b/vmware_nsx/extension_drivers/dns_integration.py index 6a2313a1e8..3748c4b3c2 100644 --- a/vmware_nsx/extension_drivers/dns_integration.py +++ b/vmware_nsx/extension_drivers/dns_integration.py @@ -15,10 +15,12 @@ # under the License. from neutron_lib.api import validators +from neutron_lib import context as n_context from neutron_lib.plugins import directory from oslo_config import cfg from oslo_log import log as logging +from neutron.extensions import availability_zone as az_ext from neutron.extensions import dns from neutron.objects import network as net_obj from neutron.objects import ports as port_obj @@ -26,11 +28,13 @@ from neutron.services.externaldns import driver from vmware_nsx._i18n import _LE, _LI from vmware_nsx.common import driver_api +from vmware_nsx.plugins.nsx_v3 import availability_zones as nsx_az LOG = logging.getLogger(__name__) DNS_DOMAIN_DEFAULT = 'openstacklocal.' +# TODO(asarfaty) use dns-domain/nameserver from network az instead of global class DNSExtensionDriver(driver_api.ExtensionDriver): _supported_extension_alias = 'dns-integration' @@ -80,7 +84,7 @@ class DNSExtensionDriver(driver_api.ExtensionDriver): if not request_data.get(dns.DNSNAME): return dns_name, is_dns_domain_default = self._get_request_dns_name( - request_data) + request_data, db_data['network_id']) if is_dns_domain_default: return network = self._get_network(plugin_context, db_data['network_id']) @@ -143,7 +147,7 @@ class DNSExtensionDriver(driver_api.ExtensionDriver): return if dns_name is not None: dns_name, is_dns_domain_default = self._get_request_dns_name( - request_data) + request_data, db_data['network_id']) if is_dns_domain_default: self._extend_port_dict(plugin_context.session, db_data, db_data, None) @@ -198,31 +202,31 @@ class DNSExtensionDriver(driver_api.ExtensionDriver): response_data[dns.DNSDOMAIN] = db_data.dns_domain[dns.DNSDOMAIN] return response_data - def _get_dns_domain(self): + def _get_dns_domain(self, network_id): if not cfg.CONF.dns_domain: return '' if cfg.CONF.dns_domain.endswith('.'): return cfg.CONF.dns_domain return '%s.' % cfg.CONF.dns_domain - def _get_request_dns_name(self, port): - dns_domain = self._get_dns_domain() + def _get_request_dns_name(self, port, network_id): + dns_domain = self._get_dns_domain(network_id) if ((dns_domain and dns_domain != DNS_DOMAIN_DEFAULT)): return (port.get(dns.DNSNAME, ''), False) return ('', True) - def _get_request_dns_name_and_domain_name(self, dns_data_db): - dns_domain = self._get_dns_domain() + def _get_request_dns_name_and_domain_name(self, dns_data_db, network_id): + dns_domain = self._get_dns_domain(network_id) dns_name = '' if ((dns_domain and dns_domain != DNS_DOMAIN_DEFAULT)): if dns_data_db: dns_name = dns_data_db.dns_name return dns_name, dns_domain - def _get_dns_names_for_port(self, ips, dns_data_db): + def _get_dns_names_for_port(self, ips, dns_data_db, network_id): dns_assignment = [] dns_name, dns_domain = self._get_request_dns_name_and_domain_name( - dns_data_db) + dns_data_db, network_id) for ip in ips: if dns_name: hostname = dns_name @@ -242,7 +246,8 @@ class DNSExtensionDriver(driver_api.ExtensionDriver): def _get_dns_name_for_port_get(self, port, dns_data_db): if port['fixed_ips']: - return self._get_dns_names_for_port(port['fixed_ips'], dns_data_db) + return self._get_dns_names_for_port( + port['fixed_ips'], dns_data_db, port['network_id']) return [] def _extend_port_dict(self, session, db_data, response_data, dns_data_db): @@ -281,10 +286,24 @@ class DNSExtensionDriverNSXv(DNSExtensionDriver): class DNSExtensionDriverNSXv3(DNSExtensionDriver): def initialize(self): + self._availability_zones = nsx_az.NsxV3AvailabilityZones() LOG.info(_LI("DNSExtensionDriverNSXv3 initialization complete")) - def _get_dns_domain(self): - if cfg.CONF.nsx_v3.dns_domain: + def _get_network_az(self, network_id): + context = n_context.get_admin_context() + network = self._get_network(context, network_id) + if az_ext.AZ_HINTS in network and network[az_ext.AZ_HINTS]: + az_name = network[az_ext.AZ_HINTS][0] + return self._availability_zones.get_availability_zone(az_name) + return self._availability_zones.get_default_availability_zone() + + def _get_dns_domain(self, network_id): + # try to get the dns-domain from the specific availability zone + # of this network + az = self._get_network_az(network_id) + if az.dns_domain: + dns_domain = az.dns_domain + elif cfg.CONF.nsx_v3.dns_domain: dns_domain = cfg.CONF.nsx_v3.dns_domain elif cfg.CONF.dns_domain: dns_domain = cfg.CONF.dns_domain diff --git a/vmware_nsx/plugins/nsx_v3/availability_zones.py b/vmware_nsx/plugins/nsx_v3/availability_zones.py new file mode 100644 index 0000000000..e8b36d86c8 --- /dev/null +++ b/vmware_nsx/plugins/nsx_v3/availability_zones.py @@ -0,0 +1,92 @@ +# Copyright 2017 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 oslo_config import cfg + +from vmware_nsx._i18n import _ +from vmware_nsx.common import availability_zones as common_az +from vmware_nsx.common import config +from vmware_nsx.common import exceptions as nsx_exc + + +DEFAULT_NAME = common_az.DEFAULT_NAME + + +class NsxV3AvailabilityZone(common_az.ConfiguredAvailabilityZone): + + def init_from_config_line(self, config_line): + # Not supported for nsx_v3 (old configuration) + raise nsx_exc.NsxInvalidConfiguration( + opt_name="availability_zones", + opt_value=config_line, + reason=_("Expected a list of names")) + + def init_from_config_section(self, az_name): + az_info = config.get_nsxv3_az_opts(self.name) + + # The optional parameters will get the global values if not + # defined for this AZ + self.metadata_proxy = az_info.get('metadata_proxy') + if not self.metadata_proxy: + raise nsx_exc.NsxInvalidConfiguration( + opt_name="metadata_proxy", + opt_value='None', + reason=(_("metadata_proxy for availability zone %s " + "must be defined") % az_name)) + + self.dhcp_profile = az_info.get('dhcp_profile') + if not self.dhcp_profile: + raise nsx_exc.NsxInvalidConfiguration( + opt_name="dhcp_profile", + opt_value='None', + reason=(_("dhcp_profile for availability zone %s " + "must be defined") % az_name)) + + self.native_metadata_route = az_info.get('native_metadata_route') + if self.native_metadata_route is None: + self.native_metadata_route = cfg.CONF.nsx_v3.native_metadata_route + + self.dns_domain = az_info.get('dns_domain') + if self.dns_domain is None: + self.dns_domain = cfg.CONF.nsx_v3.dns_domain + + self.nameservers = az_info.get('nameservers') + if self.nameservers is None: + self.nameservers = cfg.CONF.nsx_v3.nameservers + + def init_default_az(self): + # use the default configuration + self.metadata_proxy = cfg.CONF.nsx_v3.metadata_proxy + self.dhcp_profile = cfg.CONF.nsx_v3.dhcp_profile + self.native_metadata_route = cfg.CONF.nsx_v3.native_metadata_route + self.dns_domain = cfg.CONF.nsx_v3.dns_domain + self.nameservers = cfg.CONF.nsx_v3.nameservers + + def translate_configured_names_to_uuids(self, nsxlib): + dhcp_id = nsxlib.native_dhcp_profile.get_id_by_name_or_id( + self.dhcp_profile) + self._native_dhcp_profile_uuid = dhcp_id + + proxy_id = nsxlib.native_md_proxy.get_id_by_name_or_id( + self.metadata_proxy) + self._native_md_proxy_uuid = proxy_id + + +class NsxV3AvailabilityZones(common_az.ConfiguredAvailabilityZones): + + def __init__(self): + super(NsxV3AvailabilityZones, self).__init__( + cfg.CONF.nsx_v3.availability_zones, + NsxV3AvailabilityZone) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index df142350b9..d2aedd7324 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -78,6 +78,7 @@ from sqlalchemy import exc as sql_exc from vmware_nsx._i18n import _, _LE, _LI, _LW from vmware_nsx.api_replay import utils as api_replay_utils +from vmware_nsx.common import availability_zones as nsx_com_az from vmware_nsx.common import config # noqa from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.common import l3_rpc_agent_api @@ -93,6 +94,7 @@ from vmware_nsx.extensions import advancedserviceproviders as as_providers from vmware_nsx.extensions import maclearning as mac_ext from vmware_nsx.extensions import providersecuritygroup as provider_sg from vmware_nsx.extensions import securitygrouplogging as sg_logging +from vmware_nsx.plugins.nsx_v3 import availability_zones as nsx_az from vmware_nsx.plugins.nsx_v3 import utils as v3_utils from vmware_nsx.services.qos.common import utils as qos_com_utils from vmware_nsx.services.qos.nsx_v3 import driver as qos_driver @@ -134,7 +136,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, portsecurity_db.PortSecurityDbMixin, extradhcpopt_db.ExtraDhcpOptMixin, dns_db.DNSDbMixin, - mac_db.MacLearningDbMixin): + mac_db.MacLearningDbMixin, + nsx_com_az.NSXAvailabilityZonesPluginCommon): __native_bulk_support = True __native_pagination_support = True @@ -195,6 +198,11 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self.cfg_group = 'nsx_v3' # group name for nsx_v3 section in nsx.ini self.tier0_groups_dict = {} + + # Initialize the network availability zones, which will be used only + # when native_dhcp_metadata is True + self.init_availability_zones() + # Translate configured transport zones, routers, dhcp profile and # metadata proxy names to uuid. self._translate_configured_names_to_uuids() @@ -240,12 +248,24 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, # Each extension driver that supports extend attribute for the resources # can add those attribute to the result. db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( - attributes.NETWORKS, ['_ext_extend_network_dict']) + attributes.NETWORKS, ['_ext_extend_network_dict', + '_extend_availability_zone_hints']) db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( attributes.PORTS, ['_ext_extend_port_dict']) db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( attributes.SUBNETS, ['_ext_extend_subnet_dict']) + def init_availability_zones(self): + # availability zones are supported only with native dhcp + # if not - the default az will be loaded and used internally only + if (cfg.CONF.nsx_v3.availability_zones and + not cfg.CONF.nsx_v3.native_dhcp_metadata): + msg = _("Availability zones are not supported without native " + "DHCP metadata") + LOG.error(msg) + raise n_exc.InvalidInput(error_message=msg) + self._availability_zones_data = nsx_az.NsxV3AvailabilityZones() + def _init_nsx_profiles(self): LOG.debug("Initializing NSX v3 port spoofguard switching profile") if not self._init_port_security_profile(): @@ -304,23 +324,17 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, cfg.CONF.nsx_v3.default_tier0_router) self._default_tier0_router = rtr_id - self._native_dhcp_profile_uuid = None - self._native_md_proxy_uuid = None + # Validate and translate native dhcp profiles per az if cfg.CONF.nsx_v3.native_dhcp_metadata: - if cfg.CONF.nsx_v3.dhcp_profile: - id = self.nsxlib.native_dhcp_profile.get_id_by_name_or_id( - cfg.CONF.nsx_v3.dhcp_profile) - self._native_dhcp_profile_uuid = id - else: + if not cfg.CONF.nsx_v3.dhcp_profile: raise cfg.RequiredOptError("dhcp_profile") - if cfg.CONF.nsx_v3.metadata_proxy: - proxy_id = self.nsxlib.native_md_proxy.get_id_by_name_or_id( - cfg.CONF.nsx_v3.metadata_proxy) - self._native_md_proxy_uuid = proxy_id - else: + if not cfg.CONF.nsx_v3.metadata_proxy: raise cfg.RequiredOptError("metadata_proxy") + for az in self.get_azs_list(): + az.translate_configured_names_to_uuids(self.nsxlib) + def _extend_port_dict_binding(self, context, port_data): port_data[pbin.VIF_TYPE] = pbin.VIF_TYPE_OVS port_data[pbin.VNIC_TYPE] = pbin.VNIC_NORMAL @@ -515,25 +529,27 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, def _init_native_dhcp(self): try: - nsx_resources.DhcpProfile(self._nsx_client).get( - self._native_dhcp_profile_uuid) + for az in self.get_azs_list(): + nsx_resources.DhcpProfile(self._nsx_client).get( + az._native_dhcp_profile_uuid) self._dhcp_server = nsx_resources.LogicalDhcpServer( self._nsx_client) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error(_LE("Unable to retrieve DHCP Profile %s, " "native DHCP service is not supported"), - self._native_dhcp_profile_uuid) + az._native_dhcp_profile_uuid) def _init_native_metadata(self): try: - nsx_resources.MetaDataProxy(self._nsx_client).get( - self._native_md_proxy_uuid) + for az in self.get_azs_list(): + nsx_resources.MetaDataProxy(self._nsx_client).get( + az._native_md_proxy_uuid) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error(_LE("Unable to retrieve Metadata Proxy %s, " "native metadata service is not supported"), - self._native_md_proxy_uuid) + az._native_md_proxy_uuid) def _setup_rpc(self): self.endpoints = [dhcp_rpc.DhcpRpcCallback(), @@ -778,6 +794,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self._create_network_at_the_backend(context, net_data)) is_backend_network = True try: + az_name = nsx_az.DEFAULT_NAME with context.session.begin(subtransactions=True): # Create network in Neutron created_net = super(NsxV3Plugin, self).create_network(context, @@ -791,14 +808,16 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self._process_l3_create(context, created_net, net_data) if az_ext.AZ_HINTS in net_data: + net_hints = net_data[az_ext.AZ_HINTS] self.validate_availability_zones(context, 'network', - net_data[az_ext.AZ_HINTS]) - az_hints = az_ext.convert_az_list_to_string( - net_data[az_ext.AZ_HINTS]) - net_id = created_net['id'] - super(NsxV3Plugin, self).update_network(context, - net_id, {'network': {az_ext.AZ_HINTS: az_hints}}) - created_net[az_ext.AZ_HINTS] = az_hints + net_hints) + if net_hints: + az_name = net_hints[0] + az_hints = az_ext.convert_az_list_to_string(net_hints) + super(NsxV3Plugin, self).update_network( + context, + created_net['id'], + {'network': {az_ext.AZ_HINTS: az_hints}}) if is_provider_net: # Save provider network fields, needed by get_network() @@ -817,6 +836,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, nsx_net_id) if is_backend_network and cfg.CONF.nsx_v3.native_dhcp_metadata: + az = self.get_az_by_hint(az_name) # Enable native metadata proxy for this network. tags = self.nsxlib.build_v3_tags_payload( net_data, resource_type='os-neutron-net-id', @@ -825,7 +845,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, 'mdproxy', created_net['name'] or 'network'), created_net['id']) md_port = self._port_client.create( - nsx_net_id, self._native_md_proxy_uuid, + nsx_net_id, az._native_md_proxy_uuid, tags=tags, name=name, attachment_type=nsxlib_consts.ATTACHMENT_MDPROXY) LOG.debug("Created MD-Proxy logical port %(port)s " @@ -1018,10 +1038,11 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, existing_ports = super(NsxV3Plugin, self).get_ports( context, filters={'network_id': [network['id']], 'fixed_ips': {'subnet_id': [subnet['id']]}}) + az = self.get_network_az(network) port_data = { "name": "", "admin_state_up": True, - "device_id": self._native_dhcp_profile_uuid, + "device_id": az._native_dhcp_profile_uuid, "device_owner": const.DEVICE_OWNER_DHCP, "network_id": network['id'], "tenant_id": network["tenant_id"], @@ -1034,8 +1055,10 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, network, resource_type='os-neutron-net-id', project_name=context.tenant_name) server_data = self.nsxlib.native_dhcp.build_server_config( - network, subnet, neutron_port, net_tags) - server_data['dhcp_profile_id'] = self._native_dhcp_profile_uuid + network, subnet, neutron_port, net_tags, + default_dns_nameservers=az.nameservers, + default_dns_domain=az.dns_domain) + server_data['dhcp_profile_id'] = az._native_dhcp_profile_uuid nsx_net_id = self._get_network_nsx_id(context, network['id']) port_tags = self.nsxlib.build_v3_tags_payload( neutron_port, resource_type='os-neutron-dport-id', @@ -1383,7 +1406,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, port = self._get_port(context, binding['port_id']) self._update_dhcp_binding_on_server( context, binding, port['mac_address'], - binding['ip_address'], kwargs['gateway_ip']) + binding['ip_address'], kwargs['gateway_ip'], + port['network_id']) if (cfg.CONF.nsx_v3.metadata_on_demand and not cfg.CONF.nsx_v3.native_dhcp_metadata): @@ -1762,10 +1786,11 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, msg = (_("DHCP option %s is not supported") % opt_name) raise n_exc.InvalidInput(error_message=msg) - def _get_dhcp_options(self, ip, extra_dhcp_opts): + def _get_dhcp_options(self, context, ip, extra_dhcp_opts, net_id): # Always add option121. + net_az = self.get_network_az_by_net_id(context, net_id) options = {'option121': {'static_routes': [ - {'network': '%s' % cfg.CONF.nsx_v3.native_metadata_route, + {'network': '%s' % net_az.native_metadata_route, 'next_hop': ip}]}} # Adding extra options only if configured on port if extra_dhcp_opts: @@ -1797,7 +1822,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, gateway_ip = self.get_subnet( context, subnet_id).get('gateway_ip') options = self._get_dhcp_options( - ip, port.get(ext_edo.EXTRADHCPOPTS)) + context, ip, port.get(ext_edo.EXTRADHCPOPTS), + port['network_id']) binding = self._dhcp_server.create_binding( dhcp_service_id, port['mac_address'], ip, hostname, cfg.CONF.nsx_v3.dhcp_lease_time, options, gateway_ip) @@ -1933,7 +1959,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, if binding: self._update_dhcp_binding_on_server( context, binding, new_port['mac_address'], - ips_to_add[i][1], dhcp_opts=dhcp_opts) + ips_to_add[i][1], old_port['network_id'], + dhcp_opts=dhcp_opts) else: for (subnet_id, ip) in ips_to_delete: binding = self._find_dhcp_binding(subnet_id, ip, @@ -1957,18 +1984,21 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, for binding in bindings: self._update_dhcp_binding_on_server( context, binding, new_port['mac_address'], - binding['ip_address'], + binding['ip_address'], old_port['network_id'], dhcp_opts=dhcp_opts if dhcp_opts_changed else None) def _update_dhcp_binding_on_server(self, context, binding, mac, ip, - gateway_ip=False, dhcp_opts=None): + net_id, gateway_ip=False, + dhcp_opts=None): try: data = {'mac_address': mac, 'ip_address': ip} if ip != binding['ip_address']: data['host_name'] = 'host-%s' % ip.replace('.', '-') - data['options'] = self._get_dhcp_options(ip, dhcp_opts) + data['options'] = self._get_dhcp_options( + context, ip, dhcp_opts, net_id) elif dhcp_opts is not None: - data['options'] = self._get_dhcp_options(ip, dhcp_opts) + data['options'] = self._get_dhcp_options( + context, ip, dhcp_opts, net_id) if gateway_ip is not False: # Note that None is valid for gateway_ip, means deleting it. data['gateway_ip'] = gateway_ip @@ -3479,3 +3509,46 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, def save_security_group_rule_mappings(self, context, firewall_rules): rules = [(rule['display_name'], rule['id']) for rule in firewall_rules] nsx_db.save_sg_rule_mappings(context.session, rules) + + def _list_availability_zones(self, context, filters=None): + # If no native_dhcp_metadata - use neutron AZs + if not cfg.CONF.nsx_v3.native_dhcp_metadata: + return super(NsxV3Plugin, self)._list_availability_zones( + context, filters=filters) + + #TODO(asarfaty): We may need to use the filters arg, but now it + # is here only for overriding the original api + result = {} + for az in self._availability_zones_data.list_availability_zones(): + # Add this availability zone as a network resource + result[(az, 'network')] = True + return result + + def validate_availability_zones(self, context, resource_type, + availability_zones): + # If no native_dhcp_metadata - use neutron AZs + if not cfg.CONF.nsx_v3.native_dhcp_metadata: + return super(NsxV3Plugin, self).validate_availability_zones( + context, resource_type, availability_zones) + # Validate against the configured AZs + return self.validate_obj_azs(availability_zones) + + def _extend_availability_zone_hints(self, net_res, net_db): + net_res[az_ext.AZ_HINTS] = az_ext.convert_az_string_to_list( + net_db[az_ext.AZ_HINTS]) + if cfg.CONF.nsx_v3.native_dhcp_metadata: + # When using the configured AZs, the az will always be the same + # as the hint (or default if none) + if net_res[az_ext.AZ_HINTS]: + az_name = net_res[az_ext.AZ_HINTS][0] + else: + az_name = nsx_az.DEFAULT_NAME + net_res[az_ext.AVAILABILITY_ZONES] = [az_name] + + def get_network_az_by_net_id(self, context, network_id): + try: + network = self.get_network(context, network_id) + except Exception: + return self.get_default_az() + + return self.get_network_az(network) diff --git a/vmware_nsx/plugins/nsx_v3/utils.py b/vmware_nsx/plugins/nsx_v3/utils.py index 7be0769ea9..9dcf893809 100644 --- a/vmware_nsx/plugins/nsx_v3/utils.py +++ b/vmware_nsx/plugins/nsx_v3/utils.py @@ -144,6 +144,5 @@ def get_nsxlib_wrapper(nsx_username=None, nsx_password=None, basic_auth=False): plugin_tag=NSX_NEUTRON_PLUGIN, plugin_ver=n_version.version_info.release_string(), dns_nameservers=cfg.CONF.nsx_v3.nameservers, - dns_domain=cfg.CONF.nsx_v3.dns_domain, - dhcp_profile_uuid=cfg.CONF.nsx_v3.dhcp_profile) + dns_domain=cfg.CONF.nsx_v3.dns_domain) return v3.NsxLib(nsxlib_config) diff --git a/vmware_nsx/shell/admin/plugins/nsxv3/resources/dhcp_binding.py b/vmware_nsx/shell/admin/plugins/nsxv3/resources/dhcp_binding.py index b5b90cb97e..91a478e786 100644 --- a/vmware_nsx/shell/admin/plugins/nsxv3/resources/dhcp_binding.py +++ b/vmware_nsx/shell/admin/plugins/nsxv3/resources/dhcp_binding.py @@ -55,6 +55,7 @@ def nsx_update_dhcp_bindings(resource, event, trigger, **kwargs): return dhcp_profile_uuid = None + # TODO(asarfaty) Add availability zones support here if kwargs.get('property'): properties = admin_utils.parse_multi_keyval_opt(kwargs['property']) dhcp_profile_uuid = properties.get('dhcp_profile_uuid') @@ -91,6 +92,8 @@ def nsx_update_dhcp_bindings(resource, event, trigger, **kwargs): net_tags = nsxlib.build_v3_tags_payload( network, resource_type='os-neutron-net-id', project_name='admin') + # TODO(asarfaty): add default_dns_nameservers & dns_domain + # from availability zone server_data = nsxlib.native_dhcp.build_server_config( network, subnet, port, net_tags) server_data['dhcp_profile_id'] = dhcp_profile_uuid diff --git a/vmware_nsx/tests/unit/nsx_v3/test_availability_zones.py b/vmware_nsx/tests/unit/nsx_v3/test_availability_zones.py new file mode 100644 index 0000000000..edc47d732f --- /dev/null +++ b/vmware_nsx/tests/unit/nsx_v3/test_availability_zones.py @@ -0,0 +1,112 @@ +# Copyright 2017 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 oslo_config import cfg +from oslo_utils import uuidutils + +from neutron.tests import base + +from vmware_nsx.common import config +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.plugins.nsx_v3 import availability_zones as nsx_az + + +class Nsxv3AvailabilityZonesTestCase(base.BaseTestCase): + + def setUp(self): + super(Nsxv3AvailabilityZonesTestCase, self).setUp() + self.az_name = "zone1" + self.group_name = "az:%s" % self.az_name + config.register_nsxv3_azs(cfg.CONF, [self.az_name]) + self.global_md_proxy = uuidutils.generate_uuid() + cfg.CONF.set_override( + "metadata_proxy", self.global_md_proxy, group="nsx_v3") + self.global_dhcp_profile = uuidutils.generate_uuid() + cfg.CONF.set_override( + "dhcp_profile", self.global_dhcp_profile, group="nsx_v3") + cfg.CONF.set_override( + "native_metadata_route", "1.1.1.1", group="nsx_v3") + cfg.CONF.set_override("dns_domain", "xxx.com", group="nsx_v3") + cfg.CONF.set_override("nameservers", ["10.1.1.1"], group="nsx_v3") + + def _config_az(self, + metadata_proxy="metadata_proxy1", + dhcp_profile="dhcp_profile1", + native_metadata_route="2.2.2.2", + dns_domain="aaa.com", + nameservers=["20.1.1.1"]): + if metadata_proxy is not None: + cfg.CONF.set_override("metadata_proxy", metadata_proxy, + group=self.group_name) + if dhcp_profile is not None: + cfg.CONF.set_override("dhcp_profile", dhcp_profile, + group=self.group_name) + if native_metadata_route is not None: + cfg.CONF.set_override("native_metadata_route", + native_metadata_route, + group=self.group_name) + if dns_domain is not None: + cfg.CONF.set_override("dns_domain", dns_domain, + group=self.group_name) + if nameservers is not None: + cfg.CONF.set_override("nameservers", nameservers, + group=self.group_name) + + def test_simple_availability_zone(self): + self._config_az() + az = nsx_az.NsxV3AvailabilityZone(self.az_name) + self.assertEqual(self.az_name, az.name) + self.assertEqual("metadata_proxy1", az.metadata_proxy) + self.assertEqual("dhcp_profile1", az.dhcp_profile) + self.assertEqual("2.2.2.2", az.native_metadata_route) + self.assertEqual("aaa.com", az.dns_domain) + self.assertEqual(["20.1.1.1"], az.nameservers) + + def test_missing_group_section(self): + self.assertRaises( + nsx_exc.NsxInvalidConfiguration, + nsx_az.NsxV3AvailabilityZone, + "doesnt_exist") + + def test_availability_zone_missing_metadata_proxy(self): + # Mandatory parameter + self._config_az(metadata_proxy=None) + self.assertRaises( + nsx_exc.NsxInvalidConfiguration, + nsx_az.NsxV3AvailabilityZone, + self.az_name) + + def test_availability_zone_missing_dhcp_profile(self): + # Mandatory parameter + self._config_az(dhcp_profile=None) + self.assertRaises( + nsx_exc.NsxInvalidConfiguration, + nsx_az.NsxV3AvailabilityZone, + self.az_name) + + def test_availability_zone_missing_md_route(self): + self._config_az(native_metadata_route=None) + az = nsx_az.NsxV3AvailabilityZone(self.az_name) + self.assertEqual("1.1.1.1", az.native_metadata_route) + + def test_availability_zone_missing_dns_domain(self): + self._config_az(dns_domain=None) + az = nsx_az.NsxV3AvailabilityZone(self.az_name) + self.assertEqual("xxx.com", az.dns_domain) + + def test_availability_zone_missing_nameservers(self): + self._config_az(nameservers=None) + az = nsx_az.NsxV3AvailabilityZone(self.az_name) + self.assertEqual(["10.1.1.1"], az.nameservers) diff --git a/vmware_nsx/tests/unit/nsx_v3/test_dhcp_metadata.py b/vmware_nsx/tests/unit/nsx_v3/test_dhcp_metadata.py index 38aea6ffc5..e53a7a3bbd 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_dhcp_metadata.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_dhcp_metadata.py @@ -16,15 +16,17 @@ import mock import netaddr -from neutron.extensions import securitygroup as secgrp +from oslo_config import cfg +from oslo_utils import uuidutils +from neutron.extensions import securitygroup as secgrp from neutron_lib.api.definitions import provider_net as pnet from neutron_lib import constants from neutron_lib import context from neutron_lib import exceptions as n_exc -from oslo_config import cfg -from oslo_utils import uuidutils +from neutron_lib.plugins import directory +from vmware_nsx.common import config from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.common import utils from vmware_nsx.db import db as nsx_db @@ -34,6 +36,26 @@ from vmware_nsxlib.v3 import nsx_constants from vmware_nsxlib.v3 import resources as nsx_resources +def set_az_in_config(name, metadata_proxy="metadata_proxy1", + dhcp_profile="dhcp_profile1", + native_metadata_route="2.2.2.2", + dns_domain='aaaa', + nameservers=['bbbb']): + group_name = 'az:%s' % name + cfg.CONF.set_override('availability_zones', [name], group="nsx_v3") + config.register_nsxv3_azs(cfg.CONF, [name]) + cfg.CONF.set_override("metadata_proxy", metadata_proxy, + group=group_name) + cfg.CONF.set_override("dhcp_profile", dhcp_profile, + group=group_name) + cfg.CONF.set_override("native_metadata_route", native_metadata_route, + group=group_name) + cfg.CONF.set_override("dns_domain", dns_domain, + group=group_name) + cfg.CONF.set_override("nameservers", nameservers, + group=group_name) + + class NsxNativeDhcpTestCase(test_plugin.NsxV3PluginTestCaseMixin): def setUp(self): @@ -42,11 +64,15 @@ class NsxNativeDhcpTestCase(test_plugin.NsxV3PluginTestCaseMixin): self._orig_native_dhcp_metadata = cfg.CONF.nsx_v3.native_dhcp_metadata cfg.CONF.set_override('dhcp_agent_notification', False) cfg.CONF.set_override('native_dhcp_metadata', True, 'nsx_v3') + self._az_name = 'zone1' + self.az_metadata_route = '3.3.3.3' + set_az_in_config(self._az_name, + native_metadata_route=self.az_metadata_route) self._patcher = mock.patch.object(nsx_resources.DhcpProfile, 'get') self._patcher.start() - # Need to run _translate_configured_names_to_uuids and - # _init_dhcp_metadata() manually because plugin was started - # before setUp() overrides CONF.nsx_v3.native_dhcp_metadata. + # Need to run some plugin init methods manually because plugin was + # started before setUp() overrides CONF.nsx_v3.native_dhcp_metadata. + self.plugin.init_availability_zones() self.plugin._translate_configured_names_to_uuids() self.plugin._init_dhcp_metadata() @@ -741,6 +767,96 @@ class NsxNativeDhcpTestCase(test_plugin.NsxV3PluginTestCaseMixin): port['port']['id']) self.assertEqual(delete_dhcp_binding.call_count, 2) + def test_create_network_with_bad_az_hint(self): + p = directory.get_plugin() + ctx = context.get_admin_context() + data = {'network': { + 'name': 'test-az', + 'tenant_id': self._tenant_id, + 'port_security_enabled': False, + 'admin_state_up': True, + 'shared': False, + 'availability_zone_hints': ['bad_hint'] + }} + self.assertRaises(n_exc.NeutronException, + p.create_network, + ctx, data) + + def test_create_network_with_az_hint(self): + p = directory.get_plugin() + ctx = context.get_admin_context() + + data = {'network': { + 'name': 'test-az', + 'tenant_id': self._tenant_id, + 'port_security_enabled': False, + 'admin_state_up': True, + 'shared': False, + 'availability_zone_hints': [self._az_name] + }} + + # network creation should succeed + net = p.create_network(ctx, data) + self.assertEqual([self._az_name], + net['availability_zone_hints']) + self.assertEqual([self._az_name], + net['availability_zones']) + + def test_create_network_with_no_az_hint(self): + p = directory.get_plugin() + ctx = context.get_admin_context() + + data = {'network': { + 'name': 'test-az', + 'tenant_id': self._tenant_id, + 'port_security_enabled': False, + 'admin_state_up': True, + 'shared': False + }} + + # network creation should succeed + net = p.create_network(ctx, data) + self.assertEqual([], + net['availability_zone_hints']) + self.assertEqual(['default'], + net['availability_zones']) + + def test_dhcp_service_with_create_az_network(self): + # Test if DHCP service is disabled on a network when it is created. + with self.network(availability_zone_hints=[self._az_name], + arg_list=('availability_zone_hints',)) as network: + self._verify_dhcp_service(network['network']['id'], + network['network']['tenant_id'], False) + + def test_dhcp_binding_with_create_az_port(self): + # Test if DHCP binding is added when a compute port is created. + with mock.patch.object(nsx_resources.LogicalDhcpServer, + 'create_binding', + return_value={"id": uuidutils.generate_uuid()} + ) as create_dhcp_binding: + with self.network( + availability_zone_hints=[self._az_name], + arg_list=('availability_zone_hints',)) as network: + with self.subnet(enable_dhcp=True, network=network) as subnet: + device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'X' + device_id = uuidutils.generate_uuid() + with self.port(subnet=subnet, device_owner=device_owner, + device_id=device_id) as port: + dhcp_service = nsx_db.get_nsx_service_binding( + context.get_admin_context().session, + subnet['subnet']['network_id'], + nsx_constants.SERVICE_DHCP) + ip = port['port']['fixed_ips'][0]['ip_address'] + hostname = 'host-%s' % ip.replace('.', '-') + options = {'option121': {'static_routes': [ + {'network': '%s' % self.az_metadata_route, + 'next_hop': ip}]}} + create_dhcp_binding.assert_called_once_with( + dhcp_service['nsx_service_id'], + port['port']['mac_address'], ip, hostname, + cfg.CONF.nsx_v3.dhcp_lease_time, options, + subnet['subnet']['gateway_ip']) + class NsxNativeMetadataTestCase(test_plugin.NsxV3PluginTestCaseMixin): @@ -750,11 +866,12 @@ class NsxNativeMetadataTestCase(test_plugin.NsxV3PluginTestCaseMixin): self._orig_native_dhcp_metadata = cfg.CONF.nsx_v3.native_dhcp_metadata cfg.CONF.set_override('dhcp_agent_notification', False) cfg.CONF.set_override('native_dhcp_metadata', True, 'nsx_v3') + self._az_name = 'zone1' + self._az_metadata_proxy = 'dummy' + set_az_in_config(self._az_name, metadata_proxy=self._az_metadata_proxy) self._patcher = mock.patch.object(nsx_resources.MetaDataProxy, 'get') self._patcher.start() - # Need to run _translate_configured_names_to_uuids and - # _init_dhcp_metadata() manually because plugin was started - # before setUp() overrides CONF.nsx_v3.native_dhcp_metadata. + self.plugin.init_availability_zones() self.plugin._translate_configured_names_to_uuids() self.plugin._init_dhcp_metadata() @@ -801,6 +918,27 @@ class NsxNativeMetadataTestCase(test_plugin.NsxV3PluginTestCaseMixin): tags=tags, name=name, attachment_type=nsx_constants.ATTACHMENT_MDPROXY) + def test_metadata_proxy_with_create_az_network(self): + # Test if native metadata proxy is enabled on a network when it is + # created. + with mock.patch.object(nsx_resources.LogicalPort, + 'create') as create_logical_port: + with self.network( + availability_zone_hints=[self._az_name], + arg_list=('availability_zone_hints',)) as network: + nsx_net_id = self.plugin._get_network_nsx_id( + context.get_admin_context(), network['network']['id']) + tags = self.plugin.nsxlib.build_v3_tags_payload( + network['network'], resource_type='os-neutron-net-id', + project_name=None) + name = utils.get_name_and_uuid('%s-%s' % ( + 'mdproxy', network['network']['name'] or 'network'), + network['network']['id']) + create_logical_port.assert_called_once_with( + nsx_net_id, self._az_metadata_proxy, + tags=tags, name=name, + attachment_type=nsx_constants.ATTACHMENT_MDPROXY) + def test_metadata_proxy_with_get_subnets(self): # Test if get_subnets() handles advanced-service-provider extension, # which is used when processing metadata requests. diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index 1738ff94ff..f469b2b20c 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -81,6 +81,9 @@ def _mock_nsx_backend_calls(): def _return_id(*args, **kwargs): return uuidutils.generate_uuid() + def _return_same(key, *args, **kwargs): + return key + mock.patch( "vmware_nsxlib.v3.resources.SwitchingProfile.find_by_display_name", return_value=[fake_profile] @@ -120,7 +123,7 @@ def _mock_nsx_backend_calls(): mock.patch( "vmware_nsxlib.v3.NsxLibMetadataProxy.get_id_by_name_or_id", - return_value=NSX_METADATA_PROXY_ID).start() + side_effect=_return_same).start() mock.patch( "vmware_nsxlib.v3.resources.LogicalPort.create",