From f614417f11c4b936e517f0da449038f6a2ca35d4 Mon Sep 17 00:00:00 2001 From: armando-migliaccio Date: Tue, 3 Sep 2013 10:50:51 -0700 Subject: [PATCH] Add support for NSX/NVP DHCP services This is a feature patch (1 of 3) that adds support for DHCP services provided by the NSX (aka NVP) platform. Green-field deployments can use the AGENTLESS mode, which will make the core plugin interact solely with the NSX platform to provide DHCP services. Support for metadata proxy services, migration for brown field deployments will be added in future patches. Partial-implements blueprint nsx-integrated-services Change-Id: Idfc4b2d871e70cd557d8e0e6b23e5563f9ed3420 --- etc/neutron/plugins/nicira/nvp.ini | 17 +- neutron/api/v2/base.py | 8 +- neutron/plugins/nicira/NeutronPlugin.py | 6 +- neutron/plugins/nicira/common/config.py | 3 + neutron/plugins/nicira/common/exceptions.py | 27 + neutron/plugins/nicira/common/utils.py | 10 + neutron/plugins/nicira/dhcp_meta/nvp.py | 405 +++++++++++ neutron/plugins/nicira/dhcp_meta/rpc.py | 2 +- neutron/plugins/nicira/dhcpmeta_modes.py | 73 +- neutron/plugins/nicira/nsxlib/__init__.py | 16 + neutron/plugins/nicira/nsxlib/lsn.py | 201 ++++++ neutron/plugins/nicira/nvplib.py | 5 +- .../unit/nicira/etc/nvp.ini.agentless.test | 1 + neutron/tests/unit/nicira/test_dhcpmeta.py | 633 ++++++++++++++++++ neutron/tests/unit/nicira/test_lsn_lib.py | 258 +++++++ neutron/tests/unit/nicira/test_nvplib.py | 62 ++ neutron/tests/unit/nicira/test_nvpopts.py | 46 +- 17 files changed, 1737 insertions(+), 36 deletions(-) create mode 100644 neutron/plugins/nicira/dhcp_meta/nvp.py create mode 100644 neutron/plugins/nicira/nsxlib/__init__.py create mode 100644 neutron/plugins/nicira/nsxlib/lsn.py create mode 100644 neutron/tests/unit/nicira/test_dhcpmeta.py create mode 100644 neutron/tests/unit/nicira/test_lsn_lib.py diff --git a/etc/neutron/plugins/nicira/nvp.ini b/etc/neutron/plugins/nicira/nvp.ini index 921520387..831eed8b7 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 83d842752..12a10ae34 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 ab3319bc4..c9e7595fa 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 09d7e232a..18a33f1fa 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 bc7c4f5c0..14add6201 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 57d08bb55..07b456c12 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 000000000..c4b046e88 --- /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 07930455f..4bf2561e4 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 150d0feec..45a5a96d6 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 000000000..c020e3bcd --- /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 000000000..f10c59d4f --- /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 d27f92839..5345b1166 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 33550520a..3ade4e738 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 000000000..7c4663757 --- /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 000000000..88e3fcb95 --- /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 7f5616034..3694e0b50 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 a245ddca6..1fc38d82f 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, -- 2.45.2