diff --git a/etc/neutron/plugins/nicira/nvp.ini b/etc/neutron/plugins/nicira/nvp.ini index 921520387b9..831eed8b707 100644 --- a/etc/neutron/plugins/nicira/nvp.ini +++ b/etc/neutron/plugins/nicira/nvp.ini @@ -38,6 +38,12 @@ # To be specified for providing a predefined gateway tenant for connecting their networks. # default_l2_gw_service_uuid = +# (Optional) UUID for the default service cluster. A service cluster is introduced to +# represent a group of gateways and it is needed in order to use Logical Services like +# dhcp and metadata in the logical space. NOTE: If agent_mode is set to 'agentless' this +# config parameter *MUST BE* set to a valid pre-existent service cluster uuid. +# default_service_cluster_uuid = + # Name of the default interface name to be used on network-gateway. This value # will be used for any device associated with a network gateway for which an # interface name was not specified @@ -47,7 +53,6 @@ # number of network gateways allowed per tenant, -1 means unlimited # quota_network_gateway = 5 - [nvp] # Maximum number of ports for each bridged logical switch # The recommended value for this parameter varies with NVP version @@ -158,3 +163,13 @@ # (Optional) Asynchronous task status check interval # default is 2000 (millisecond) # task_status_check_interval = 2000 + +[nvp_dhcp] +# (Optional) Comma separated list of additional dns servers. Default is an empty list +# extra_domain_name_servers = + +# Domain to use for building the hostnames +# domain_name = openstacklocal + +# Default DHCP lease time +# default_lease_time = 43200 diff --git a/neutron/api/v2/base.py b/neutron/api/v2/base.py index 83d842752e1..12a10ae34fc 100644 --- a/neutron/api/v2/base.py +++ b/neutron/api/v2/base.py @@ -24,6 +24,7 @@ from neutron.api import api_common from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.api.v2 import attributes from neutron.api.v2 import resource as wsgi_resource +from neutron.common import constants as const from neutron.common import exceptions from neutron.openstack.common import log as logging from neutron.openstack.common.notifier import api as notifier_api @@ -68,7 +69,12 @@ class Controller(object): self._policy_attrs = [name for (name, info) in self._attr_info.items() if info.get('required_by_policy')] self._publisher_id = notifier_api.publisher_id('network') - self._dhcp_agent_notifier = dhcp_rpc_agent_api.DhcpAgentNotifyAPI() + # use plugin's dhcp notifier, if this is already instantiated + agent_notifiers = getattr(plugin, 'agent_notifiers', {}) + self._dhcp_agent_notifier = ( + agent_notifiers.get(const.AGENT_TYPE_DHCP) or + dhcp_rpc_agent_api.DhcpAgentNotifyAPI() + ) self._member_actions = member_actions self._primary_key = self._get_primary_key() if self._allow_pagination and self._native_pagination: diff --git a/neutron/plugins/nicira/NeutronPlugin.py b/neutron/plugins/nicira/NeutronPlugin.py index ab3319bc4bd..c9e7595fa01 100644 --- a/neutron/plugins/nicira/NeutronPlugin.py +++ b/neutron/plugins/nicira/NeutronPlugin.py @@ -160,6 +160,8 @@ class NvpPluginV2(addr_pair_db.AllowedAddressPairsMixin, # TODO(salv-orlando): Replace These dicts with # collections.defaultdict for better handling of default values # Routines for managing logical ports in NVP + self.port_special_owners = [l3_db.DEVICE_OWNER_ROUTER_GW, + l3_db.DEVICE_OWNER_ROUTER_INTF] self._port_drivers = { 'create': {l3_db.DEVICE_OWNER_ROUTER_GW: self._nvp_create_ext_gw_port, @@ -470,9 +472,7 @@ class NvpPluginV2(addr_pair_db.AllowedAddressPairsMixin, True) nicira_db.add_neutron_nvp_port_mapping( context.session, port_data['id'], lport['uuid']) - if (not port_data['device_owner'] in - (l3_db.DEVICE_OWNER_ROUTER_GW, - l3_db.DEVICE_OWNER_ROUTER_INTF)): + if port_data['device_owner'] not in self.port_special_owners: nvplib.plug_interface(self.cluster, selected_lswitch['uuid'], lport['uuid'], "VifAttachment", port_data['id']) diff --git a/neutron/plugins/nicira/common/config.py b/neutron/plugins/nicira/common/config.py index 09d7e232a27..18a33f1fa45 100644 --- a/neutron/plugins/nicira/common/config.py +++ b/neutron/plugins/nicira/common/config.py @@ -119,6 +119,9 @@ cluster_opts = [ cfg.StrOpt('default_l2_gw_service_uuid', help=_("Unique identifier of the NVP L2 Gateway service " "which will be used by default for network gateways")), + cfg.StrOpt('default_service_cluster_uuid', + help=_("Unique identifier of the Service Cluster which will " + "be used by logical services like dhcp and metadata")), cfg.StrOpt('default_interface_name', default='breth0', help=_("Name of the interface on a L2 Gateway transport node" "which should be used by default when setting up a " diff --git a/neutron/plugins/nicira/common/exceptions.py b/neutron/plugins/nicira/common/exceptions.py index bc7c4f5c0b4..14add620194 100644 --- a/neutron/plugins/nicira/common/exceptions.py +++ b/neutron/plugins/nicira/common/exceptions.py @@ -80,3 +80,30 @@ class NvpServiceOverQuota(q_exc.Conflict): class NvpVcnsDriverException(NvpServicePluginException): message = _("Error happened in NVP VCNS Driver: %(err_msg)s") + + +class ServiceClusterUnavailable(NvpPluginException): + message = _("Service cluster: '%(cluster_id)s' is unavailable. Please, " + "check NVP setup and/or configuration") + + +class PortConfigurationError(NvpPluginException): + message = _("An error occurred while connecting LSN %(lsn_id)s " + "and network %(net_id)s via port %(port_id)s") + + def __init__(self, **kwargs): + super(PortConfigurationError, self).__init__(**kwargs) + self.port_id = kwargs.get('port_id') + + +class LsnNotFound(q_exc.NotFound): + message = _('Unable to find LSN for %(entity)s %(entity_id)s') + + +class LsnPortNotFound(q_exc.NotFound): + message = (_('Unable to find port for LSN %(lsn_id)s ' + 'and %(entity)s %(entity_id)s')) + + +class LsnConfigurationConflict(NvpPluginException): + message = _("Configuration conflict on Logical Service Node %(lsn_id)s") diff --git a/neutron/plugins/nicira/common/utils.py b/neutron/plugins/nicira/common/utils.py index 57d08bb55af..07b456c1218 100644 --- a/neutron/plugins/nicira/common/utils.py +++ b/neutron/plugins/nicira/common/utils.py @@ -16,9 +16,19 @@ # under the License. from neutron.openstack.common import log +from neutron.version import version_info + LOG = log.getLogger(__name__) MAX_DISPLAY_NAME_LEN = 40 +NEUTRON_VERSION = version_info.release_string() + + +def get_tags(**kwargs): + tags = ([dict(tag=value, scope=key) + for key, value in kwargs.iteritems()]) + tags.append({"tag": NEUTRON_VERSION, "scope": "quantum"}) + return tags def check_and_truncate(display_name): diff --git a/neutron/plugins/nicira/dhcp_meta/nvp.py b/neutron/plugins/nicira/dhcp_meta/nvp.py new file mode 100644 index 00000000000..c4b046e8897 --- /dev/null +++ b/neutron/plugins/nicira/dhcp_meta/nvp.py @@ -0,0 +1,405 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 neutron.api.v2 import attributes as attr +from neutron.common import constants as const +from neutron.common import exceptions as n_exc +from neutron.db import db_base_plugin_v2 +from neutron.openstack.common import log as logging +from neutron.plugins.nicira.common import exceptions as p_exc +from neutron.plugins.nicira.nsxlib import lsn as lsn_api +from neutron.plugins.nicira import nvplib + + +LOG = logging.getLogger(__name__) + + +dhcp_opts = [ + cfg.ListOpt('extra_domain_name_servers', + default=[], + help=_('Comma separated list of additional ' + 'domain name servers')), + cfg.StrOpt('domain_name', + default='openstacklocal', + help=_('Domain to use for building the hostnames')), + cfg.IntOpt('default_lease_time', default=43200, + help=_("Default DHCP lease time")), +] + + +def register_dhcp_opts(config): + config.CONF.register_opts(dhcp_opts, "NVP_DHCP") + + +class LsnManager(object): + """Manage LSN entities associated with networks.""" + + def __init__(self, plugin): + self.plugin = plugin + + @property + def cluster(self): + return self.plugin.cluster + + def lsn_get(self, context, network_id, raise_on_err=True): + """Retrieve the LSN id associated to the network.""" + try: + return lsn_api.lsn_for_network_get(self.cluster, network_id) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + logger = raise_on_err and LOG.error or LOG.warn + logger(_('Unable to find Logical Service Node for ' + 'network %s'), network_id) + if raise_on_err: + raise p_exc.LsnNotFound(entity='network', + entity_id=network_id) + + def lsn_create(self, context, network_id): + """Create a LSN associated to the network.""" + try: + return lsn_api.lsn_for_network_create(self.cluster, network_id) + except nvplib.NvpApiClient.NvpApiException: + err_msg = _('Unable to create LSN for network %s') % network_id + raise p_exc.NvpPluginException(err_msg=err_msg) + + def lsn_delete(self, context, lsn_id): + """Delete a LSN given its id.""" + try: + lsn_api.lsn_delete(self.cluster, lsn_id) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + LOG.warn(_('Unable to delete Logical Service Node %s'), lsn_id) + + def lsn_delete_by_network(self, context, network_id): + """Delete a LSN associated to the network.""" + lsn_id = self.lsn_get(context, network_id, raise_on_err=False) + if lsn_id: + self.lsn_delete(context, lsn_id) + + def lsn_port_get(self, context, network_id, subnet_id, raise_on_err=True): + """Retrieve LSN and LSN port for the network and the subnet.""" + lsn_id = self.lsn_get(context, network_id, raise_on_err=raise_on_err) + if lsn_id: + try: + lsn_port_id = lsn_api.lsn_port_by_subnet_get( + self.cluster, lsn_id, subnet_id) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + logger = raise_on_err and LOG.error or LOG.warn + logger(_('Unable to find Logical Service Node Port for ' + 'LSN %(lsn_id)s and subnet %(subnet_id)s') + % {'lsn_id': lsn_id, 'subnet_id': subnet_id}) + if raise_on_err: + raise p_exc.LsnPortNotFound(lsn_id=lsn_id, + entity='subnet', + entity_id=subnet_id) + return (lsn_id, None) + else: + return (lsn_id, lsn_port_id) + else: + return (None, None) + + def lsn_port_get_by_mac(self, context, network_id, mac, raise_on_err=True): + """Retrieve LSN and LSN port given network and mac address.""" + lsn_id = self.lsn_get(context, network_id, raise_on_err=raise_on_err) + if lsn_id: + try: + lsn_port_id = lsn_api.lsn_port_by_mac_get( + self.cluster, lsn_id, mac) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + logger = raise_on_err and LOG.error or LOG.warn + logger(_('Unable to find Logical Service Node Port for ' + 'LSN %(lsn_id)s and mac address %(mac)s') + % {'lsn_id': lsn_id, 'mac': mac}) + if raise_on_err: + raise p_exc.LsnPortNotFound(lsn_id=lsn_id, + entity='MAC', + entity_id=mac) + return (lsn_id, None) + else: + return (lsn_id, lsn_port_id) + else: + return (None, None) + + def lsn_port_create(self, context, lsn_id, subnet_info): + """Create and return LSN port for associated subnet.""" + try: + return lsn_api.lsn_port_create(self.cluster, lsn_id, subnet_info) + except n_exc.NotFound: + raise p_exc.LsnNotFound(entity='', entity_id=lsn_id) + except nvplib.NvpApiClient.NvpApiException: + err_msg = _('Unable to create port for LSN %s') % lsn_id + raise p_exc.NvpPluginException(err_msg=err_msg) + + def lsn_port_delete(self, context, lsn_id, lsn_port_id): + """Delete a LSN port from the Logical Service Node.""" + try: + lsn_api.lsn_port_delete(self.cluster, lsn_id, lsn_port_id) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + LOG.warn(_('Unable to delete LSN Port %s'), lsn_port_id) + + def lsn_port_dispose(self, context, network_id, mac_address): + """Delete a LSN port given the network and the mac address.""" + # NOTE(armando-migliaccio): dispose and delete are functionally + # equivalent, but they use different paraments to identify LSN + # and LSN port resources. + lsn_id, lsn_port_id = self.lsn_port_get_by_mac( + context, network_id, mac_address, raise_on_err=False) + if lsn_port_id: + self.lsn_port_delete(context, lsn_id, lsn_port_id) + + def lsn_port_dhcp_setup( + self, context, network_id, port_id, port_data, subnet_config=None): + """Connect network to LSN via specified port and port_data.""" + try: + lsn_id = None + lswitch_port_id = nvplib.get_port_by_neutron_tag( + self.cluster, network_id, port_id)['uuid'] + lsn_id = self.lsn_get(context, network_id) + lsn_port_id = self.lsn_port_create(context, lsn_id, port_data) + except (n_exc.NotFound, p_exc.NvpPluginException): + raise p_exc.PortConfigurationError( + net_id=network_id, lsn_id=lsn_id, port_id=port_id) + try: + lsn_api.lsn_port_plug_network( + self.cluster, lsn_id, lsn_port_id, lswitch_port_id) + except p_exc.LsnConfigurationConflict: + self.lsn_port_delete(self.cluster, lsn_id, lsn_port_id) + raise p_exc.PortConfigurationError( + net_id=network_id, lsn_id=lsn_id, port_id=port_id) + if subnet_config: + self.lsn_port_dhcp_configure( + context, lsn_id, lsn_port_id, subnet_config) + else: + return (lsn_id, lsn_port_id) + + def lsn_port_dhcp_configure(self, context, lsn_id, lsn_port_id, subnet): + """Enable/disable dhcp services with the given config options.""" + is_enabled = subnet["enable_dhcp"] + dhcp_options = { + "domain_name": cfg.CONF.NVP_DHCP.domain_name, + "default_lease_time": cfg.CONF.NVP_DHCP.default_lease_time, + } + dns_servers = cfg.CONF.NVP_DHCP.extra_domain_name_servers + dns_servers.extend(subnet["dns_nameservers"]) + if subnet['gateway_ip']: + dhcp_options["routers"] = subnet["gateway_ip"] + if dns_servers: + dhcp_options["domain_name_servers"] = ",".join(dns_servers) + if subnet["host_routes"]: + dhcp_options["classless_static_routes"] = ( + ",".join(subnet["host_routes"]) + ) + try: + lsn_api.lsn_port_dhcp_configure( + self.cluster, lsn_id, lsn_port_id, is_enabled, dhcp_options) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + err_msg = (_('Unable to configure dhcp for Logical Service ' + 'Node %(lsn_id)s and port %(lsn_port_id)s') + % {'lsn_id': lsn_id, 'lsn_port_id': lsn_port_id}) + LOG.error(err_msg) + raise p_exc.NvpPluginException(err_msg=err_msg) + + def _lsn_port_host_conf(self, context, network_id, subnet_id, data, hdlr): + lsn_id = None + lsn_port_id = None + try: + lsn_id, lsn_port_id = self.lsn_port_get( + context, network_id, subnet_id) + hdlr(self.cluster, lsn_id, lsn_port_id, data) + except (n_exc.NotFound, nvplib.NvpApiClient.NvpApiException): + LOG.error(_('Error while configuring LSN ' + 'port %s'), lsn_port_id) + raise p_exc.PortConfigurationError( + net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id) + + def lsn_port_dhcp_host_add(self, context, network_id, subnet_id, host): + """Add dhcp host entry from LSN port configuration.""" + self._lsn_port_host_conf(context, network_id, subnet_id, host, + lsn_api.lsn_port_dhcp_host_add) + + def lsn_port_dhcp_host_remove(self, context, network_id, subnet_id, host): + """Remove dhcp host entry from LSN port configuration.""" + self._lsn_port_host_conf(context, network_id, subnet_id, host, + lsn_api.lsn_port_dhcp_host_remove) + + +class DhcpAgentNotifyAPI(object): + + def __init__(self, plugin, lsn_manager): + self.plugin = plugin + self.lsn_manager = lsn_manager + self._handle_subnet_dhcp_access = {'create': self._subnet_create, + 'update': self._subnet_update, + 'delete': self._subnet_delete} + + def notify(self, context, data, methodname): + [resource, action, _e] = methodname.split('.') + if resource == 'subnet': + self._handle_subnet_dhcp_access[action](context, data['subnet']) + + def _subnet_create(self, context, subnet, clean_on_err=True): + if subnet['enable_dhcp']: + network_id = subnet['network_id'] + # Create port for DHCP service + dhcp_port = { + "name": "", + "admin_state_up": True, + "device_id": "", + "device_owner": const.DEVICE_OWNER_DHCP, + "network_id": network_id, + "tenant_id": subnet["tenant_id"], + "mac_address": attr.ATTR_NOT_SPECIFIED, + "fixed_ips": [{"subnet_id": subnet['id']}] + } + try: + # This will end up calling handle_port_dhcp_access + # down below + self.plugin.create_port(context, {'port': dhcp_port}) + except p_exc.PortConfigurationError as e: + err_msg = (_("Error while creating subnet %(cidr)s for " + "network %(network)s. Please, contact " + "administrator") % + {"cidr": subnet["cidr"], + "network": network_id}) + LOG.error(err_msg) + db_base_plugin_v2.NeutronDbPluginV2.delete_port( + self.plugin, context, e.port_id) + if clean_on_err: + self.plugin.delete_subnet(context, subnet['id']) + raise n_exc.Conflict() + + def _subnet_update(self, context, subnet): + network_id = subnet['network_id'] + try: + lsn_id, lsn_port_id = self.lsn_manager.lsn_port_get( + context, network_id, subnet['id']) + self.lsn_manager.lsn_port_dhcp_configure( + context, lsn_id, lsn_port_id, subnet) + except p_exc.LsnPortNotFound: + # It's possible that the subnet was created with dhcp off; + # check that a dhcp port exists first and provision it + # accordingly + filters = dict(network_id=[network_id], + device_owner=[const.DEVICE_OWNER_DHCP]) + ports = self.plugin.get_ports(context, filters=filters) + if ports: + handle_port_dhcp_access( + self.plugin, context, ports[0], 'create_port') + else: + self._subnet_create(context, subnet, clean_on_err=False) + + def _subnet_delete(self, context, subnet): + # FIXME(armando-migliaccio): it looks like that a subnet filter + # is ineffective; so filter by network for now. + network_id = subnet['network_id'] + filters = dict(network_id=[network_id], + device_owner=[const.DEVICE_OWNER_DHCP]) + # FIXME(armando-migliaccio): this may be race-y + ports = self.plugin.get_ports(context, filters=filters) + if ports: + # This will end up calling handle_port_dhcp_access + # down below + self.plugin.delete_port(context, ports[0]['id']) + + +def check_services_requirements(cluster): + ver = cluster.api_client.get_nvp_version() + # It sounds like 4.1 is the first one where DHCP in NSX/NVP + # will have the experimental feature + if ver.major >= 4 and ver.minor >= 1: + cluster_id = cfg.CONF.default_service_cluster_uuid + if not lsn_api.service_cluster_exists(cluster, cluster_id): + raise p_exc.ServiceClusterUnavailable(cluster_id=cluster_id) + else: + raise p_exc.NvpInvalidVersion(version=ver) + + +def handle_network_dhcp_access(plugin, context, network, action): + LOG.info(_("Performing DHCP %(action)s for resource: %(resource)s") + % {"action": action, "resource": network}) + if action == 'create_network': + network_id = network['id'] + plugin.lsn_manager.lsn_create(context, network_id) + elif action == 'delete_network': + # NOTE(armando-migliaccio): on delete_network, network + # is just the network id + network_id = network + plugin.lsn_manager.lsn_delete_by_network(context, network_id) + LOG.info(_("Logical Services Node for network " + "%s configured successfully"), network_id) + + +def handle_port_dhcp_access(plugin, context, port, action): + LOG.info(_("Performing DHCP %(action)s for resource: %(resource)s") + % {"action": action, "resource": port}) + if port["device_owner"] == const.DEVICE_OWNER_DHCP: + network_id = port["network_id"] + if action == "create_port": + # at this point the port must have a subnet and a fixed ip + subnet_id = port["fixed_ips"][0]['subnet_id'] + subnet = plugin.get_subnet(context, subnet_id) + subnet_data = { + "mac_address": port["mac_address"], + "ip_address": subnet['cidr'], + "subnet_id": subnet['id'] + } + try: + plugin.lsn_manager.lsn_port_dhcp_setup( + context, network_id, port['id'], subnet_data, subnet) + except p_exc.PortConfigurationError: + err_msg = (_("Error while configuring DHCP for " + "port %s"), port['id']) + LOG.error(err_msg) + raise n_exc.NeutronException() + elif action == "delete_port": + plugin.lsn_manager.lsn_port_dispose(context, network_id, + port['mac_address']) + elif port["device_owner"] != const.DEVICE_OWNER_DHCP: + if port.get("fixed_ips"): + # do something only if there are IP's and dhcp is enabled + subnet_id = port["fixed_ips"][0]['subnet_id'] + if not plugin.get_subnet(context, subnet_id)['enable_dhcp']: + LOG.info(_("DHCP is disabled: nothing to do")) + return + host_data = { + "mac_address": port["mac_address"], + "ip_address": port["fixed_ips"][0]['ip_address'] + } + network_id = port["network_id"] + if action == "create_port": + handler = plugin.lsn_manager.lsn_port_dhcp_host_add + elif action == "delete_port": + handler = plugin.lsn_manager.lsn_port_dhcp_host_remove + try: + handler(context, network_id, subnet_id, host_data) + except p_exc.PortConfigurationError: + if action == 'create_port': + db_base_plugin_v2.NeutronDbPluginV2.delete_port( + plugin, context, port['id']) + raise + LOG.info(_("DHCP for port %s configured successfully"), port['id']) + + +def handle_port_metadata_access(context, port, is_delete=False): + # TODO(armando-migliaccio) + LOG.info('%s port with data %s' % (is_delete, port)) + + +def handle_router_metadata_access(plugin, context, router_id, do_create=True): + # TODO(armando-migliaccio) + LOG.info('%s router %s' % (do_create, router_id)) diff --git a/neutron/plugins/nicira/dhcp_meta/rpc.py b/neutron/plugins/nicira/dhcp_meta/rpc.py index 07930455f5c..4bf2561e4bb 100644 --- a/neutron/plugins/nicira/dhcp_meta/rpc.py +++ b/neutron/plugins/nicira/dhcp_meta/rpc.py @@ -225,7 +225,7 @@ def _destroy_metadata_access_network(plugin, context, router_id, ports): # must re-add the router interface plugin.add_router_interface(context, router_id, {'subnet_id': meta_sub_id}) - # Tell to stop the metadata agent proxy + # Tell to stop the metadata agent proxy _notify_rpc_agent( context, {'network': {'id': meta_net_id}}, 'network.delete.end') diff --git a/neutron/plugins/nicira/dhcpmeta_modes.py b/neutron/plugins/nicira/dhcpmeta_modes.py index 150d0feecd8..45a5a96d6ba 100644 --- a/neutron/plugins/nicira/dhcpmeta_modes.py +++ b/neutron/plugins/nicira/dhcpmeta_modes.py @@ -22,10 +22,15 @@ from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api from neutron.common import constants as const from neutron.common import topics from neutron.openstack.common import importutils +from neutron.openstack.common import log as logging from neutron.openstack.common import rpc from neutron.plugins.nicira.common import config +from neutron.plugins.nicira.common import exceptions as nvp_exc +from neutron.plugins.nicira.dhcp_meta import nvp as nvp_svc from neutron.plugins.nicira.dhcp_meta import rpc as nvp_rpc +LOG = logging.getLogger(__name__) + class DhcpMetadataAccess(object): @@ -33,30 +38,22 @@ class DhcpMetadataAccess(object): """Initialize support for DHCP and Metadata services.""" if cfg.CONF.NVP.agent_mode == config.AgentModes.AGENT: self._setup_rpc_dhcp_metadata() - self.handle_network_dhcp_access_delegate = ( - nvp_rpc.handle_network_dhcp_access - ) - self.handle_port_dhcp_access_delegate = ( - nvp_rpc.handle_port_dhcp_access - ) - self.handle_port_metadata_access_delegate = ( - nvp_rpc.handle_port_metadata_access - ) - self.handle_metadata_access_delegate = ( - nvp_rpc.handle_router_metadata_access - ) + mod = nvp_rpc elif cfg.CONF.NVP.agent_mode == config.AgentModes.AGENTLESS: - # In agentless mode the following extensions, and related - # operations, are not supported; so do not publish them - if "agent" in self.supported_extension_aliases: - self.supported_extension_aliases.remove("agent") - if "dhcp_agent_scheduler" in self.supported_extension_aliases: - self.supported_extension_aliases.remove( - "dhcp_agent_scheduler") - # TODO(armando-migliaccio): agentless support is not yet complete - # so it's better to raise an exception for now, in case some admin - # decides to jump the gun - raise NotImplementedError() + self._setup_nvp_dhcp_metadata() + mod = nvp_svc + self.handle_network_dhcp_access_delegate = ( + mod.handle_network_dhcp_access + ) + self.handle_port_dhcp_access_delegate = ( + mod.handle_port_dhcp_access + ) + self.handle_port_metadata_access_delegate = ( + mod.handle_port_metadata_access + ) + self.handle_metadata_access_delegate = ( + mod.handle_router_metadata_access + ) def _setup_rpc_dhcp_metadata(self): self.topic = topics.PLUGIN @@ -71,6 +68,36 @@ class DhcpMetadataAccess(object): cfg.CONF.network_scheduler_driver ) + def _setup_nvp_dhcp_metadata(self): + # In agentless mode the following extensions, and related + # operations, are not supported; so do not publish them + if "agent" in self.supported_extension_aliases: + self.supported_extension_aliases.remove("agent") + if "dhcp_agent_scheduler" in self.supported_extension_aliases: + self.supported_extension_aliases.remove( + "dhcp_agent_scheduler") + nvp_svc.register_dhcp_opts(cfg) + self.lsn_manager = nvp_svc.LsnManager(self) + self.agent_notifiers[const.AGENT_TYPE_DHCP] = ( + nvp_svc.DhcpAgentNotifyAPI(self, self.lsn_manager)) + # In agentless mode, ports whose owner is DHCP need to + # be special cased; so add it to the list of special + # owners list + if const.DEVICE_OWNER_DHCP not in self.port_special_owners: + self.port_special_owners.append(const.DEVICE_OWNER_DHCP) + try: + error = None + nvp_svc.check_services_requirements(self.cluster) + except nvp_exc.NvpInvalidVersion: + error = _("Unable to run Neutron with config option '%s', as NVP " + "does not support it") % config.AgentModes.AGENTLESS + except nvp_exc.ServiceClusterUnavailable: + error = _("Unmet dependency for config option " + "'%s'") % config.AgentModes.AGENTLESS + if error: + LOG.exception(error) + raise nvp_exc.NvpPluginException(err_msg=error) + def handle_network_dhcp_access(self, context, network, action): self.handle_network_dhcp_access_delegate(self, context, network, action) diff --git a/neutron/plugins/nicira/nsxlib/__init__.py b/neutron/plugins/nicira/nsxlib/__init__.py new file mode 100644 index 00000000000..c020e3bcdad --- /dev/null +++ b/neutron/plugins/nicira/nsxlib/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/neutron/plugins/nicira/nsxlib/lsn.py b/neutron/plugins/nicira/nsxlib/lsn.py new file mode 100644 index 00000000000..f10c59d4fce --- /dev/null +++ b/neutron/plugins/nicira/nsxlib/lsn.py @@ -0,0 +1,201 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. + +import json + +from neutron.common import exceptions as exception +from neutron.openstack.common import log +from neutron.plugins.nicira.common import exceptions as nvp_exc +from neutron.plugins.nicira.common import utils +from neutron.plugins.nicira import NvpApiClient +from neutron.plugins.nicira.nvplib import _build_uri_path +from neutron.plugins.nicira.nvplib import do_request + +HTTP_GET = "GET" +HTTP_POST = "POST" +HTTP_DELETE = "DELETE" +HTTP_PUT = "PUT" + +SERVICECLUSTER_RESOURCE = "service-cluster" +LSERVICESNODE_RESOURCE = "lservices-node" +LSERVICESNODEPORT_RESOURCE = "lport/%s" % LSERVICESNODE_RESOURCE + +LOG = log.getLogger(__name__) + + +def service_cluster_exists(cluster, svc_cluster_id): + exists = False + try: + exists = ( + svc_cluster_id and + do_request(HTTP_GET, + _build_uri_path(SERVICECLUSTER_RESOURCE, + resource_id=svc_cluster_id), + cluster=cluster) is not None) + except exception.NotFound: + pass + return exists + + +def lsn_for_network_create(cluster, network_id): + lsn_obj = { + "service_cluster_uuid": cluster.default_service_cluster_uuid, + "tags": utils.get_tags(n_network_id=network_id) + } + return do_request(HTTP_POST, + _build_uri_path(LSERVICESNODE_RESOURCE), + json.dumps(lsn_obj), + cluster=cluster)["uuid"] + + +def lsn_for_network_get(cluster, network_id): + filters = {"tag": network_id, "tag_scope": "n_network_id"} + results = do_request(HTTP_GET, + _build_uri_path(LSERVICESNODE_RESOURCE, + fields="uuid", + filters=filters), + cluster=cluster)['results'] + if not results: + raise exception.NotFound() + elif len(results) == 1: + return results[0]['uuid'] + + +def lsn_delete(cluster, lsn_id): + do_request(HTTP_DELETE, + _build_uri_path(LSERVICESNODE_RESOURCE, + resource_id=lsn_id), + cluster=cluster) + + +def lsn_port_create(cluster, lsn_id, port_data): + port_obj = { + "ip_address": port_data["ip_address"], + "mac_address": port_data["mac_address"], + "tags": utils.get_tags(n_mac_address=port_data["mac_address"], + n_subnet_id=port_data["subnet_id"]), + "type": "LogicalServicesNodePortConfig", + } + return do_request(HTTP_POST, + _build_uri_path(LSERVICESNODEPORT_RESOURCE, + parent_resource_id=lsn_id), + json.dumps(port_obj), + cluster=cluster)["uuid"] + + +def lsn_port_delete(cluster, lsn_id, lsn_port_id): + return do_request(HTTP_DELETE, + _build_uri_path(LSERVICESNODEPORT_RESOURCE, + parent_resource_id=lsn_id, + resource_id=lsn_port_id), + cluster=cluster) + + +def _lsn_port_get(cluster, lsn_id, filters): + results = do_request(HTTP_GET, + _build_uri_path(LSERVICESNODEPORT_RESOURCE, + parent_resource_id=lsn_id, + fields="uuid", + filters=filters), + cluster=cluster)['results'] + if not results: + raise exception.NotFound() + elif len(results) == 1: + return results[0]['uuid'] + + +def lsn_port_by_mac_get(cluster, lsn_id, mac_address): + filters = {"tag": mac_address, "tag_scope": "n_mac_address"} + return _lsn_port_get(cluster, lsn_id, filters) + + +def lsn_port_by_subnet_get(cluster, lsn_id, subnet_id): + filters = {"tag": subnet_id, "tag_scope": "n_subnet_id"} + return _lsn_port_get(cluster, lsn_id, filters) + + +def lsn_port_plug_network(cluster, lsn_id, lsn_port_id, lswitch_port_id): + patch_obj = { + "type": "PatchAttachment", + "peer_port_uuid": lswitch_port_id + } + try: + do_request(HTTP_PUT, + _build_uri_path(LSERVICESNODEPORT_RESOURCE, + parent_resource_id=lsn_id, + resource_id=lsn_port_id, + is_attachment=True), + json.dumps(patch_obj), + cluster=cluster) + except NvpApiClient.Conflict: + # This restriction might be lifted at some point + msg = (_("Attempt to plug Logical Services Node %(lsn)s into " + "network with port %(port)s failed. PatchAttachment " + "already exists with another port") % + {'lsn': lsn_id, 'port': lswitch_port_id}) + LOG.exception(msg) + raise nvp_exc.LsnConfigurationConflict(lsn_id=lsn_id) + + +def _lsn_port_configure_action( + cluster, lsn_id, lsn_port_id, action, is_enabled, obj): + do_request(HTTP_PUT, + _build_uri_path(LSERVICESNODE_RESOURCE, + resource_id=lsn_id, + extra_action=action), + json.dumps({"enabled": is_enabled}), + cluster=cluster) + do_request(HTTP_PUT, + _build_uri_path(LSERVICESNODEPORT_RESOURCE, + parent_resource_id=lsn_id, + resource_id=lsn_port_id, + extra_action=action), + json.dumps(obj), + cluster=cluster) + + +def lsn_port_dhcp_configure( + cluster, lsn_id, lsn_port_id, is_enabled=True, dhcp_options=None): + dhcp_options = dhcp_options or {} + opts = ["%s=%s" % (key, val) for key, val in dhcp_options.iteritems()] + dhcp_obj = { + 'options': {'options': opts} + } + _lsn_port_configure_action( + cluster, lsn_id, lsn_port_id, 'dhcp', is_enabled, dhcp_obj) + + +def _lsn_port_host_action( + cluster, lsn_id, lsn_port_id, host_obj, extra_action, action): + do_request(HTTP_POST, + _build_uri_path(LSERVICESNODEPORT_RESOURCE, + parent_resource_id=lsn_id, + resource_id=lsn_port_id, + extra_action=extra_action, + filters={"action": action}), + json.dumps(host_obj), + cluster=cluster) + + +def lsn_port_dhcp_host_add(cluster, lsn_id, lsn_port_id, host_data): + _lsn_port_host_action( + cluster, lsn_id, lsn_port_id, host_data, 'dhcp', 'add_host') + + +def lsn_port_dhcp_host_remove(cluster, lsn_id, lsn_port_id, host_data): + _lsn_port_host_action( + cluster, lsn_id, lsn_port_id, host_data, 'dhcp', 'remove_host') diff --git a/neutron/plugins/nicira/nvplib.py b/neutron/plugins/nicira/nvplib.py index d27f92839b4..5345b1166c9 100644 --- a/neutron/plugins/nicira/nvplib.py +++ b/neutron/plugins/nicira/nvplib.py @@ -122,7 +122,8 @@ def _build_uri_path(resource, relations=None, filters=None, types=None, - is_attachment=False): + is_attachment=False, + extra_action=None): resources = resource.split('/') res_path = resources[0] + (resource_id and "/%s" % resource_id or '') if len(resources) > 1: @@ -132,6 +133,8 @@ def _build_uri_path(resource, res_path) if is_attachment: res_path = "%s/attachment" % res_path + elif extra_action: + res_path = "%s/%s" % (res_path, extra_action) params = [] params.append(fields and "fields=%s" % fields) params.append(relations and "relations=%s" % relations) diff --git a/neutron/tests/unit/nicira/etc/nvp.ini.agentless.test b/neutron/tests/unit/nicira/etc/nvp.ini.agentless.test index 33550520ab4..3ade4e7381f 100644 --- a/neutron/tests/unit/nicira/etc/nvp.ini.agentless.test +++ b/neutron/tests/unit/nicira/etc/nvp.ini.agentless.test @@ -7,6 +7,7 @@ nvp_user = foo nvp_password = bar default_l3_gw_service_uuid = whatever default_l2_gw_service_uuid = whatever +default_service_cluster_uuid = whatever default_interface_name = whatever req_timeout = 14 http_timeout = 13 diff --git a/neutron/tests/unit/nicira/test_dhcpmeta.py b/neutron/tests/unit/nicira/test_dhcpmeta.py new file mode 100644 index 00000000000..7c4663757c9 --- /dev/null +++ b/neutron/tests/unit/nicira/test_dhcpmeta.py @@ -0,0 +1,633 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 + +from oslo.config import cfg + +from neutron.common import exceptions as n_exc +from neutron.plugins.nicira.common import exceptions as p_exc +from neutron.plugins.nicira.dhcp_meta import nvp +from neutron.plugins.nicira.NvpApiClient import NvpApiException +from neutron.tests import base + + +class LsnManagerTestCase(base.BaseTestCase): + + def setUp(self): + super(LsnManagerTestCase, self).setUp() + self.net_id = 'foo_network_id' + self.sub_id = 'foo_subnet_id' + self.port_id = 'foo_port_id' + self.lsn_id = 'foo_lsn_id' + self.mac = 'aa:bb:cc:dd:ee:ff' + self.lsn_port_id = 'foo_lsn_port_id' + self.manager = nvp.LsnManager(mock.Mock()) + self.mock_lsn_api_p = mock.patch.object(nvp, 'lsn_api') + self.mock_lsn_api = self.mock_lsn_api_p.start() + nvp.register_dhcp_opts(cfg) + self.addCleanup(cfg.CONF.reset) + self.addCleanup(self.mock_lsn_api_p.stop) + + def test_lsn_get(self): + self.mock_lsn_api.lsn_for_network_get.return_value = self.lsn_id + expected = self.manager.lsn_get(mock.ANY, self.net_id) + self.mock_lsn_api.lsn_for_network_get.assert_called_once_with( + mock.ANY, self.net_id) + self.assertEqual(expected, self.lsn_id) + + def _test_lsn_get_raise_not_found_with_exc(self, exc): + self.mock_lsn_api.lsn_for_network_get.side_effect = exc + self.assertRaises(p_exc.LsnNotFound, + self.manager.lsn_get, + mock.ANY, self.net_id) + self.mock_lsn_api.lsn_for_network_get.assert_called_once_with( + mock.ANY, self.net_id) + + def test_lsn_get_raise_not_found_with_not_found(self): + self._test_lsn_get_raise_not_found_with_exc(n_exc.NotFound) + + def test_lsn_get_raise_not_found_with_api_error(self): + self._test_lsn_get_raise_not_found_with_exc(NvpApiException) + + def _test_lsn_get_silent_raise_with_exc(self, exc): + self.mock_lsn_api.lsn_for_network_get.side_effect = exc + expected = self.manager.lsn_get( + mock.ANY, self.net_id, raise_on_err=False) + self.mock_lsn_api.lsn_for_network_get.assert_called_once_with( + mock.ANY, self.net_id) + self.assertIsNone(expected) + + def test_lsn_get_silent_raise_with_not_found(self): + self._test_lsn_get_silent_raise_with_exc(n_exc.NotFound) + + def test_lsn_get_silent_raise_with_api_error(self): + self._test_lsn_get_silent_raise_with_exc(NvpApiException) + + def test_lsn_create(self): + self.mock_lsn_api.lsn_for_network_create.return_value = self.lsn_id + self.manager.lsn_create(mock.ANY, self.net_id) + self.mock_lsn_api.lsn_for_network_create.assert_called_once_with( + mock.ANY, self.net_id) + + def test_lsn_create_raise_api_error(self): + self.mock_lsn_api.lsn_for_network_create.side_effect = NvpApiException + self.assertRaises(p_exc.NvpPluginException, + self.manager.lsn_create, + mock.ANY, self.net_id) + self.mock_lsn_api.lsn_for_network_create.assert_called_once_with( + mock.ANY, self.net_id) + + def test_lsn_delete(self): + self.manager.lsn_delete(mock.ANY, self.lsn_id) + self.mock_lsn_api.lsn_delete.assert_called_once_with( + mock.ANY, self.lsn_id) + + def _test_lsn_delete_with_exc(self, exc): + self.mock_lsn_api.lsn_delete.side_effect = exc + self.manager.lsn_delete(mock.ANY, self.lsn_id) + self.mock_lsn_api.lsn_delete.assert_called_once_with( + mock.ANY, self.lsn_id) + + def test_lsn_delete_with_not_found(self): + self._test_lsn_delete_with_exc(n_exc.NotFound) + + def test_lsn_delete_api_exception(self): + self._test_lsn_delete_with_exc(NvpApiException) + + def test_lsn_delete_by_network(self): + self.mock_lsn_api.lsn_for_network_get.return_value = self.lsn_id + with mock.patch.object(self.manager, 'lsn_delete') as f: + self.manager.lsn_delete_by_network(mock.ANY, self.net_id) + self.mock_lsn_api.lsn_for_network_get.assert_called_once_with( + mock.ANY, self.net_id) + f.assert_called_once_with(mock.ANY, self.lsn_id) + + def _test_lsn_delete_by_network_with_exc(self, exc): + self.mock_lsn_api.lsn_for_network_get.side_effect = exc + with mock.patch.object(nvp.LOG, 'warn') as l: + self.manager.lsn_delete_by_network(mock.ANY, self.net_id) + self.assertEqual(1, l.call_count) + + def test_lsn_delete_by_network_with_not_found(self): + self._test_lsn_delete_by_network_with_exc(n_exc.NotFound) + + def test_lsn_delete_by_network_with_not_api_error(self): + self._test_lsn_delete_by_network_with_exc(NvpApiException) + + def test_lsn_port_get(self): + self.mock_lsn_api.lsn_port_by_subnet_get.return_value = ( + self.lsn_port_id) + with mock.patch.object( + self.manager, 'lsn_get', return_value=self.lsn_id): + expected = self.manager.lsn_port_get( + mock.ANY, self.net_id, self.sub_id) + self.assertEqual(expected, (self.lsn_id, self.lsn_port_id)) + + def test_lsn_port_get_lsn_not_found_on_raise(self): + with mock.patch.object( + self.manager, 'lsn_get', + side_effect=p_exc.LsnNotFound(entity='network', + entity_id=self.net_id)): + self.assertRaises(p_exc.LsnNotFound, + self.manager.lsn_port_get, + mock.ANY, self.net_id, self.sub_id) + + def test_lsn_port_get_lsn_not_found_silent_raise(self): + with mock.patch.object(self.manager, 'lsn_get', return_value=None): + expected = self.manager.lsn_port_get( + mock.ANY, self.net_id, self.sub_id, raise_on_err=False) + self.assertEqual(expected, (None, None)) + + def test_lsn_port_get_port_not_found_on_raise(self): + self.mock_lsn_api.lsn_port_by_subnet_get.side_effect = n_exc.NotFound + with mock.patch.object( + self.manager, 'lsn_get', return_value=self.lsn_id): + self.assertRaises(p_exc.LsnPortNotFound, + self.manager.lsn_port_get, + mock.ANY, self.net_id, self.sub_id) + + def test_lsn_port_get_port_not_found_silent_raise(self): + self.mock_lsn_api.lsn_port_by_subnet_get.side_effect = n_exc.NotFound + with mock.patch.object( + self.manager, 'lsn_get', return_value=self.lsn_id): + expected = self.manager.lsn_port_get( + mock.ANY, self.net_id, self.sub_id, raise_on_err=False) + self.assertEqual(expected, (self.lsn_id, None)) + + def test_lsn_port_create(self): + self.mock_lsn_api.lsn_port_create.return_value = self.lsn_port_id + expected = self.manager.lsn_port_create(mock.ANY, mock.ANY, mock.ANY) + self.assertEqual(expected, self.lsn_port_id) + + def _test_lsn_port_create_with_exc(self, exc, expected): + self.mock_lsn_api.lsn_port_create.side_effect = exc + self.assertRaises(expected, + self.manager.lsn_port_create, + mock.ANY, mock.ANY, mock.ANY) + + def test_lsn_port_create_with_not_found(self): + self._test_lsn_port_create_with_exc(n_exc.NotFound, p_exc.LsnNotFound) + + def test_lsn_port_create_api_exception(self): + self._test_lsn_port_create_with_exc(NvpApiException, + p_exc.NvpPluginException) + + def test_lsn_port_delete(self): + self.manager.lsn_port_delete(mock.ANY, mock.ANY, mock.ANY) + self.assertEqual(1, self.mock_lsn_api.lsn_port_delete.call_count) + + def _test_lsn_port_delete_with_exc(self, exc): + self.mock_lsn_api.lsn_port_delete.side_effect = exc + with mock.patch.object(nvp.LOG, 'warn') as l: + self.manager.lsn_port_delete(mock.ANY, mock.ANY, mock.ANY) + self.assertEqual(1, self.mock_lsn_api.lsn_port_delete.call_count) + self.assertEqual(1, l.call_count) + + def test_lsn_port_delete_with_not_found(self): + self._test_lsn_port_delete_with_exc(n_exc.NotFound) + + def test_lsn_port_delete_api_exception(self): + self._test_lsn_port_delete_with_exc(NvpApiException) + + def _test_lsn_port_dhcp_setup(self, ret_val, sub): + self.mock_lsn_api.lsn_port_create.return_value = self.lsn_port_id + with mock.patch.object( + self.manager, 'lsn_get', return_value=self.lsn_id): + with mock.patch.object(nvp.nvplib, 'get_port_by_neutron_tag'): + expected = self.manager.lsn_port_dhcp_setup( + mock.ANY, mock.ANY, mock.ANY, mock.ANY, subnet_config=sub) + self.assertEqual( + 1, self.mock_lsn_api.lsn_port_create.call_count) + self.assertEqual( + 1, self.mock_lsn_api.lsn_port_plug_network.call_count) + self.assertEqual(expected, ret_val) + + def test_lsn_port_dhcp_setup(self): + self._test_lsn_port_dhcp_setup((self.lsn_id, self.lsn_port_id), None) + + def test_lsn_port_dhcp_setup_with_config(self): + with mock.patch.object(self.manager, 'lsn_port_dhcp_configure') as f: + self._test_lsn_port_dhcp_setup(None, mock.ANY) + self.assertEqual(1, f.call_count) + + def test_lsn_port_dhcp_setup_with_not_found(self): + with mock.patch.object(nvp.nvplib, 'get_port_by_neutron_tag') as f: + f.side_effect = n_exc.NotFound + self.assertRaises(p_exc.PortConfigurationError, + self.manager.lsn_port_dhcp_setup, + mock.ANY, mock.ANY, mock.ANY, mock.ANY) + + def test_lsn_port_dhcp_setup_with_conflict(self): + self.mock_lsn_api.lsn_port_plug_network.side_effect = ( + p_exc.LsnConfigurationConflict(lsn_id=self.lsn_id)) + with mock.patch.object(nvp.nvplib, 'get_port_by_neutron_tag'): + with mock.patch.object(self.manager, 'lsn_port_delete') as g: + self.assertRaises(p_exc.PortConfigurationError, + self.manager.lsn_port_dhcp_setup, + mock.ANY, mock.ANY, mock.ANY, mock.ANY) + self.assertEqual(1, g.call_count) + + def _test_lsn_port_dhcp_configure_with_subnet( + self, expected, dns=None, gw=None, routes=None): + subnet = { + 'enable_dhcp': True, + 'dns_nameservers': dns or [], + 'gateway_ip': gw, + 'host_routes': routes + } + self.manager.lsn_port_dhcp_configure(mock.ANY, self.lsn_id, + self.lsn_port_id, subnet) + self.mock_lsn_api.lsn_port_dhcp_configure.assert_called_once_with( + mock.ANY, self.lsn_id, self.lsn_port_id, subnet['enable_dhcp'], + expected) + + def test_lsn_port_dhcp_configure(self): + expected = { + 'routers': '127.0.0.1', + 'default_lease_time': cfg.CONF.NVP_DHCP.default_lease_time, + 'domain_name': cfg.CONF.NVP_DHCP.domain_name + } + self._test_lsn_port_dhcp_configure_with_subnet( + expected, dns=[], gw='127.0.0.1', routes=[]) + + def test_lsn_port_dhcp_configure_gatewayless(self): + expected = { + 'default_lease_time': cfg.CONF.NVP_DHCP.default_lease_time, + 'domain_name': cfg.CONF.NVP_DHCP.domain_name + } + self._test_lsn_port_dhcp_configure_with_subnet(expected, gw=None) + + def test_lsn_port_dhcp_configure_with_extra_dns_servers(self): + expected = { + 'default_lease_time': cfg.CONF.NVP_DHCP.default_lease_time, + 'domain_name_servers': '8.8.8.8,9.9.9.9', + 'domain_name': cfg.CONF.NVP_DHCP.domain_name + } + self._test_lsn_port_dhcp_configure_with_subnet( + expected, dns=['8.8.8.8', '9.9.9.9']) + + def test_lsn_port_dhcp_configure_with_host_routes(self): + expected = { + 'default_lease_time': cfg.CONF.NVP_DHCP.default_lease_time, + 'domain_name': cfg.CONF.NVP_DHCP.domain_name, + 'classless_static_routes': '8.8.8.8,9.9.9.9' + } + self._test_lsn_port_dhcp_configure_with_subnet( + expected, routes=['8.8.8.8', '9.9.9.9']) + + def _test_lsn_port_dispose_with_values(self, lsn_id, lsn_port_id, count): + with mock.patch.object(self.manager, + 'lsn_port_get_by_mac', + return_value=(lsn_id, lsn_port_id)): + self.manager.lsn_port_dispose(mock.ANY, self.net_id, self.mac) + self.assertEqual(count, + self.mock_lsn_api.lsn_port_delete.call_count) + + def test_lsn_port_dispose(self): + self._test_lsn_port_dispose_with_values( + self.lsn_id, self.lsn_port_id, 1) + + def test_lsn_port_dispose_lsn_not_found(self): + self._test_lsn_port_dispose_with_values(None, None, 0) + + def test_lsn_port_dispose_lsn_port_not_found(self): + self._test_lsn_port_dispose_with_values(self.lsn_id, None, 0) + + def test_lsn_port_dispose_api_error(self): + self.mock_lsn_api.lsn_port_delete.side_effect = NvpApiException + with mock.patch.object(nvp.LOG, 'warn') as l: + self.manager.lsn_port_dispose(mock.ANY, self.net_id, self.mac) + self.assertEqual(1, l.call_count) + + def test_lsn_port_host_conf(self): + with mock.patch.object(self.manager, + 'lsn_port_get', + return_value=(self.lsn_id, self.lsn_port_id)): + f = mock.Mock() + self.manager._lsn_port_host_conf(mock.ANY, self.net_id, + self.sub_id, mock.ANY, f) + self.assertEqual(1, f.call_count) + + def test_lsn_port_host_conf_lsn_port_not_found(self): + with mock.patch.object( + self.manager, + 'lsn_port_get', + side_effect=p_exc.LsnPortNotFound(lsn_id=self.lsn_id, + entity='subnet', + entity_id=self.sub_id)): + self.assertRaises(p_exc.PortConfigurationError, + self.manager._lsn_port_host_conf, mock.ANY, + self.net_id, self.sub_id, mock.ANY, mock.Mock()) + + +class DhcpAgentNotifyAPITestCase(base.BaseTestCase): + + def setUp(self): + super(DhcpAgentNotifyAPITestCase, self).setUp() + self.notifier = nvp.DhcpAgentNotifyAPI(mock.Mock(), mock.Mock()) + self.plugin = self.notifier.plugin + self.lsn_manager = self.notifier.lsn_manager + + def _test_notify_subnet_action(self, action): + with mock.patch.object(self.notifier, '_subnet_%s' % action) as f: + self.notifier._handle_subnet_dhcp_access[action] = f + subnet = {'subnet': mock.ANY} + self.notifier.notify( + mock.ANY, subnet, 'subnet.%s.end' % action) + f.assert_called_once_with(mock.ANY, subnet) + + def test_notify_subnet_create(self): + self._test_notify_subnet_action('create') + + def test_notify_subnet_update(self): + self._test_notify_subnet_action('update') + + def test_notify_subnet_delete(self): + self._test_notify_subnet_action('delete') + + def _test_subnet_create(self, enable_dhcp, exc=None, + exc_obj=None, call_notify=True): + subnet = { + 'id': 'foo_subnet_id', + 'enable_dhcp': enable_dhcp, + 'network_id': 'foo_network_id', + 'tenant_id': 'foo_tenant_id', + 'cidr': '0.0.0.0/0' + } + if exc: + self.plugin.create_port.side_effect = exc_obj or exc + self.assertRaises(exc, + self.notifier.notify, + mock.ANY, + {'subnet': subnet}, + 'subnet.create.end') + self.plugin.delete_subnet.assert_called_with( + mock.ANY, subnet['id']) + else: + if call_notify: + self.notifier.notify( + mock.ANY, {'subnet': subnet}, 'subnet.create.end') + if enable_dhcp: + dhcp_port = { + 'name': '', + 'admin_state_up': True, + 'network_id': 'foo_network_id', + 'tenant_id': 'foo_tenant_id', + 'device_owner': 'network:dhcp', + 'mac_address': mock.ANY, + 'fixed_ips': [{'subnet_id': 'foo_subnet_id'}], + 'device_id': '' + } + self.plugin.create_port.assert_called_once_with( + mock.ANY, {'port': dhcp_port}) + else: + self.assertEqual(0, self.plugin.create_port.call_count) + + def test_subnet_create_enabled_dhcp(self): + self._test_subnet_create(True) + + def test_subnet_create_disabled_dhcp(self): + self._test_subnet_create(False) + + def test_subnet_create_raise_port_config_error(self): + with mock.patch.object(nvp.db_base_plugin_v2.NeutronDbPluginV2, + 'delete_port') as d: + self._test_subnet_create( + True, + exc=n_exc.Conflict, + exc_obj=p_exc.PortConfigurationError(lsn_id='foo_lsn_id', + net_id='foo_net_id', + port_id='foo_port_id')) + d.assert_called_once_with(self.plugin, mock.ANY, 'foo_port_id') + + def test_subnet_update(self): + subnet = { + 'id': 'foo_subnet_id', + 'network_id': 'foo_network_id', + } + self.lsn_manager.lsn_port_get.return_value = ('foo_lsn_id', + 'foo_lsn_port_id') + self.notifier.notify( + mock.ANY, {'subnet': subnet}, 'subnet.update.end') + self.lsn_manager.lsn_port_dhcp_configure.assert_called_once_with( + mock.ANY, 'foo_lsn_id', 'foo_lsn_port_id', subnet) + + def test_subnet_update_raise_lsn_not_found(self): + subnet = { + 'id': 'foo_subnet_id', + 'network_id': 'foo_network_id', + } + self.lsn_manager.lsn_port_get.side_effect = ( + p_exc.LsnNotFound(entity='network', + entity_id=subnet['network_id'])) + self.assertRaises(p_exc.LsnNotFound, + self.notifier.notify, + mock.ANY, {'subnet': subnet}, 'subnet.update.end') + + def _test_subnet_update_lsn_port_not_found(self, dhcp_port): + subnet = { + 'id': 'foo_subnet_id', + 'enable_dhcp': True, + 'network_id': 'foo_network_id', + 'tenant_id': 'foo_tenant_id' + } + self.lsn_manager.lsn_port_get.side_effect = ( + p_exc.LsnPortNotFound(lsn_id='foo_lsn_id', + entity='subnet', + entity_id=subnet['id'])) + self.notifier.plugin.get_ports.return_value = dhcp_port + count = 0 if dhcp_port is None else 1 + with mock.patch.object(nvp, 'handle_port_dhcp_access') as h: + self.notifier.notify( + mock.ANY, {'subnet': subnet}, 'subnet.update.end') + self.assertEqual(count, h.call_count) + if not dhcp_port: + self._test_subnet_create(enable_dhcp=True, + exc=None, call_notify=False) + + def test_subnet_update_lsn_port_not_found_without_dhcp_port(self): + self._test_subnet_update_lsn_port_not_found(None) + + def test_subnet_update_lsn_port_not_found_with_dhcp_port(self): + self._test_subnet_update_lsn_port_not_found([mock.ANY]) + + def _test_subnet_delete(self, ports=None): + subnet = { + 'id': 'foo_subnet_id', + 'network_id': 'foo_network_id', + 'cidr': '0.0.0.0/0' + } + self.plugin.get_ports.return_value = ports + self.notifier.notify(mock.ANY, {'subnet': subnet}, 'subnet.delete.end') + filters = { + 'network_id': [subnet['network_id']], + 'device_owner': ['network:dhcp'] + } + self.plugin.get_ports.assert_called_once_with( + mock.ANY, filters=filters) + if ports: + self.plugin.delete_port.assert_called_once_with( + mock.ANY, ports[0]['id']) + else: + self.assertEqual(0, self.plugin.delete_port.call_count) + + def test_subnet_delete_enabled_dhcp_no_ports(self): + self._test_subnet_delete() + + def test_subnet_delete_enabled_dhcp_with_dhcp_port(self): + self._test_subnet_delete([{'id': 'foo_port_id'}]) + + +class DhcpTestCase(base.BaseTestCase): + + def setUp(self): + super(DhcpTestCase, self).setUp() + self.plugin = mock.Mock() + self.plugin.lsn_manager = mock.Mock() + + def test_handle_create_network(self): + network = {'id': 'foo_network_id'} + nvp.handle_network_dhcp_access( + self.plugin, mock.ANY, network, 'create_network') + self.plugin.lsn_manager.lsn_create.assert_called_once_with( + mock.ANY, network['id']) + + def test_handle_delete_network(self): + network_id = 'foo_network_id' + self.plugin.lsn_manager.lsn_delete_by_network.return_value = ( + 'foo_lsn_id') + nvp.handle_network_dhcp_access( + self.plugin, mock.ANY, network_id, 'delete_network') + self.plugin.lsn_manager.lsn_delete_by_network.assert_called_once_with( + mock.ANY, 'foo_network_id') + + def _test_handle_create_dhcp_owner_port(self, exc=None): + subnet = { + 'cidr': '0.0.0.0/0', + 'id': 'foo_subnet_id' + } + port = { + 'id': 'foo_port_id', + 'device_owner': 'network:dhcp', + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'network_id': 'foo_network_id', + 'fixed_ips': [{'subnet_id': subnet['id']}] + } + expected_data = { + 'subnet_id': subnet['id'], + 'ip_address': subnet['cidr'], + 'mac_address': port['mac_address'] + } + self.plugin.get_subnet.return_value = subnet + if exc is None: + nvp.handle_port_dhcp_access( + self.plugin, mock.ANY, port, 'create_port') + (self.plugin.lsn_manager.lsn_port_dhcp_setup. + assert_called_once_with(mock.ANY, port['network_id'], + port['id'], expected_data, subnet)) + else: + self.plugin.lsn_manager.lsn_port_dhcp_setup.side_effect = exc + self.assertRaises(n_exc.NeutronException, + nvp.handle_port_dhcp_access, + self.plugin, mock.ANY, port, 'create_port') + + def test_handle_create_dhcp_owner_port(self): + self._test_handle_create_dhcp_owner_port() + + def test_handle_create_dhcp_owner_port_raise_port_config_error(self): + config_error = p_exc.PortConfigurationError(lsn_id='foo_lsn_id', + net_id='foo_net_id', + port_id='foo_port_id') + self._test_handle_create_dhcp_owner_port(exc=config_error) + + def test_handle_delete_dhcp_owner_port(self): + port = { + 'id': 'foo_port_id', + 'device_owner': 'network:dhcp', + 'network_id': 'foo_network_id', + 'fixed_ips': [], + 'mac_address': 'aa:bb:cc:dd:ee:ff' + } + nvp.handle_port_dhcp_access(self.plugin, mock.ANY, port, 'delete_port') + self.plugin.lsn_manager.lsn_port_dispose.assert_called_once_with( + mock.ANY, port['network_id'], port['mac_address']) + + def _test_handle_user_port(self, action, handler): + port = { + 'id': 'foo_port_id', + 'device_owner': 'foo_device_owner', + 'network_id': 'foo_network_id', + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'fixed_ips': [{'subnet_id': 'foo_subnet_id', + 'ip_address': '1.2.3.4'}] + } + expected_data = { + 'ip_address': '1.2.3.4', + 'mac_address': 'aa:bb:cc:dd:ee:ff' + } + self.plugin.get_subnet.return_value = {'enable_dhcp': True} + nvp.handle_port_dhcp_access(self.plugin, mock.ANY, port, action) + handler.assert_called_once_with( + mock.ANY, port['network_id'], 'foo_subnet_id', expected_data) + + def test_handle_create_user_port(self): + self._test_handle_user_port( + 'create_port', self.plugin.lsn_manager.lsn_port_dhcp_host_add) + + def test_handle_delete_user_port(self): + self._test_handle_user_port( + 'delete_port', self.plugin.lsn_manager.lsn_port_dhcp_host_remove) + + def _test_handle_user_port_disabled_dhcp(self, action, handler): + port = { + 'id': 'foo_port_id', + 'device_owner': 'foo_device_owner', + 'network_id': 'foo_network_id', + 'mac_address': 'aa:bb:cc:dd:ee:ff', + 'fixed_ips': [{'subnet_id': 'foo_subnet_id', + 'ip_address': '1.2.3.4'}] + } + self.plugin.get_subnet.return_value = {'enable_dhcp': False} + nvp.handle_port_dhcp_access(self.plugin, mock.ANY, port, action) + self.assertEqual(0, handler.call_count) + + def test_handle_create_user_port_disabled_dhcp(self): + self._test_handle_user_port_disabled_dhcp( + 'create_port', self.plugin.lsn_manager.lsn_port_dhcp_host_add) + + def test_handle_delete_user_port_disabled_dhcp(self): + self._test_handle_user_port_disabled_dhcp( + 'delete_port', self.plugin.lsn_manager.lsn_port_dhcp_host_remove) + + def _test_handle_user_port_no_fixed_ips(self, action, handler): + port = { + 'id': 'foo_port_id', + 'device_owner': 'foo_device_owner', + 'network_id': 'foo_network_id', + 'fixed_ips': [] + } + nvp.handle_port_dhcp_access(self.plugin, mock.ANY, port, action) + self.assertEqual(0, handler.call_count) + + def test_handle_create_user_port_no_fixed_ips(self): + self._test_handle_user_port_no_fixed_ips( + 'create_port', self.plugin.lsn_manager.lsn_port_dhcp_host_add) + + def test_handle_delete_user_port_no_fixed_ips(self): + self._test_handle_user_port_no_fixed_ips( + 'delete_port', self.plugin.lsn_manager.lsn_port_dhcp_host_remove) diff --git a/neutron/tests/unit/nicira/test_lsn_lib.py b/neutron/tests/unit/nicira/test_lsn_lib.py new file mode 100644 index 00000000000..88e3fcb9544 --- /dev/null +++ b/neutron/tests/unit/nicira/test_lsn_lib.py @@ -0,0 +1,258 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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 json +import mock + +from neutron.common import exceptions +from neutron.plugins.nicira.common import exceptions as nvp_exc +from neutron.plugins.nicira.common import utils +from neutron.plugins.nicira.nsxlib import lsn as lsnlib +from neutron.plugins.nicira import NvpApiClient +from neutron.tests import base + + +class LSNTestCase(base.BaseTestCase): + + def setUp(self): + super(LSNTestCase, self).setUp() + self.mock_request_p = mock.patch.object(lsnlib, 'do_request') + self.mock_request = self.mock_request_p.start() + self.cluster = mock.Mock() + self.cluster.default_service_cluster_uuid = 'foo' + self.addCleanup(self.mock_request_p.stop) + + def test_service_cluster_None(self): + self.mock_request.return_value = None + expected = lsnlib.service_cluster_exists(None, None) + self.assertFalse(expected) + + def test_service_cluster_found(self): + self.mock_request.return_value = { + "results": [ + { + "_href": "/ws.v1/service-cluster/foo_uuid", + "display_name": "foo_name", + "uuid": "foo_uuid", + "tags": [], + "_schema": "/ws.v1/schema/ServiceClusterConfig", + "gateways": [] + } + ], + "result_count": 1 + } + expected = lsnlib.service_cluster_exists(None, 'foo_uuid') + self.assertTrue(expected) + + def test_service_cluster_not_found(self): + self.mock_request.side_effect = exceptions.NotFound() + expected = lsnlib.service_cluster_exists(None, 'foo_uuid') + self.assertFalse(expected) + + def test_lsn_for_network_create(self): + net_id = "foo_network_id" + tags = utils.get_tags(n_network_id=net_id) + obj = {"service_cluster_uuid": "foo", "tags": tags} + lsnlib.lsn_for_network_create(self.cluster, net_id) + self.mock_request.assert_called_once_with( + "POST", "/ws.v1/lservices-node", + json.dumps(obj), cluster=self.cluster) + + def test_lsn_for_network_get(self): + net_id = "foo_network_id" + lsn_id = "foo_lsn_id" + self.mock_request.return_value = { + "results": [{"uuid": "foo_lsn_id"}], + "result_count": 1 + } + result = lsnlib.lsn_for_network_get(self.cluster, net_id) + self.assertEqual(lsn_id, result) + self.mock_request.assert_called_once_with( + "GET", + ("/ws.v1/lservices-node?fields=uuid&tag_scope=" + "n_network_id&tag=%s" % net_id), + cluster=self.cluster) + + def test_lsn_for_network_get_none(self): + net_id = "foo_network_id" + self.mock_request.return_value = { + "results": [{"uuid": "foo_lsn_id1"}, {"uuid": "foo_lsn_id2"}], + "result_count": 2 + } + result = lsnlib.lsn_for_network_get(self.cluster, net_id) + self.assertIsNone(result) + + def test_lsn_for_network_get_raise_not_found(self): + net_id = "foo_network_id" + self.mock_request.return_value = { + "results": [], "result_count": 0 + } + self.assertRaises(exceptions.NotFound, + lsnlib.lsn_for_network_get, + self.cluster, net_id) + + def test_lsn_delete(self): + lsn_id = "foo_id" + lsnlib.lsn_delete(self.cluster, lsn_id) + self.mock_request.assert_called_once_with( + "DELETE", + "/ws.v1/lservices-node/%s" % lsn_id, cluster=self.cluster) + + def test_lsn_port_create(self): + port_data = { + "ip_address": "1.2.3.0/24", + "mac_address": "aa:bb:cc:dd:ee:ff", + "subnet_id": "foo_subnet_id" + } + port_id = "foo_port_id" + self.mock_request.return_value = {"uuid": port_id} + lsn_id = "foo_lsn_id" + result = lsnlib.lsn_port_create(self.cluster, lsn_id, port_data) + self.assertEqual(result, port_id) + tags = utils.get_tags(n_subnet_id=port_data["subnet_id"], + n_mac_address=port_data["mac_address"]) + port_obj = { + "ip_address": port_data["ip_address"], + "mac_address": port_data["mac_address"], + "type": "LogicalServicesNodePortConfig", + "tags": tags + } + self.mock_request.assert_called_once_with( + "POST", "/ws.v1/lservices-node/%s/lport" % lsn_id, + json.dumps(port_obj), cluster=self.cluster) + + def test_lsn_port_delete(self): + lsn_id = "foo_lsn_id" + lsn_port_id = "foo_port_id" + lsnlib.lsn_port_delete(self.cluster, lsn_id, lsn_port_id) + self.mock_request.assert_called_once_with( + "DELETE", + "/ws.v1/lservices-node/%s/lport/%s" % (lsn_id, lsn_port_id), + cluster=self.cluster) + + def test_lsn_port_get_with_filters(self): + lsn_id = "foo_lsn_id" + port_id = "foo_port_id" + filters = {"tag": "foo_tag", "tag_scope": "foo_scope"} + self.mock_request.return_value = { + "results": [{"uuid": port_id}], + "result_count": 1 + } + result = lsnlib._lsn_port_get(self.cluster, lsn_id, filters) + self.assertEqual(result, port_id) + self.mock_request.assert_called_once_with( + "GET", + ("/ws.v1/lservices-node/%s/lport?fields=uuid&tag_scope=%s&" + "tag=%s" % (lsn_id, filters["tag_scope"], filters["tag"])), + cluster=self.cluster) + + def test_lsn_port_get_with_filters_return_none(self): + self.mock_request.return_value = { + "results": [{"uuid": "foo1"}, {"uuid": "foo2"}], + "result_count": 2 + } + result = lsnlib._lsn_port_get(self.cluster, "lsn_id", None) + self.assertIsNone(result) + + def test_lsn_port_get_with_filters_raises_not_found(self): + self.mock_request.return_value = {"results": [], "result_count": 0} + self.assertRaises(exceptions.NotFound, + lsnlib._lsn_port_get, + self.cluster, "lsn_id", None) + + def test_lsn_port_plug_network(self): + lsn_id = "foo_lsn_id" + lsn_port_id = "foo_lsn_port_id" + lswitch_port_id = "foo_lswitch_port_id" + lsnlib.lsn_port_plug_network( + self.cluster, lsn_id, lsn_port_id, lswitch_port_id) + self.mock_request.assert_called_once_with( + "PUT", + ("/ws.v1/lservices-node/%s/lport/%s/" + "attachment") % (lsn_id, lsn_port_id), + json.dumps({"peer_port_uuid": lswitch_port_id, + "type": "PatchAttachment"}), + cluster=self.cluster) + + def test_lsn_port_plug_network_raise_conflict(self): + lsn_id = "foo_lsn_id" + lsn_port_id = "foo_lsn_port_id" + lswitch_port_id = "foo_lswitch_port_id" + self.mock_request.side_effect = NvpApiClient.Conflict + self.assertRaises( + nvp_exc.LsnConfigurationConflict, + lsnlib.lsn_port_plug_network, + self.cluster, lsn_id, lsn_port_id, lswitch_port_id) + + def _test_lsn_port_dhcp_configure( + self, lsn_id, lsn_port_id, is_enabled, opts): + lsnlib.lsn_port_dhcp_configure( + self.cluster, lsn_id, lsn_port_id, is_enabled, opts) + opt_array = ["%s=%s" % (key, val) for key, val in opts.iteritems()] + self.mock_request.assert_has_calls([ + mock.call("PUT", "/ws.v1/lservices-node/%s/dhcp" % lsn_id, + json.dumps({"enabled": is_enabled}), + cluster=self.cluster), + mock.call("PUT", + ("/ws.v1/lservices-node/%s/" + "lport/%s/dhcp") % (lsn_id, lsn_port_id), + json.dumps({"options": {"options": opt_array}}), + cluster=self.cluster) + ]) + + def test_lsn_port_dhcp_configure_empty_opts(self): + lsn_id = "foo_lsn_id" + lsn_port_id = "foo_lsn_port_id" + is_enabled = False + opts = {} + self._test_lsn_port_dhcp_configure( + lsn_id, lsn_port_id, is_enabled, opts) + + def test_lsn_port_dhcp_configure_with_opts(self): + lsn_id = "foo_lsn_id" + lsn_port_id = "foo_lsn_port_id" + is_enabled = True + opts = {"opt1": "val1", "opt2": "val2"} + self._test_lsn_port_dhcp_configure( + lsn_id, lsn_port_id, is_enabled, opts) + + def _test_lsn_port_host_action( + self, lsn_port_action_func, extra_action, action, host): + lsn_id = "foo_lsn_id" + lsn_port_id = "foo_lsn_port_id" + lsn_port_action_func(self.cluster, lsn_id, lsn_port_id, host) + self.mock_request.assert_called_once_with( + "POST", + ("/ws.v1/lservices-node/%s/lport/" + "%s/%s?action=%s") % (lsn_id, lsn_port_id, extra_action, action), + json.dumps(host), cluster=self.cluster) + + def test_lsn_port_dhcp_host_add(self): + host = { + "ip_address": "1.2.3.4", + "mac_address": "aa:bb:cc:dd:ee:ff" + } + self._test_lsn_port_host_action( + lsnlib.lsn_port_dhcp_host_add, "dhcp", "add_host", host) + + def test_lsn_port_dhcp_host_remove(self): + host = { + "ip_address": "1.2.3.4", + "mac_address": "aa:bb:cc:dd:ee:ff" + } + self._test_lsn_port_host_action( + lsnlib.lsn_port_dhcp_host_remove, "dhcp", "remove_host", host) diff --git a/neutron/tests/unit/nicira/test_nvplib.py b/neutron/tests/unit/nicira/test_nvplib.py index 7f561603442..3694e0b5087 100644 --- a/neutron/tests/unit/nicira/test_nvplib.py +++ b/neutron/tests/unit/nicira/test_nvplib.py @@ -1501,6 +1501,68 @@ class NvplibMiscTestCase(base.BaseTestCase): result = utils.check_and_truncate(name) self.assertEqual(len(result), utils.MAX_DISPLAY_NAME_LEN) + def test_build_uri_path_plain(self): + result = nvplib._build_uri_path('RESOURCE') + self.assertEqual("%s/%s" % (nvplib.URI_PREFIX, 'RESOURCE'), result) + + def test_build_uri_path_with_field(self): + result = nvplib._build_uri_path('RESOURCE', fields='uuid') + expected = "%s/%s?fields=uuid" % (nvplib.URI_PREFIX, 'RESOURCE') + self.assertEqual(expected, result) + + def test_build_uri_path_with_filters(self): + filters = {"tag": 'foo', "tag_scope": "scope_foo"} + result = nvplib._build_uri_path('RESOURCE', filters=filters) + expected = ( + "%s/%s?tag_scope=scope_foo&tag=foo" % + (nvplib.URI_PREFIX, 'RESOURCE')) + self.assertEqual(expected, result) + + def test_build_uri_path_with_resource_id(self): + res = 'RESOURCE' + res_id = 'resource_id' + result = nvplib._build_uri_path(res, resource_id=res_id) + expected = "%s/%s/%s" % (nvplib.URI_PREFIX, res, res_id) + self.assertEqual(expected, result) + + def test_build_uri_path_with_parent_and_resource_id(self): + parent_res = 'RESOURCE_PARENT' + child_res = 'RESOURCE_CHILD' + res = '%s/%s' % (child_res, parent_res) + par_id = 'parent_resource_id' + res_id = 'resource_id' + result = nvplib._build_uri_path( + res, parent_resource_id=par_id, resource_id=res_id) + expected = ("%s/%s/%s/%s/%s" % + (nvplib.URI_PREFIX, parent_res, par_id, child_res, res_id)) + self.assertEqual(expected, result) + + def test_build_uri_path_with_attachment(self): + parent_res = 'RESOURCE_PARENT' + child_res = 'RESOURCE_CHILD' + res = '%s/%s' % (child_res, parent_res) + par_id = 'parent_resource_id' + res_id = 'resource_id' + result = nvplib._build_uri_path(res, parent_resource_id=par_id, + resource_id=res_id, is_attachment=True) + expected = ("%s/%s/%s/%s/%s/%s" % + (nvplib.URI_PREFIX, parent_res, + par_id, child_res, res_id, 'attachment')) + self.assertEqual(expected, result) + + def test_build_uri_path_with_extra_action(self): + parent_res = 'RESOURCE_PARENT' + child_res = 'RESOURCE_CHILD' + res = '%s/%s' % (child_res, parent_res) + par_id = 'parent_resource_id' + res_id = 'resource_id' + result = nvplib._build_uri_path(res, parent_resource_id=par_id, + resource_id=res_id, extra_action='doh') + expected = ("%s/%s/%s/%s/%s/%s" % + (nvplib.URI_PREFIX, parent_res, + par_id, child_res, res_id, 'doh')) + self.assertEqual(expected, result) + def _nicira_method(method_name, module_name='nvplib'): return '%s.%s.%s' % ('neutron.plugins.nicira', module_name, method_name) diff --git a/neutron/tests/unit/nicira/test_nvpopts.py b/neutron/tests/unit/nicira/test_nvpopts.py index a245ddca6b7..1fc38d82f3b 100644 --- a/neutron/tests/unit/nicira/test_nvpopts.py +++ b/neutron/tests/unit/nicira/test_nvpopts.py @@ -27,7 +27,9 @@ from neutron.openstack.common import uuidutils from neutron.plugins.nicira.common import config # noqa from neutron.plugins.nicira.common import exceptions from neutron.plugins.nicira.common import sync +from neutron.plugins.nicira.nsxlib import lsn as lsnlib from neutron.plugins.nicira import nvp_cluster +from neutron.plugins.nicira import NvpApiClient as nvp_client from neutron.tests.unit.nicira import get_fake_conf from neutron.tests.unit.nicira import PLUGIN_NAME @@ -149,17 +151,49 @@ class ConfigurationTest(testtools.TestCase): self.assertIn('extensions', cfg.CONF.api_extensions_path) def test_agentless_extensions(self): - self.skipTest('Enable once agentless support is added') q_config.parse(['--config-file', NVP_BASE_CONF_PATH, '--config-file', NVP_INI_AGENTLESS_PATH]) cfg.CONF.set_override('core_plugin', PLUGIN_NAME) self.assertEqual(config.AgentModes.AGENTLESS, cfg.CONF.NVP.agent_mode) - plugin = NeutronManager().get_plugin() - self.assertNotIn('agent', - plugin.supported_extension_aliases) - self.assertNotIn('dhcp_agent_scheduler', - plugin.supported_extension_aliases) + # The version returned from NVP does not really matter here + with mock.patch.object(nvp_client.NVPApiHelper, + 'get_nvp_version', + return_value=nvp_client.NVPVersion("9.9")): + with mock.patch.object(lsnlib, + 'service_cluster_exists', + return_value=True): + plugin = NeutronManager().get_plugin() + self.assertNotIn('agent', + plugin.supported_extension_aliases) + self.assertNotIn('dhcp_agent_scheduler', + plugin.supported_extension_aliases) + + def test_agentless_extensions_version_fail(self): + q_config.parse(['--config-file', NVP_BASE_CONF_PATH, + '--config-file', NVP_INI_AGENTLESS_PATH]) + cfg.CONF.set_override('core_plugin', PLUGIN_NAME) + self.assertEqual(config.AgentModes.AGENTLESS, + cfg.CONF.NVP.agent_mode) + with mock.patch.object(nvp_client.NVPApiHelper, + 'get_nvp_version', + return_value=nvp_client.NVPVersion("3.2")): + self.assertRaises(exceptions.NvpPluginException, NeutronManager) + + def test_agentless_extensions_unmet_deps_fail(self): + q_config.parse(['--config-file', NVP_BASE_CONF_PATH, + '--config-file', NVP_INI_AGENTLESS_PATH]) + cfg.CONF.set_override('core_plugin', PLUGIN_NAME) + self.assertEqual(config.AgentModes.AGENTLESS, + cfg.CONF.NVP.agent_mode) + with mock.patch.object(nvp_client.NVPApiHelper, + 'get_nvp_version', + return_value=nvp_client.NVPVersion("3.2")): + with mock.patch.object(lsnlib, + 'service_cluster_exists', + return_value=False): + self.assertRaises(exceptions.NvpPluginException, + NeutronManager) def test_agent_extensions(self): q_config.parse(['--config-file', NVP_BASE_CONF_PATH,