diff --git a/MANIFEST.in b/MANIFEST.in index 9d77adccb84..e207f42845e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,7 @@ include etc/quantum/plugins/openvswitch/*.ini include etc/quantum/plugins/cisco/*.ini include etc/quantum/plugins/cisco/quantum.conf.ciscoext include etc/quantum/plugins/linuxbridge/*.ini +include etc/quantum/plugins/nicira/* include quantum/plugins/*/README include quantum/plugins/openvswitch/Makefile include quantum/plugins/openvswitch/agent/xenserver_install.sh diff --git a/etc/quantum/plugins/nicira/nvp.ini b/etc/quantum/plugins/nicira/nvp.ini new file mode 100644 index 00000000000..f3e484fa19a --- /dev/null +++ b/etc/quantum/plugins/nicira/nvp.ini @@ -0,0 +1,36 @@ +# Example configuration: +# [NVP] +# DEFAULT_TZ_UUID = 1e8e52cf-fa7f-46b0-a14a-f99835a9cb53 +# NVP_CONTROLLER_CONNECTIONS = NVP_CONN_1 NVP_CONN_2 NVP_CONN_3 +# NVP_CONN_1=10.0.1.2:443:admin:password:30:10:2:2 +# NVP_CONN_2=10.0.1.3:443:admin:password:30:10:2:2 +# NVP_CONN_3=10.0.1.4:443:admin:password:30:10:2:2 +[DEFAULT] +# No default config for now. +[NVP] +# This is the uuid of the default NVP Transport zone that will be used for +# creating isolated "Quantum" networks. The transport zone needs to be +# created in NVP before starting Quantum with the plugin. +DEFAULT_TZ_UUID = +# This parameter is a space separated list of NVP_CONTROLLER_CONNECTIONS. +NVP_CONTROLLER_CONNECTIONS = +# This parameter describes a connection to a single NVP controller. +# is the ip address of the controller +# is the port of the controller (default NVP port is 443) +# is the user name for this controller +# is the user password. +# : The total time limit on all operations for a controller +# request (including retries, redirects from unresponsive controllers). +# Default is 30. +# : How long to wait before aborting an unresponsive controller +# (and allow for retries to another controller). +# Default is 10. +# : the maximum number of times to retry a particular request +# Default is 2. +# : the maximum number of times to follow a redirect response from a server. +# Default is 2. +# There must be at least one NVP_CONTROLLER_CONNECTION per system. +# +# Here is an example: +# NVP_CONTROLLER_CONNECTION_1=10.0.0.1:443:admin:password:30:10:2:2 +=::::::: diff --git a/quantum/plugins/nicira/__init__.py b/quantum/plugins/nicira/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py b/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py new file mode 100644 index 00000000000..dff3871c14e --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py @@ -0,0 +1,204 @@ +''' +# Copyright 2012 Nicira Networks, 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. + +@author: Somik Behera, Nicira Networks, Inc. +''' + +import httplib # basic HTTP library for HTTPS connections +import logging +from api_client.client_eventlet import NvpApiClientEventlet +from api_client.request_eventlet import NvpGenericRequestEventlet + +LOG = logging.getLogger("NVPApiHelper") +LOG.setLevel(logging.INFO) + + +class NVPApiHelper(NvpApiClientEventlet): + ''' + Helper class to do basic login, cookie management, and provide base + method to send HTTP requests. + + Implements new eventlet-based framework derived from the management + console nvp_gevent_client module. + ''' + + def __init__(self, api_providers, user, password, request_timeout, + http_timeout, retries, redirects, failover_time, + concurrent_connections=3): + '''Constructor. + + :param api_providers: a list of tuples in the form: + (host, port, is_ssl=True). Passed on to NvpClientEventlet. + :param user: the login username. + :param password: the login password. + :param concurrent_connections: the number of concurrent connections. + :param request_timeout: all operations (including retries, redirects + from unresponsive controllers, etc) should finish within this + timeout. + :param http_timeout: how long to wait before aborting an + unresponsive controller + :param retries: the number of concurrent connections. + :param redirects: the number of concurrent connections. + :param failover_time: minimum time between controller failover and new + connections allowed. + ''' + NvpApiClientEventlet.__init__( + self, api_providers, user, password, concurrent_connections, + failover_time=failover_time) + + self._request_timeout = request_timeout + self._http_timeout = http_timeout + self._retries = retries + self._redirects = redirects + + def login(self, user=None, password=None): + '''Login to NVP controller. + + Assumes same password is used for all controllers. + + :param user: NVP controller user (usually admin). Provided for + backwards compatability. In the normal mode of operation + this should be None. + :param password: NVP controller password. Provided for backwards + compatability. In the normal mode of operation this should + be None. + + :returns: Does not return a value. + ''' + if user: + self._user = user + if password: + self._password = password + + return NvpApiClientEventlet.login(self) + + def request(self, method, url, body="", content_type="application/json"): + '''Issues request to controller.''' + + g = NvpGenericRequestEventlet( + self, method, url, body, content_type, auto_login=True, + request_timeout=self._request_timeout, + http_timeout=self._http_timeout, + retries=self._retries, redirects=self._redirects) + g.start() + response = g.join() + LOG.debug('NVPApiHelper.request() returns "%s"' % response) + + # response is a modified HTTPResponse object or None. + # response.read() will not work on response as the underlying library + # request_eventlet.NvpApiRequestEventlet has already called this + # method in order to extract the body and headers for processing. + # NvpApiRequestEventlet derived classes call .read() and + # .getheaders() on the HTTPResponse objects and store the results in + # the response object's .body and .headers data members for future + # access. + + if response is None: + # Timeout. + LOG.error('Request timed out: %s to %s' % (method, url)) + raise RequestTimeout() + + status = response.status + if status == httplib.UNAUTHORIZED: + raise UnAuthorizedRequest() + + # Fail-fast: Check for exception conditions and raise the + # appropriate exceptions for known error codes. + if status in self.error_codes: + LOG.error("Received error code: %s" % status) + LOG.error("Server Error Message: %s" % response.body) + self.error_codes[status](self) + + # Continue processing for non-error condition. + if (status != httplib.OK and status != httplib.CREATED + and status != httplib.NO_CONTENT): + LOG.error("%s to %s, unexpected response code: %d (content = '%s')" + % (method, url, response.status, response.body)) + return None + + return response.body + + def fourZeroFour(self): + raise ResourceNotFound() + + def fourZeroNine(self): + raise Conflict() + + def fiveZeroThree(self): + raise ServiceUnavailable() + + def fourZeroThree(self): + raise Forbidden() + + def zero(self): + raise NvpApiException() + + error_codes = {404: fourZeroFour, + 409: fourZeroNine, + 503: fiveZeroThree, + 403: fourZeroThree, + 301: zero, + 307: zero, + 400: zero, + 500: zero} + + +class NvpApiException(Exception): + ''' + Base NvpApiClient Exception + + To correctly use this class, inherit from it and define + a 'message' property. That message will get printf'd + with the keyword arguments provided to the constructor. + + ''' + message = "An unknown exception occurred." + + def __init__(self, **kwargs): + try: + self._error_string = self.message % kwargs + + except Exception: + # at least get the core message out if something happened + self._error_string = self.message + + def __str__(self): + return self._error_string + + +class UnAuthorizedRequest(NvpApiException): + message = "Server denied session's authentication credentials." + + +class ResourceNotFound(NvpApiException): + message = "An entity referenced in the request was not found." + + +class Conflict(NvpApiException): + message = "Request conflicts with configuration on a different entity." + + +class ServiceUnavailable(NvpApiException): + message = "Request could not completed because the associated " \ + "resource could not be reached." + + +class Forbidden(NvpApiException): + message = "The request is forbidden from accessing the " \ + "referenced resource." + + +class RequestTimeout(NvpApiException): + message = "The request has timed out." diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py new file mode 100644 index 00000000000..37cf8440ede --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py @@ -0,0 +1,593 @@ +# Copyright 2012 Nicira Networks, 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. +# +# @author: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. + +import ConfigParser +import logging +import nvplib +import NvpApiClient +import os +import sys + +from api_client.client_eventlet import DEFAULT_CONCURRENT_CONNECTIONS +from api_client.client_eventlet import DEFAULT_FAILOVER_TIME +from api_client.request_eventlet import DEFAULT_REQUEST_TIMEOUT +from api_client.request_eventlet import DEFAULT_HTTP_TIMEOUT +from api_client.request_eventlet import DEFAULT_RETRIES +from api_client.request_eventlet import DEFAULT_REDIRECTS + +from quantum.common import exceptions as exception + +CONFIG_FILE = "nvp.ini" +CONFIG_FILE_PATHS = [] +if os.environ.get('QUANTUM_HOME', None): + CONFIG_FILE_PATHS.append('%s/etc' % os.environ['QUANTUM_HOME']) +CONFIG_FILE_PATHS.append("/etc/quantum/plugins/nicira") +CONFIG_KEYS = ["DEFAULT_TZ_UUID", "NVP_CONTROLLER_IP", "PORT", "USER", + "PASSWORD"] +LOG = logging.getLogger("QuantumPlugin") + + +def initConfig(cfile=None): + config = ConfigParser.ConfigParser() + if cfile == None: + if os.path.exists(CONFIG_FILE): + cfile = CONFIG_FILE + else: + cfile = find_config(os.path.abspath(os.path.dirname(__file__))) + + if cfile == None: + raise Exception("Configuration file \"%s\" doesn't exist" % + (cfile)) + LOG.info("Using configuration file: %s" % cfile) + config.read(cfile) + LOG.debug("Config: %s" % config) + return config + + +def find_config(basepath): + LOG.info("Looking for %s in %s" % (CONFIG_FILE, basepath)) + for root, dirs, files in os.walk(basepath, followlinks=True): + if CONFIG_FILE in files: + return os.path.join(root, CONFIG_FILE) + for alternate_path in CONFIG_FILE_PATHS: + p = os.path.join(alternate_path, CONFIG_FILE) + if os.path.exists(p): + return p + return None + + +def parse_config(config): + '''Backwards compatible parsing. + + :param config: ConfigParser object initilized with nvp.ini. + :returns: A tuple consisting of a control cluster object and a + plugin_config variable. + raises: In general, system exceptions are not caught but are propagated + up to the user. Config parsing is still very lightweight. + At some point, error handling needs to be significantly + enhanced to provide user friendly error messages, clean program + exists, rather than exceptions propagated to the user. + ''' + # Extract plugin config parameters. + try: + failover_time = config.get('NVP', 'failover_time') + except ConfigParser.NoOptionError, e: + failover_time = str(DEFAULT_FAILOVER_TIME) + + try: + concurrent_connections = config.get('NVP', 'concurrent_connections') + except ConfigParser.NoOptionError, e: + concurrent_connections = str(DEFAULT_CONCURRENT_CONNECTIONS) + + plugin_config = { + 'failover_time': failover_time, + 'concurrent_connections': concurrent_connections + } + LOG.info('parse_config(): plugin_config == "%s"' % plugin_config) + + cluster = NVPCluster('cluster1') + + # Extract connection information. + try: + defined_connections = config.get( + 'NVP', 'NVP_CONTROLLER_CONNECTIONS') + + for conn_key in defined_connections.split(): + args = [config.get('NVP', 'DEFAULT_TZ_UUID')] + args.extend(config.get('NVP', conn_key).split(':')) + try: + cluster.add_controller(*args) + except Exception, e: + LOG.fatal('Invalid connection parameters: %s' % str(e)) + sys.exit(1) + + return cluster, plugin_config + except Exception, e: + LOG.info('No new style connections defined: %s' % e) + + # Old style controller specification. + args = [config.get('NVP', k) for k in CONFIG_KEYS] + try: + cluster.add_controller(*args) + except Exception, e: + LOG.fatal('Invalid connection parameters.') + sys.exit(1) + + return cluster, plugin_config + + +class NVPCluster(object): + '''Encapsulates controller connection and api_client. + + Initialized within parse_config(). + Accessed within the NvpPlugin class. + + Each element in the self.controllers list is a dictionary that + contains the following keys: + ip, port, user, password, default_tz_uuid + + There may be some redundancy here, but that has been done to provide + future flexibility. + ''' + def __init__(self, name): + self._name = name + self.controllers = [] + self.api_client = None + + def __repr__(self): + ss = ['{ "NVPCluster": ['] + ss.append('{ "name" : "%s" }' % self.name) + ss.append(',') + for c in self.controllers: + ss.append(str(c)) + ss.append(',') + ss.append('] }') + return ''.join(ss) + + def add_controller(self, default_tz_uuid, ip, port, user, password, + request_timeout=DEFAULT_REQUEST_TIMEOUT, + http_timeout=DEFAULT_HTTP_TIMEOUT, + retries=DEFAULT_RETRIES, redirects=DEFAULT_REDIRECTS): + '''Add a new set of controller parameters. + + :param ip: IP address of controller. + :param port: port controller is listening on. + :param user: user name. + :param password: user password. + :param request_timeout: timeout for an entire API request. + :param http_timeout: timeout for a connect to a controller. + :param retries: maximum number of request retries. + :param redirects: maximum number of server redirect responses to + follow. + :param default_tz_uuid: default transport zone uuid. + ''' + + keys = [ + 'ip', 'port', 'user', 'password', 'default_tz_uuid'] + controller_dict = dict([(k, locals()[k]) for k in keys]) + + int_keys = [ + 'request_timeout', 'http_timeout', 'retries', 'redirects'] + for k in int_keys: + controller_dict[k] = int(locals()[k]) + + self.controllers.append(controller_dict) + + def get_controller(self, idx): + return self.controllers[idx] + + @property + def name(self): + return self._name + + @name.setter + def name(self, val=None): + self._name = val + + @property + def host(self): + return self.controllers[0]['ip'] + + @property + def port(self): + return self.controllers[0]['port'] + + @property + def user(self): + return self.controllers[0]['user'] + + @property + def password(self): + return self.controllers[0]['password'] + + @property + def request_timeout(self): + return self.controllers[0]['request_timeout'] + + @property + def http_timeout(self): + return self.controllers[0]['http_timeout'] + + @property + def retries(self): + return self.controllers[0]['retries'] + + @property + def redirects(self): + return self.controllers[0]['redirects'] + + @property + def default_tz_uuid(self): + return self.controllers[0]['default_tz_uuid'] + + +class NvpPlugin(object): + ''' + NvpPlugin is a Quantum plugin that provides L2 Virtual Network + functionality using NVP. + ''' + supported_extension_aliases = ["portstats"] + + def __init__(self, configfile=None, loglevel=None, cli=False): + if loglevel: + logging.basicConfig(level=loglevel) + nvplib.LOG.setLevel(loglevel) + NvpApiClient.LOG.setLevel(loglevel) + + config = initConfig(configfile) + self.controller, self.plugin_config = parse_config(config) + c = self.controller + api_providers = [ + (x['ip'], x['port'], True) for x in c.controllers] + + c.api_client = NvpApiClient.NVPApiHelper( + api_providers, c.user, c.password, + request_timeout=c.request_timeout, http_timeout=c.http_timeout, + retries=c.retries, redirects=c.redirects, + failover_time=int(self.plugin_config['failover_time']), + concurrent_connections=int( + self.plugin_config['concurrent_connections'])) + + c.api_client.login() + + # For testing.. + self.api_client = self.controller.api_client + + def get_all_networks(self, tenant_id, **kwargs): + ''' + Returns a dictionary containing all for + the specified tenant. + + :returns: a list of mapping sequences with the following signature: + [{'net-id': uuid that uniquely identifies + the particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id + }, + .... + {'net-id': uuid that uniquely identifies the + particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id + } + ] + :raises: None + ''' + networks = nvplib.get_all_networks(self.controller, tenant_id, + []) + LOG.debug("get_all_networks() completed for tenant %s: %s" % ( + tenant_id, networks)) + return networks + + def create_network(self, tenant_id, net_name, **kwargs): + ''' + Creates a new Virtual Network, and assigns it a symbolic name. + :returns: a sequence of mappings with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network, + 'net-name': a human-readable name associated + with network referenced by net-id + } + :raises: + ''' + kwargs["controller"] = self.controller + return nvplib.create_network(tenant_id, net_name, **kwargs) + + def create_custom_network(self, tenant_id, net_name, transport_zone, + controller): + return self.create_network(tenant_id, net_name, + network_type="custom", + transport_zone=transport_zone, + controller=controller) + + def delete_network(self, tenant_id, netw_id): + ''' + Deletes the network with the specified network identifier + belonging to the specified tenant. + + :returns: a sequence of mappings with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network + } + :raises: exception.NetworkInUse + :raises: exception.NetworkNotFound + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + nvplib.delete_network(self.controller, netw_id) + + LOG.debug("delete_network() completed for tenant: %s" % tenant_id) + return {'net-id': netw_id} + + def get_network_details(self, tenant_id, netw_id): + ''' + Retrieves a list of all the remote vifs that + are attached to the network. + + :returns: a sequence of mappings with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network + 'net-name': a human-readable name associated + with network referenced by net-id + 'net-ifaces': ['vif1_on_network_uuid', + 'vif2_on_network_uuid',...,'vifn_uuid'] + } + :raises: exception.NetworkNotFound + :raises: exception.QuantumException + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + result = None + remote_vifs = [] + switch = netw_id + lports = nvplib.query_ports(self.controller, switch, + relations="LogicalPortAttachment") + + for port in lports: + relation = port["_relations"] + vic = relation["LogicalPortAttachment"] + if "vif_uuid" in vic: + remote_vifs.append(vic["vif_uuid"]) + + if not result: + result = nvplib.get_network(self.controller, switch) + + d = {"net-id": netw_id, + "net-ifaces": remote_vifs, + "net-name": result["display_name"], + "net-op-status": "UP"} + LOG.debug("get_network_details() completed for tenant %s: %s" % ( + tenant_id, d)) + return d + + def update_network(self, tenant_id, netw_id, **kwargs): + ''' + Updates the properties of a particular Virtual Network. + + :returns: a sequence of mappings representing the new network + attributes, with the following signature: + {'net-id': uuid that uniquely identifies the + particular quantum network + 'net-name': the new human-readable name + associated with network referenced by net-id + } + :raises: exception.NetworkNotFound + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + result = nvplib.update_network(self.controller, netw_id, **kwargs) + LOG.debug("update_network() completed for tenant: %s" % tenant_id) + return {'net-id': netw_id, 'net-name': result["display_name"], + 'net-op-status': "UP"} + + def get_all_ports(self, tenant_id, netw_id, **kwargs): + ''' + Retrieves all port identifiers belonging to the + specified Virtual Network. + + :returns: a list of mapping sequences with the following signature: + [{'port-id': uuid representing a particular port + on the specified quantum network + }, + .... + {'port-id': uuid representing a particular port + on the specified quantum network + } + ] + :raises: exception.NetworkNotFound + ''' + ids = [] + filters = kwargs.get("filter_opts") or {} + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + LOG.debug("Getting logical ports on lswitch: %s" % netw_id) + lports = nvplib.query_ports(self.controller, netw_id, fields="uuid", + filters=filters) + for port in lports: + ids.append({"port-id": port["uuid"]}) + + # Delete from the filter so that Quantum doesn't attempt to filter on + # this too + if filters and "attachment" in filters: + del filters["attachment"] + + LOG.debug("get_all_ports() completed for tenant: %s" % tenant_id) + LOG.debug("returning port listing:") + LOG.debug(ids) + return ids + + def create_port(self, tenant_id, netw_id, port_init_state=None, + **params): + ''' + Creates a port on the specified Virtual Network. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the created port + on specified quantum network + } + :raises: exception.NetworkNotFound + :raises: exception.StateInvalid + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + params["controller"] = self.controller + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + result = nvplib.create_port(tenant_id, netw_id, port_init_state, + **params) + d = {"port-id": result["uuid"], + "port-op-status": result["port-op-status"]} + LOG.debug("create_port() completed for tenant %s: %s" % (tenant_id, d)) + return d + + def update_port(self, tenant_id, netw_id, portw_id, **params): + ''' + Updates the properties of a specific port on the + specified Virtual Network. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the + updated port on specified quantum network + 'port-state': update port state (UP or DOWN) + } + :raises: exception.StateInvalid + :raises: exception.PortNotFound + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + LOG.debug("Update port request: %s" % (params)) + params["controller"] = self.controller + result = nvplib.update_port(netw_id, portw_id, **params) + LOG.debug("update_port() completed for tenant: %s" % tenant_id) + port = {'port-id': portw_id, + 'port-state': result["admin_status_enabled"], + 'port-op-status': result["port-op-status"]} + LOG.debug("returning updated port %s: " % port) + return port + + def delete_port(self, tenant_id, netw_id, portw_id): + ''' + Deletes a port on a specified Virtual Network, + if the port contains a remote interface attachment, + the remote interface is first un-plugged and then the port + is deleted. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the deleted port + on specified quantum network + } + :raises: exception.PortInUse + :raises: exception.PortNotFound + :raises: exception.NetworkNotFound + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + nvplib.delete_port(self.controller, netw_id, portw_id) + LOG.debug("delete_port() completed for tenant: %s" % tenant_id) + return {"port-id": portw_id} + + def get_port_details(self, tenant_id, netw_id, portw_id): + ''' + This method allows the user to retrieve a remote interface + that is attached to this particular port. + + :returns: a mapping sequence with the following signature: + {'port-id': uuid representing the port on + specified quantum network + 'net-id': uuid representing the particular + quantum network + 'attachment': uuid of the virtual interface + bound to the port, None otherwise + } + :raises: exception.PortNotFound + :raises: exception.NetworkNotFound + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + port = nvplib.get_port(self.controller, netw_id, portw_id, + "LogicalPortAttachment") + state = "ACTIVE" if port["admin_status_enabled"] else "DOWN" + op_status = nvplib.get_port_status(self.controller, netw_id, portw_id) + + relation = port["_relations"] + attach_type = relation["LogicalPortAttachment"]["type"] + + vif_uuid = "None" + if attach_type == "VifAttachment": + vif_uuid = relation["LogicalPortAttachment"]["vif_uuid"] + + d = {"port-id": portw_id, "attachment": vif_uuid, + "net-id": netw_id, "port-state": state, + "port-op-status": op_status} + LOG.debug("Port details for tenant %s: %s" % (tenant_id, d)) + return d + + def plug_interface(self, tenant_id, netw_id, portw_id, + remote_interface_id): + ''' + Attaches a remote interface to the specified port on the + specified Virtual Network. + + :returns: None + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound + :raises: exception.AlreadyAttached + (? should the network automatically unplug/replug) + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + result = nvplib.plug_interface(self.controller, netw_id, portw_id, + "VifAttachment", attachment=remote_interface_id) + LOG.debug("plug_interface() completed for %s: %s" % ( + tenant_id, result)) + + def unplug_interface(self, tenant_id, netw_id, portw_id): + ''' + Detaches a remote interface from the specified port on the + specified Virtual Network. + + :returns: None + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound + ''' + if not nvplib.check_tenant(self.controller, netw_id, tenant_id): + raise exception.NetworkNotFound(net_id=netw_id) + result = nvplib.unplug_interface(self.controller, netw_id, portw_id) + + LOG.debug("unplug_interface() completed for tenant %s: %s" % + (tenant_id, result)) + + def get_port_stats(self, tenant_id, network_id, port_id): + """ + Returns port statistics for a given port. + + { + "rx_packets": 0, + "rx_bytes": 0, + "tx_errors": 0, + "rx_errors": 0, + "tx_bytes": 0, + "tx_packets": 0 + } + + :returns: dict() of stats + :raises: exception.NetworkNotFound + :raises: exception.PortNotFound + """ + if not nvplib.check_tenant(self.controller, network_id, tenant_id): + raise exception.NetworkNotFound(net_id=network_id) + return nvplib.get_port_stats(self.controller, network_id, port_id) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/README b/quantum/plugins/nicira/nicira_nvp_plugin/README new file mode 100644 index 00000000000..4bc0df59c21 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/README @@ -0,0 +1,24 @@ +nvp-plugin +----------------------------------------------------------------------------- + +Overview and pre-requisites + + This is a Quantum plugin that can talk to a set of NVP controllers and + implements the core Quantum L2 api. In order to use it you must have + Nicira NVP running and configured. You must also have Quantum installed + and configured. + +Installation and Configuration + + Edit nvp.ini to match your controller configuration and then modify your + Quantum plugins.ini provider path: + + provider = quantum.plugins.nicira.nicira_nvp_plugin.QuantumPlugin.NvpPlugin + +Testing + + Edit etc/quantum/plugins/nicira/nvp.ini to match your nvp configuration + (nvp must be up and running). Then: + + $ cd quantum/plugins/nicira + $ PYTHONPATH=../../../:. nosetests -v diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/__init__.py b/quantum/plugins/nicira/nicira_nvp_plugin/__init__.py new file mode 100644 index 00000000000..0697d2ebcd9 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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/quantum/plugins/nicira/nicira_nvp_plugin/api_client/__init__.py b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/api_client/client.py b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/client.py new file mode 100644 index 00000000000..545387c58c1 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/client.py @@ -0,0 +1,69 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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. +# +# Author: David Lapsley , Nicira Networks, Inc. + +from abc import ABCMeta +from abc import abstractmethod +from abc import abstractproperty + + +class NvpApiClient(object): + '''An abstract baseclass for all NvpApiClient implementations. + + This defines the interface and property structure for synchronous and + coroutine-based classes. + ''' + + __metaclass__ = ABCMeta + + # Default connection timeout for a controller. After CONN_IDLE_TIMEOUT + # seconds the client attempt to reconnect. + CONN_IDLE_TIMEOUT = 60 * 15 + + @abstractmethod + def update_providers(self, api_providers): + pass + + @abstractproperty + def user(self): + pass + + @abstractproperty + def password(self): + pass + + @abstractproperty + def auth_cookie(self): + pass + + @abstractmethod + def acquire_connection(self): + pass + + @abstractmethod + def release_connection(self, http_conn, bad_state=False): + pass + + @abstractproperty + def need_login(self): + pass + + @abstractmethod + def wait_for_login(self): + pass + + @abstractmethod + def login(self): + pass diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/api_client/client_eventlet.py b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/client_eventlet.py new file mode 100644 index 00000000000..d9493d09f58 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/client_eventlet.py @@ -0,0 +1,226 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 client +import eventlet +import httplib +import logging +import request_eventlet +import time +from common import _conn_str + + +logging.basicConfig(level=logging.INFO) +lg = logging.getLogger('nvp_api_client') + +# Default parameters. +DEFAULT_FAILOVER_TIME = 5 +DEFAULT_CONCURRENT_CONNECTIONS = 3 +DEFAULT_CONNECT_TIMEOUT = 5 + + +class NvpApiClientEventlet(object): + '''Eventlet-based implementation of NvpApiClient ABC.''' + + CONN_IDLE_TIMEOUT = 60 * 15 + + def __init__(self, api_providers, user, password, + concurrent_connections=DEFAULT_CONCURRENT_CONNECTIONS, + use_https=True, + connect_timeout=DEFAULT_CONNECT_TIMEOUT, + failover_time=DEFAULT_FAILOVER_TIME): + '''Constructor + + Args: + api_providers: a list of tuples of the form: (host, port, is_ssl). + user: login username. + password: login password. + concurrent_connections: total number of concurrent connections. + use_https: whether or not to use https for requests. + connect_timeout: connection timeout in seconds. + ''' + self._api_providers = set([tuple(p) for p in api_providers]) + self._user = user + self._password = password + self._concurrent_connections = concurrent_connections + self._use_https = use_https + self._connect_timeout = connect_timeout + self._failover_time = failover_time + + # Connection pool is a queue. Head of the queue is the + # connection pool with the highest priority. + self._conn_pool = eventlet.queue.Queue() + for host, port, is_ssl in self._api_providers: + provider_conn_pool = eventlet.queue.Queue() + for i in range(concurrent_connections): + # All connections in a provider_conn_poool have the + # same priority (they connect to the same server). + conn = self._create_connection(host, port, is_ssl) + conn.conn_pool = provider_conn_pool + provider_conn_pool.put(conn) + + self._conn_pool.put(provider_conn_pool) + + self._active_conn_pool = self._conn_pool.get() + + self._cookie = None + self._need_login = True + self._doing_login_sem = eventlet.semaphore.Semaphore(1) + + def _create_connection(self, host, port, is_ssl): + if is_ssl: + return httplib.HTTPSConnection(host, port, + timeout=self._connect_timeout) + return httplib.HTTPConnection(host, port, + timeout=self._connect_timeout) + + @staticmethod + def _conn_params(http_conn): + is_ssl = isinstance(http_conn, httplib.HTTPSConnection) + return (http_conn.host, http_conn.port, is_ssl) + + def update_providers(self, api_providers): + raise Exception('update_providers() not implemented.') + + @property + def user(self): + return self._user + + @property + def password(self): + return self._password + + @property + def auth_cookie(self): + return self._cookie + + def acquire_connection(self): + '''Check out an available HTTPConnection instance. + + Blocks until a connection is available. + + Returns: An available HTTPConnection instance or None if no + api_providers are configured. + ''' + if not self._api_providers: + return None + + # The sleep time is to give controllers time to become consistent after + # there has been a change in the controller used as the api_provider. + now = time.time() + if now < getattr(self, '_issue_conn_barrier', now): + lg.info("acquire_connection() waiting for timer to expire.") + time.sleep(self._issue_conn_barrier - now) + + if self._active_conn_pool.empty(): + lg.debug("Waiting to acquire an API client connection") + + # get() call is blocking. + conn = self._active_conn_pool.get() + now = time.time() + if getattr(conn, 'last_used', now) < now - self.CONN_IDLE_TIMEOUT: + lg.info("Connection %s idle for %0.2f seconds; reconnecting." + % (_conn_str(conn), now - conn.last_used)) + conn = self._create_connection(*self._conn_params(conn)) + + # Stash conn pool so conn knows where to go when it releases. + conn.conn_pool = self._active_conn_pool + + conn.last_used = now + lg.debug("API client connection %s acquired" % _conn_str(conn)) + return conn + + def release_connection(self, http_conn, bad_state=False): + '''Mark HTTPConnection instance as available for check-out. + + Args: + http_conn: An HTTPConnection instance obtained from this + instance. + bad_state: True if http_conn is known to be in a bad state + (e.g. connection fault.) + ''' + if self._conn_params(http_conn) not in self._api_providers: + lg.debug("Released connection '%s' is no longer an API provider " + "for the cluster" % _conn_str(http_conn)) + return + + # Retrieve "home" connection pool. + conn_pool = http_conn.conn_pool + if bad_state: + # reconnect + lg.info("API connection fault, reconnecting to %s" + % _conn_str(http_conn)) + http_conn = self._create_connection(*self._conn_params(http_conn)) + http_conn.conn_pool = conn_pool + conn_pool.put(http_conn) + + if self._active_conn_pool == http_conn.conn_pool: + # Get next connection from the connection pool and make it + # active. + lg.info("API connection fault changing active_conn_pool.") + self._conn_pool.put(self._active_conn_pool) + self._active_conn_pool = self._conn_pool.get() + self._issue_conn_barrier = time.time() + self._failover_time + else: + conn_pool.put(http_conn) + + lg.debug("API client connection %s released" % _conn_str(http_conn)) + + @property + def need_login(self): + return self._need_login + + @need_login.setter + def need_login(self, val=True): + self._need_login = val + + def wait_for_login(self): + if self._need_login: + if self._doing_login_sem.acquire(blocking=False): + self.login() + self._doing_login_sem.release() + else: + lg.debug("Waiting for auth to complete") + self._doing_login_sem.acquire() + self._doing_login_sem.release() + return self._cookie + + def login(self): + '''Issue login request and update authentication cookie.''' + g = request_eventlet.NvpLoginRequestEventlet( + self, self._user, self._password) + g.start() + ret = g.join() + + if ret: + if isinstance(ret, Exception): + lg.error('NvpApiClient: login error "%s"' % ret) + raise ret + + self._cookie = None + cookie = ret.getheader("Set-Cookie") + if cookie: + lg.debug("Saving new authentication cookie '%s'" % cookie) + self._cookie = cookie + self._need_login = False + + if not ret: + return None + + return self._cookie + + +# Register as subclass. +client.NvpApiClient.register(NvpApiClientEventlet) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/api_client/common.py b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/common.py new file mode 100644 index 00000000000..2ed8fb11f61 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/common.py @@ -0,0 +1,30 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 httplib +import mock + + +def _conn_str(conn): + if isinstance(conn, httplib.HTTPSConnection): + proto = "https://" + elif isinstance(conn, httplib.HTTPConnection): + proto = "http://" + elif isinstance(conn, mock.Mock): + proto = "http://" + else: + raise TypeError('_conn_str() invalid connection type: %s' % type(conn)) + + return "%s%s:%s" % (proto, conn.host, conn.port) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/api_client/request.py b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/request.py new file mode 100644 index 00000000000..51a27b3e1d0 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/request.py @@ -0,0 +1,44 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 abc import ABCMeta +from abc import abstractmethod +from abc import abstractproperty + + +class NvpApiRequest: + '''An abstract baseclass for all ApiRequest implementations. + + This defines the interface and property structure for both eventlet and + gevent-based ApiRequest classes. + ''' + + __metaclass__ = ABCMeta + + @abstractmethod + def start(self): + pass + + @abstractmethod + def join(self): + pass + + @abstractmethod + def copy(self): + pass + + @abstractproperty + def request_error(self): + pass diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/api_client/request_eventlet.py b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/request_eventlet.py new file mode 100644 index 00000000000..7d31c99ca67 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/api_client/request_eventlet.py @@ -0,0 +1,362 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 client_eventlet +import eventlet +import httplib +import urllib +import urlparse +import logging +import request +import time +import json +from common import _conn_str +from eventlet import timeout + + +logging.basicConfig(level=logging.INFO) +lg = logging.getLogger("nvp_api_request") +USER_AGENT = "NVP gevent client/1.0" + +# Default parameters. +DEFAULT_REQUEST_TIMEOUT = 30 +DEFAULT_HTTP_TIMEOUT = 10 +DEFAULT_RETRIES = 2 +DEFAULT_REDIRECTS = 2 +API_REQUEST_POOL_SIZE = 10000 + + +class NvpApiRequestEventlet: + '''Eventlet-based ApiRequest class. + + This class will form the basis for eventlet-based ApiRequest classes + (e.g. those used by the Quantum NVP Plugin). + ''' + + ALLOWED_STATUS_CODES = [ + httplib.OK, + httplib.CREATED, + httplib.NO_CONTENT, + httplib.MOVED_PERMANENTLY, + httplib.TEMPORARY_REDIRECT, + httplib.BAD_REQUEST, + httplib.UNAUTHORIZED, + httplib.FORBIDDEN, + httplib.NOT_FOUND, + httplib.CONFLICT, + httplib.INTERNAL_SERVER_ERROR, + httplib.SERVICE_UNAVAILABLE + ] + + API_REQUEST_POOL = eventlet.GreenPool(API_REQUEST_POOL_SIZE) + + def __init__(self, nvp_api_client, url, method="GET", body=None, + headers=None, + request_timeout=DEFAULT_REQUEST_TIMEOUT, + retries=DEFAULT_RETRIES, + auto_login=True, + redirects=DEFAULT_REDIRECTS, + http_timeout=DEFAULT_HTTP_TIMEOUT): + + self._api_client = nvp_api_client + self._url = url + self._method = method + self._body = body + self._headers = headers or {} + self._request_timeout = request_timeout + self._retries = retries + self._auto_login = auto_login + self._redirects = redirects + self._http_timeout = http_timeout + + self._request_error = None + + if "User-Agent" not in self._headers: + self._headers["User-Agent"] = USER_AGENT + + self._green_thread = None + + @classmethod + def _spawn(cls, func, *args, **kwargs): + return cls.API_REQUEST_POOL.spawn(func, *args, **kwargs) + + def spawn(self, func, *args, **kwargs): + return self.__class__._spawn(func, *args, **kwargs) + + @classmethod + def joinall(cls): + return cls.API_REQUEST_POOL.waitall() + + def join(self): + if self._green_thread is not None: + return self._green_thread.wait() + lg.error('Joining on invalid green thread') + return Exception('Joining an invalid green thread') + + def start(self): + self._green_thread = self.spawn(self._run) + + def copy(self): + return NvpApiRequestEventlet( + self._api_client, self._url, self._method, self._body, + self._headers, self._request_timeout, self._retries, + self._auto_login, self._redirects, self._http_timeout) + + @property + def request_error(self): + return self._request_error + + def _run(self): + if self._request_timeout: + # No timeout exception escapes the with block. + with timeout.Timeout(self._request_timeout, False): + return self._handle_request() + + lg.info('Request timeout handling request.') + self._request_error = Exception('Request timeout') + return None + else: + return self._handle_request() + + def _request_str(self, conn, url): + return "%s %s/%s" % (self._method, _conn_str(conn), url) + + def _issue_request(self): + conn = self._api_client.acquire_connection() + if conn is None: + error = Exception("No API connections available") + self._request_error = error + return error + + url = self._url + lg.info("Issuing request '%s'" % self._request_str(conn, url)) + issued_time = time.time() + is_conn_error = False + try: + redirects = 0 + while (redirects <= self._redirects): + # Update connection with user specified request timeout, + # the connect timeout is usually smaller so we only set + # the request timeout after a connection is established + if conn.sock is None: + conn.connect() + conn.sock.settimeout(self._http_timeout) + elif conn.sock.gettimeout() != self._http_timeout: + conn.sock.settimeout(self._http_timeout) + + try: + conn.request(self._method, url, self._body, self._headers) + except Exception, e: + lg.info('_issue_request: conn.request() exception: %s' % e) + raise e + + response = conn.getresponse() + response.body = response.read() + response.headers = response.getheaders() + lg.info("Request '%s' complete: %s (%0.2f seconds)" + % (self._request_str(conn, url), response.status, + time.time() - issued_time)) + if response.status not in [httplib.MOVED_PERMANENTLY, + httplib.TEMPORARY_REDIRECT]: + break + elif redirects >= self._redirects: + lg.warn("Maximum redirects exceeded, aborting request") + break + redirects += 1 + conn, url = self._redirect_params(conn, response.headers) + if url is None: + response.status = httplib.INTERNAL_SERVER_ERROR + break + lg.info("Redirecting request to: %s" % \ + self._request_str(conn, url)) + + # If we receive any of these responses, then our server did not + # process our request and may be in an errored state. Raise an + # exception, which will cause the the conn to be released with + # is_conn_error == True which puts the conn on the back of the + # client's priority queue. + if response.status >= 500: + lg.warn("API Request '%s %s' received: %s" + % (self._method, self._url, response.status)) + raise Exception('Server error return: %s' % + response.status) + return response + except Exception, e: + if isinstance(e, httplib.BadStatusLine): + msg = "Invalid server response" + else: + msg = unicode(e) + lg.warn("Request '%s' failed: %s (%0.2f seconds)" + % (self._request_str(conn, url), msg, + time.time() - issued_time)) + self._request_error = e + is_conn_error = True + return e + finally: + self._api_client.release_connection(conn, is_conn_error) + + def _redirect_params(self, conn, headers): + url = None + for name, value in headers: + if name.lower() == "location": + url = value + break + if not url: + lg.warn("Received redirect status without location header field") + return (conn, None) + # Accept location with the following format: + # 1. /path, redirect to same node + # 2. scheme://hostname:[port]/path where scheme is https or http + # Reject others + # 3. e.g. relative paths, unsupported scheme, unspecified host + result = urlparse.urlparse(url) + if not result.scheme and not result.hostname and result.path: + if result.path[0] == "/": + if result.query: + url = "%s?%s" % (result.path, result.query) + else: + url = result.path + return (conn, url) # case 1 + else: + lg.warn("Received invalid redirect location: %s" % url) + return (conn, None) # case 3 + elif result.scheme not in ["http", "https"] or not result.hostname: + lg.warn("Received malformed redirect location: %s" % url) + return (conn, None) # case 3 + # case 2, redirect location includes a scheme + # so setup a new connection and authenticate + use_https = result.scheme == "https" + api_providers = [(result.hostname, result.port, use_https)] + api_client = client_eventlet.NvpApiClientEventlet( + api_providers, self._api_client.user, self._api_client.password, + use_https=use_https) + api_client.wait_for_login() + if api_client.auth_cookie: + self._headers["Cookie"] = api_client.auth_cookie + else: + self._headers["Cookie"] = "" + conn = api_client.acquire_connection() + if result.query: + url = "%s?%s" % (result.path, result.query) + else: + url = result.path + return (conn, url) + + def _handle_request(self): + attempt = 0 + response = None + while response is None and attempt <= self._retries: + attempt += 1 + + if self._auto_login and self._api_client.need_login: + self._api_client.wait_for_login() + + if self._api_client.auth_cookie and "Cookie" not in self._headers: + self._headers["Cookie"] = self._api_client.auth_cookie + + req = self.spawn(self._issue_request).wait() + # automatically raises any exceptions returned. + lg.debug('req: %s' % type(req)) + + if isinstance(req, httplib.HTTPResponse): + if (req.status == httplib.UNAUTHORIZED + or req.status == httplib.FORBIDDEN): + self._api_client.need_login = True + if attempt <= self._retries: + continue + # else fall through to return the error code + + lg.debug("API Request '%s %s' complete: %s" + % (self._method, self._url, req.status)) + self._request_error = None + response = req + else: + lg.info('_handle_request: caught an error - %s' % req) + self._request_error = req + + lg.debug('_handle_request: response - %s' % response) + return response + + +class NvpLoginRequestEventlet(NvpApiRequestEventlet): + def __init__(self, nvp_client, user, password): + headers = {"Content-Type": "application/x-www-form-urlencoded"} + body = urllib.urlencode({"username": user, "password": password}) + NvpApiRequestEventlet.__init__( + self, nvp_client, "/ws.v1/login", "POST", body, headers, + auto_login=False) + + def session_cookie(self): + if self.successful(): + return self.value.getheader("Set-Cookie") + return None + + +class NvpGetApiProvidersRequestEventlet(NvpApiRequestEventlet): + def __init__(self, nvp_client): + url = "/ws.v1/control-cluster/node?fields=roles" + NvpApiRequestEventlet.__init__( + self, nvp_client, url, "GET", auto_login=True) + + def api_providers(self): + """Parse api_providers from response. + + Returns: api_providers in [(host, port, is_ssl), ...] format + """ + def _provider_from_listen_addr(addr): + # (pssl|ptcp):: => (host, port, is_ssl) + parts = addr.split(':') + return (parts[1], int(parts[2]), parts[0] == 'pssl') + + try: + if self.successful(): + ret = [] + body = json.loads(self.value.body) + for node in body.get('results', []): + for role in node.get('roles', []): + if role.get('role') == 'api_provider': + addr = role.get('listen_addr') + if addr: + ret.append(_provider_from_listen_addr(addr)) + return ret + except Exception, e: + lg.warn("Failed to parse API provider: %s" % e) + # intentionally fall through + return None + + +class NvpGenericRequestEventlet(NvpApiRequestEventlet): + def __init__(self, nvp_client, method, url, body, content_type, + auto_login=False, + request_timeout=DEFAULT_REQUEST_TIMEOUT, + http_timeout=DEFAULT_HTTP_TIMEOUT, + retries=DEFAULT_RETRIES, + redirects=DEFAULT_REDIRECTS): + headers = {"Content-Type": content_type} + + NvpApiRequestEventlet.__init__( + self, nvp_client, url, method, body, headers, + request_timeout=request_timeout, retries=retries, + auto_login=auto_login, redirects=redirects, + http_timeout=http_timeout) + + def session_cookie(self): + if self.successful(): + return self.value.getheader("Set-Cookie") + return None + + +# Register subclasses +request.NvpApiRequest.register(NvpApiRequestEventlet) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/cli.py b/quantum/plugins/nicira/nicira_nvp_plugin/cli.py new file mode 100644 index 00000000000..9e548e02c30 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/cli.py @@ -0,0 +1,131 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 optparse import OptionParser + +import gettext +import logging +import os +import sys + +gettext.install('nvp-plugin-cli', unicode=1) + +from QuantumPlugin import NvpPlugin as QuantumManager +import nvplib + +logging.basicConfig(level=logging.INFO) +LOG = logging.getLogger('nvp-plugin-cli') + + +def print_help(): + """Help for CLI""" + print "\nNVP Plugin Commands:" + for key in COMMANDS.keys(): + print " %s %s" % (key, + " ".join(["<%s>" % y for y in COMMANDS[key]["args"]])) + + +def build_args(cmd, cmdargs, arglist): + """Building the list of args for a particular CLI""" + args = [] + orig_arglist = arglist[:] + try: + for cmdarg in cmdargs: + args.append(arglist[0]) + del arglist[0] + except: + LOG.error("Not enough arguments for \"%s\" (expected: %d, got: %d)" % ( + cmd, len(cmdargs), len(orig_arglist))) + print "Usage:\n %s %s" % (cmd, + " ".join(["<%s>" % y for y in COMMANDS[cmd]["args"]])) + sys.exit() + if len(arglist) > 0: + LOG.error("Too many arguments for \"%s\" (expected: %d, got: %d)" % ( + cmd, len(cmdargs), len(orig_arglist))) + print "Usage:\n %s %s" % (cmd, + " ".join(["<%s>" % y for y in COMMANDS[cmd]["args"]])) + sys.exit() + return args + + +def check_config(manager): + """A series of checks to make sure the plugin is correctly configured.""" + checks = [{"function": nvplib.check_default_transport_zone, + "desc": "Transport zone check:"}] + any_failed = False + for c in checks: + result, msg = "PASS", "" + try: + c["function"]() + except Exception, e: + any_failed = True + result = "FAIL" + msg = "(%s)" % str(e) + print "%s %s%s" % (c["desc"], result, msg) + sys.exit({False: 0, True: 1}[any_failed]) + + +COMMANDS = { + "check_config": { + "need_login": True, + "func": check_config, + "args": []}, + } + + +def main(): + usagestr = "Usage: %prog [OPTIONS] [args]" + PARSER = OptionParser(usage=usagestr) + PARSER.add_option("-v", "--verbose", dest="verbose", + action="store_true", default=False, help="turn on verbose logging") + PARSER.add_option("-c", "--configfile", dest="configfile", + type="string", default="/etc/quantum/plugins/nvp/nvp.ini", + help="nvp plugin config file path (nvp.ini)") + options, args = PARSER.parse_args() + + loglevel = logging.INFO + if options.verbose: + loglevel = logging.DEBUG + + LOG.setLevel(loglevel) + + if len(args) < 1: + PARSER.print_help() + print_help() + sys.exit(1) + + CMD = args[0] + if CMD not in COMMANDS.keys(): + LOG.error("Unknown command: %s" % CMD) + print_help() + sys.exit(1) + + args = build_args(CMD, COMMANDS[CMD]["args"], args[1:]) + + LOG.debug("Executing command \"%s\" with args: %s" % (CMD, args)) + + manager = None + if COMMANDS[CMD]["need_login"] == True: + if not os.path.exists(options.configfile): + LOG.error("NVP plugin configuration file \"%s\" doesn't exist!" % + options.configfile) + sys.exit(1) + manager = QuantumManager(options.configfile, loglevel, cli=True) + + COMMANDS[CMD]["func"](manager, *args) + + sys.exit(0) + +if __name__ == "__main__": + main() diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py new file mode 100644 index 00000000000..74ba648f8e8 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py @@ -0,0 +1,402 @@ +# Copyright 2012 Nicira Networks, 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. +# +# @author: Brad Hall, Nicira Networks, Inc. + +from quantum.common import exceptions as exception +import json +import logging +import NvpApiClient + +LOG = logging.getLogger("nvplib") +LOG.setLevel(logging.INFO) + + +def do_single_request(*args, **kwargs): + """Issue a request to a specified controller if specified via kwargs + (controller=).""" + controller = kwargs["controller"] + LOG.debug("Issuing request to controller: %s" % controller.name) + return controller.api_client.request(*args) + + +def check_default_transport_zone(c): + """Make sure the default transport zone specified in the config exists""" + msg = [] + # This will throw an exception on failure and that's ok since it will + # just propogate to the cli. + resp = do_single_request("GET", + "/ws.v1/transport-zone?uuid=%s" % c.default_tz_uuid, + controller=c) + result = json.loads(resp) + if int(result["result_count"]) == 0: + msg.append("Unable to find zone \"%s\" for controller \"%s\"" % + (c.default_tz_uuid, c.name)) + if len(msg) > 0: + raise Exception(' '.join(msg)) + + +def check_tenant(controller, net_id, tenant_id): + """Return true if the tenant "owns" this network""" + net = get_network(controller, net_id) + for t in net["tags"]: + if t["scope"] == "os_tid" and t["tag"] == tenant_id: + return True + return False + +# ------------------------------------------------------------------- +# Network functions +# ------------------------------------------------------------------- + + +def get_network(controller, net_id): + path = "/ws.v1/lswitch/%s" % net_id + try: + resp_obj = do_single_request("GET", path, controller=controller) + network = json.loads(resp_obj) + except NvpApiClient.ResourceNotFound as e: + raise exception.NetworkNotFound(net_id=net_id) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + LOG.debug("Got network \"%s\": %s" % (net_id, network)) + return network + + +def create_lswitch(controller, lswitch_obj): + LOG.debug("Creating lswitch: %s" % lswitch_obj) + # Warn if no tenant is specified + found = "os_tid" in [x["scope"] for x in lswitch_obj["tags"]] + if not found: + LOG.warn("No tenant-id tag specified in logical switch: %s" % ( + lswitch_obj)) + uri = "/ws.v1/lswitch" + try: + resp_obj = do_single_request("POST", uri, + json.dumps(lswitch_obj), + controller=controller) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + r = json.loads(resp_obj) + d = {} + d["net-id"] = r["uuid"] + d["net-name"] = r["display_name"] + LOG.debug("Created logical switch: %s" % d["net-id"]) + return d + + +def update_network(controller, network, **kwargs): + uri = "/ws.v1/lswitch/" + network + lswitch_obj = {} + if "name" in kwargs: + lswitch_obj["display_name"] = kwargs["name"] + try: + resp_obj = do_single_request("PUT", uri, + json.dumps(lswitch_obj), controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Network not found, Error: %s" % str(e)) + raise exception.NetworkNotFound(net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + obj = json.loads(resp_obj) + return obj + + +def get_all_networks(controller, tenant_id, networks): + """Append the quantum network uuids we can find in the given controller to + "networks" + """ + uri = "/ws.v1/lswitch?fields=*&tag=%s&tag_scope=os_tid" % tenant_id + try: + resp_obj = do_single_request("GET", uri, controller=controller) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + if not resp_obj: + return [] + lswitches = json.loads(resp_obj)["results"] + for lswitch in lswitches: + net_id = lswitch["uuid"] + if net_id not in [x["net-id"] for x in networks]: + networks.append({"net-id": net_id, + "net-name": lswitch["display_name"]}) + return networks + + +def query_networks(controller, tenant_id, fields="*", tags=None): + uri = "/ws.v1/lswitch?fields=%s" % fields + if tags: + for t in tags: + uri += "&tag=%s&tag_scope=%s" % (t[0], t[1]) + try: + resp_obj = do_single_request("GET", uri, controller=controller) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + if not resp_obj: + return [] + lswitches = json.loads(resp_obj)["results"] + nets = [{'net-id': lswitch["uuid"], + 'net-name': lswitch["display_name"]} + for lswitch in lswitches] + return nets + + +def delete_network(controller, network): + delete_networks(controller, [network]) + + +def delete_networks(controller, networks): + for network in networks: + path = "/ws.v1/lswitch/%s" % network + + try: + do_single_request("DELETE", path, controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Network not found, Error: %s" % str(e)) + raise exception.NetworkNotFound(net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + +def create_network(tenant_id, net_name, **kwargs): + controller = kwargs["controller"] + + transport_zone = kwargs.get("transport_zone", + controller.default_tz_uuid) + transport_type = kwargs.get("transport_type", "gre") + lswitch_obj = {"display_name": net_name, + "transport_zones": [ + {"zone_uuid": transport_zone, + "transport_type": transport_type} + ], + "tags": [{"tag": tenant_id, "scope": "os_tid"}] + } + + net = create_lswitch(controller, lswitch_obj) + net['net-op-status'] = "UP" + return net + +#--------------------------------------------------------------------- +# Port functions +#--------------------------------------------------------------------- + + +def get_port_stats(controller, network_id, port_id): + try: + do_single_request("GET", "/ws.v1/lswitch/%s" % (network_id), + controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Network not found, Error: %s" % str(e)) + raise exception.NetworkNotFound(net_id=network_id) + try: + path = "/ws.v1/lswitch/%s/lport/%s/statistic" % (network_id, port_id) + resp = do_single_request("GET", path, controller=controller) + stats = json.loads(resp) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port_id, net_id=network_id) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + LOG.debug("Returning stats for port \"%s\" on \"%s\": %s" % (port_id, + network_id, + stats)) + return stats + + +def check_port_state(state): + if state not in ["ACTIVE", "DOWN"]: + LOG.error("Invalid port state (ACTIVE and " \ + "DOWN are valid states): %s" % state) + raise exception.StateInvalid(port_state=state) + + +def query_ports(controller, network, relations=None, fields="*", filters=None): + uri = "/ws.v1/lswitch/" + network + "/lport?" + if relations: + uri += "relations=%s" % relations + uri += "&fields=%s" % fields + if filters and "attachment" in filters: + uri += "&attachment_vif_uuid=%s" % filters["attachment"] + try: + resp_obj = do_single_request("GET", uri, + controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Network not found, Error: %s" % str(e)) + raise exception.NetworkNotFound(net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + return json.loads(resp_obj)["results"] + + +def delete_port(controller, network, port): + uri = "/ws.v1/lswitch/" + network + "/lport/" + port + try: + do_single_request("DELETE", uri, controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port or Network not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port, net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + +def delete_all_ports(controller, ls_uuid): + res = do_single_request("GET", + "/ws.v1/lswitch/%s/lport?fields=uuid" % ls_uuid, + controller=controller) + res = json.loads(res) + for r in res["results"]: + do_single_request("DELETE", + "/ws.v1/lswitch/%s/lport/%s" % (ls_uuid, r["uuid"]), + controller=controller) + + +def get_port(controller, network, port, relations=None): + uri = "/ws.v1/lswitch/" + network + "/lport/" + port + "?" + if relations: + uri += "relations=%s" % relations + try: + resp_obj = do_single_request("GET", uri, controller=controller) + port = json.loads(resp_obj) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port or Network not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port, net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + return port + + +def plug_interface(controller, network, port, type, attachment=None): + uri = "/ws.v1/lswitch/" + network + "/lport/" + port + "/attachment" + + lport_obj = {} + if attachment: + lport_obj["vif_uuid"] = attachment + + lport_obj["type"] = type + try: + resp_obj = do_single_request("PUT", uri, + json.dumps(lport_obj), controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port or Network not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port, net_id=network) + except NvpApiClient.Conflict as e: + LOG.error("Conflict while making attachment to port, " \ + "Error: %s" % str(e)) + raise exception.AlreadyAttached(att_id=attachment, + port_id=port, + net_id=network, + att_port_id="UNKNOWN") + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + result = json.dumps(resp_obj) + return result + + +def unplug_interface(controller, network, port): + uri = "/ws.v1/lswitch/" + network + "/lport/" + port + "/attachment" + lport_obj = {"type": "NoAttachment"} + try: + resp_obj = do_single_request("PUT", + uri, json.dumps(lport_obj), controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port or Network not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port, net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + return json.loads(resp_obj) + + +def update_port(network, port_id, **params): + controller = params["controller"] + lport_obj = {} + + if "state" in params: + state = params["state"] + check_port_state(state) + admin_status = True + if state == "DOWN": + admin_status = False + lport_obj["admin_status_enabled"] = admin_status + + uri = "/ws.v1/lswitch/" + network + "/lport/" + port_id + try: + resp_obj = do_single_request("PUT", uri, + json.dumps(lport_obj), controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port or Network not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port_id, net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + obj = json.loads(resp_obj) + obj["port-op-status"] = get_port_status(controller, network, obj["uuid"]) + return obj + + +def create_port(tenant, network, port_init_state, **params): + # Check initial state -- this throws an exception if the port state is + # invalid + check_port_state(port_init_state) + + controller = params["controller"] + + ls_uuid = network + + admin_status = True + if port_init_state == "DOWN": + admin_status = False + lport_obj = {"admin_status_enabled": admin_status} + + path = "/ws.v1/lswitch/" + ls_uuid + "/lport" + try: + resp_obj = do_single_request("POST", path, + json.dumps(lport_obj), controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Network not found, Error: %s" % str(e)) + raise exception.NetworkNotFound(net_id=network) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + + result = json.loads(resp_obj) + result['port-op-status'] = get_port_status(controller, ls_uuid, + result['uuid']) + return result + + +def get_port_status(controller, lswitch_id, port_id): + """Retrieve the operational status of the port""" + # Make sure the network exists first + try: + do_single_request("GET", "/ws.v1/lswitch/%s" % (lswitch_id), + controller=controller) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Network not found, Error: %s" % str(e)) + raise exception.NetworkNotFound(net_id=lswitch_id) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + try: + r = do_single_request("GET", + "/ws.v1/lswitch/%s/lport/%s/status" % (lswitch_id, port_id), + controller=controller) + r = json.loads(r) + except NvpApiClient.ResourceNotFound as e: + LOG.error("Port not found, Error: %s" % str(e)) + raise exception.PortNotFound(port_id=port_id, net_id=lswitch_id) + except NvpApiClient.NvpApiException as e: + raise exception.QuantumException() + if r['link_status_up'] is True: + return "UP" + else: + return "DOWN" diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_check.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_check.py new file mode 100644 index 00000000000..3bc0af91366 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_check.py @@ -0,0 +1,36 @@ +# Copyright 2012 Nicira Networks, 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. +# +# @author: Brad Hall, Nicira Networks, Inc. + +import logging +import unittest + +from nicira_nvp_plugin.QuantumPlugin import NvpPlugin +from nicira_nvp_plugin import nvplib + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("test_check") + + +class NvpTests(unittest.TestCase): + def setUp(self): + self.quantum = NvpPlugin() + + def tearDown(self): + pass + + # These nvplib functions will throw an exception if the check fails + def test_check_default_transport_zone(self): + nvplib.check_default_transport_zone(self.quantum.controller) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_config.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_config.py new file mode 100644 index 00000000000..2c0085c70ca --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_config.py @@ -0,0 +1,239 @@ +# Copyright 2012 Nicira Networks, 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 unittest +import StringIO +import ConfigParser +from nicira_nvp_plugin.QuantumPlugin import parse_config +from nicira_nvp_plugin.QuantumPlugin import NVPCluster + + +class ConfigParserTest(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_nvp_config_000(self): + nvpc = NVPCluster('cluster1') + for f in [ + ( + 'default_tz_id1', 'ip1', 'port1', 'user1', 'passwd1', 42, 43, + 44, 45), + ( + 'default_tz_id1', 'ip2', 'port2', 'user2', 'passwd2', 42, 43, + 44, 45), + ( + 'default_tz_id1', 'ip3', 'port3', 'user3', 'passwd3', 42, 43, + 44, 45), + ]: + nvpc.add_controller(*f) + + self.assertTrue(nvpc.name == 'cluster1') + self.assertTrue(len(nvpc.controllers) == 3) + + def test_old_config_parser_old_style(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +DEFAULT_TZ_UUID = +NVP_CONTROLLER_IP = +PORT = +USER = +PASSWORD = +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + + self.assertTrue(cluster1.name == 'cluster1') + self.assertTrue( + cluster1.controllers[0]['default_tz_uuid'] == '') + self.assertTrue( + cluster1.controllers[0]['port'] == '') + self.assertTrue( + cluster1.controllers[0]['user'] == '') + self.assertTrue( + cluster1.controllers[0]['password'] == '') + self.assertTrue( + cluster1.controllers[0]['request_timeout'] == 30) + self.assertTrue( + cluster1.controllers[0]['http_timeout'] == 10) + self.assertTrue( + cluster1.controllers[0]['retries'] == 2) + self.assertTrue( + cluster1.controllers[0]['redirects'] == 2) + + def test_old_config_parser_new_style(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +DEFAULT_TZ_UUID = +NVP_CONTROLLER_CONNECTIONS = CONNECTION1 +CONNECTION1 = 10.0.0.1:4242:admin:admin:42:43:44:45 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + + self.assertTrue(cluster1.name == 'cluster1') + self.assertTrue( + cluster1.controllers[0]['default_tz_uuid'] == '') + self.assertTrue( + cluster1.controllers[0]['port'] == '4242') + self.assertTrue( + cluster1.controllers[0]['user'] == 'admin') + self.assertTrue( + cluster1.controllers[0]['password'] == 'admin') + self.assertTrue( + cluster1.controllers[0]['request_timeout'] == 42) + self.assertTrue( + cluster1.controllers[0]['http_timeout'] == 43) + self.assertTrue( + cluster1.controllers[0]['retries'] == 44) + self.assertTrue( + cluster1.controllers[0]['redirects'] == 45) + + def test_old_config_parser_both_styles(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +NVP_CONTROLLER_IP = +PORT = +USER = +PASSWORD = +DEFAULT_TZ_UUID = +NVP_CONTROLLER_CONNECTIONS = CONNECTION1 +CONNECTION1 = 10.0.0.1:4242:admin:admin:42:43:44:45 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + + self.assertTrue(cluster1.name == 'cluster1') + self.assertTrue( + cluster1.controllers[0]['default_tz_uuid'] == '') + self.assertTrue( + cluster1.controllers[0]['port'] == '4242') + self.assertTrue( + cluster1.controllers[0]['user'] == 'admin') + self.assertTrue( + cluster1.controllers[0]['password'] == 'admin') + self.assertTrue( + cluster1.controllers[0]['request_timeout'] == 42) + self.assertTrue( + cluster1.controllers[0]['http_timeout'] == 43) + self.assertTrue( + cluster1.controllers[0]['retries'] == 44) + self.assertTrue( + cluster1.controllers[0]['redirects'] == 45) + + def test_old_config_parser_both_styles(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +NVP_CONTROLLER_IP = +PORT = +USER = +PASSWORD = +DEFAULT_TZ_UUID = +NVP_CONTROLLER_CONNECTIONS = CONNECTION1 +CONNECTION1 = 10.0.0.1:4242:admin:admin:42:43:44:45 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + + self.assertTrue(cluster1.name == 'cluster1') + self.assertTrue( + cluster1.controllers[0]['default_tz_uuid'] == '') + self.assertTrue( + cluster1.controllers[0]['port'] == '4242') + self.assertTrue( + cluster1.controllers[0]['user'] == 'admin') + self.assertTrue( + cluster1.controllers[0]['password'] == 'admin') + self.assertTrue( + cluster1.controllers[0]['request_timeout'] == 42) + self.assertTrue( + cluster1.controllers[0]['http_timeout'] == 43) + self.assertTrue( + cluster1.controllers[0]['retries'] == 44) + self.assertTrue( + cluster1.controllers[0]['redirects'] == 45) + + def test_failover_time(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +DEFAULT_TZ_UUID = +NVP_CONTROLLER_IP = +PORT = 443 +USER = admin +PASSWORD = admin +FAILOVER_TIME = 10 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + self.assertTrue(plugin_config['failover_time'] == '10') + + def test_failover_time_new_style(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +DEFAULT_TZ_UUID = +NVP_CONTROLLER_CONNECTIONS = CONNECTION1 +CONNECTION1 = 10.0.0.1:4242:admin:admin:42:43:44:45 +FAILOVER_TIME = 10 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + self.assertTrue(plugin_config['failover_time'] == '10') + + def test_concurrent_connections_time(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +DEFAULT_TZ_UUID = +NVP_CONTROLLER_IP = +PORT = 443 +USER = admin +PASSWORD = admin +CONCURRENT_CONNECTIONS = 5 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + self.assertTrue(plugin_config['concurrent_connections'] == '5') + + def test_concurrent_connections_time_new_style(self): + config = StringIO.StringIO(''' +[DEFAULT] +[NVP] +DEFAULT_TZ_UUID = +NVP_CONTROLLER_CONNECTIONS = CONNECTION1 +CONNECTION1 = 10.0.0.1:4242:admin:admin:42:43:44:45 +CONCURRENT_CONNECTIONS = 5 +''') + cp = ConfigParser.ConfigParser() + cp.readfp(config) + cluster1, plugin_config = parse_config(cp) + self.assertTrue(plugin_config['concurrent_connections'] == '5') + +if __name__ == '__main__': + unittest.main() diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_network.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_network.py new file mode 100644 index 00000000000..8eacdc24827 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_network.py @@ -0,0 +1,197 @@ +# Copyright 2012 Nicira Networks, 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. +# +# @author: Somik Behera, Nicira Networks, Inc. +# @author: Brad Hall, Nicira Networks, Inc. + +import json +import logging +import os +import unittest +from quantum.common import exceptions as exception +from nicira_nvp_plugin.QuantumPlugin import NvpPlugin +from nicira_nvp_plugin import NvpApiClient +from nicira_nvp_plugin import nvplib + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("test_network") + + +class NvpTests(unittest.TestCase): + def setUp(self): + self.quantum = NvpPlugin() + self.BRIDGE_TZ_UUID = self._create_tz("bridge") + self.DEFAULT_TZ_UUID = self._create_tz("default") + + self.nets = [] + self.ports = [] + + def tearDown(self): + self._delete_tz(self.BRIDGE_TZ_UUID) + self._delete_tz(self.DEFAULT_TZ_UUID) + + for tenant, net, port in self.ports: + self.quantum.delete_port(tenant, net, port) + for tenant, net in self.nets: + self.quantum.delete_network(tenant, net) + + def _create_tz(self, name): + post_uri = "/ws.v1/transport-zone" + body = {"display_name": name, + "tags": [{"tag": "plugin-test"}]} + try: + resp_obj = self.quantum.api_client.request("POST", + post_uri, json.dumps(body)) + except NvpApiClient.NvpApiException as e: + print("Unknown API Error: %s" % str(e)) + raise exception.QuantumException() + return json.loads(resp_obj)["uuid"] + + def _delete_tz(self, uuid): + post_uri = "/ws.v1/transport-zone/%s" % uuid + try: + resp_obj = self.quantum.api_client.request("DELETE", post_uri) + except NvpApiClient.NvpApiException as e: + LOG.error("Unknown API Error: %s" % str(e)) + raise exception.QuantumException() + + def test_create_multi_networks(self): + + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + resp2 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantC") + resp3 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantD") + net_id = resp["net-id"] + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id1 = resp["port-id"] + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id1) + old_vic = resp["attachment"] + self.assertTrue(old_vic == "None") + + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id1, + "nova-instance-test-%s" % os.getpid()) + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id1) + new_vic = resp["attachment"] + self.assertTrue(old_vic != new_vic) + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id2 = resp["port-id"] + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id2) + old_vic2 = resp["attachment"] + self.assertTrue(old_vic2 == "None") + + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id2, + "nova-instance-test2-%s" % os.getpid()) + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id2) + new_vic = resp["attachment"] + self.assertTrue(old_vic2 != new_vic) + + resp = self.quantum.get_all_ports("quantum-test-tenant", net_id) + + resp = self.quantum.get_network_details("quantum-test-tenant", net_id) + + resp = self.quantum.get_all_networks("quantum-test-tenant") + + resp = self.quantum.delete_port("quantum-test-tenant", net_id, + port_id1) + resp = self.quantum.delete_port("quantum-test-tenant", net_id, + port_id2) + self.quantum.delete_network("quantum-test-tenant", net_id) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + self.quantum.delete_network("quantum-test-tenant", resp2["net-id"]) + self.quantum.delete_network("quantum-test-tenant", resp3["net-id"]) + + def test_update_network(self): + resp = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantA") + net_id = resp["net-id"] + try: + resp = self.quantum.update_network("quantum-test-tenant", net_id, + name="new-name") + except exception.NetworkNotFound: + self.assertTrue(False) + + self.assertTrue(resp["net-name"] == "new-name") + + def test_negative_delete_networks(self): + try: + self.quantum.delete_network("quantum-test-tenant", "xxx-no-net-id") + except exception.NetworkNotFound: + self.assertTrue(True) + + def test_negative_get_network_details(self): + try: + self.quantum.get_network_details("quantum-test-tenant", + "xxx-no-net-id") + except exception.NetworkNotFound: + self.assertTrue(True) + + def test_negative_update_network(self): + try: + self.quantum.update_network("quantum-test-tenant", "xxx-no-net-id", + name="new-name") + except exception.NetworkNotFound: + self.assertTrue(True) + + def test_get_all_networks(self): + networks = self.quantum.get_all_networks("quantum-test-tenant") + num_nets = len(networks) + + # Make sure we only get back networks with the specified tenant_id + unique_tid = "tenant-%s" % os.getpid() + # Add a network that we shouldn't get back + resp = self.quantum.create_custom_network( + "another_tid", "another_tid_network", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + self.nets.append(("another_tid", net_id)) + # Add 3 networks that we should get back + for i in [1, 2, 3]: + resp = self.quantum.create_custom_network( + unique_tid, "net-%s" % str(i), + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + self.nets.append((unique_tid, net_id)) + networks = self.quantum.get_all_networks(unique_tid) + self.assertTrue(len(networks) == 3) + + def test_delete_nonexistent_network(self): + try: + nvplib.delete_network(self.quantum.controller, + "my-non-existent-network") + except exception.NetworkNotFound: + return + # shouldn't be reached + self.assertTrue(False) + + def test_query_networks(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + self.nets.append(("quantum-test-tenant", net_id)) + nets = nvplib.query_networks(self.quantum.controller, + "quantum-test-tenant") diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_common.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_common.py new file mode 100644 index 00000000000..d84e9efd54c --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_common.py @@ -0,0 +1,40 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 httplib +import unittest + +import nicira_nvp_plugin.api_client.common as naco + + +class NvpApiCommonTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_conn_str(self): + conn = httplib.HTTPSConnection('localhost', 4242, timeout=0) + self.assertTrue( + naco._conn_str(conn) == 'https://localhost:4242') + + conn = httplib.HTTPConnection('localhost', 4242, timeout=0) + self.assertTrue( + naco._conn_str(conn) == 'http://localhost:4242') + + with self.assertRaises(TypeError): + naco._conn_str('not an httplib.HTTPSConnection') diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request.py new file mode 100644 index 00000000000..56aad5c1f7f --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request.py @@ -0,0 +1,36 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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 logging +import unittest +from eventlet.green import urllib2 + +logging.basicConfig(level=logging.DEBUG) +lg = logging.getLogger("test_nvp_api_request") + +REQUEST_TIMEOUT = 1 + + +def fetch(url): + return urllib2.urlopen(url).read() + + +class NvpApiRequestTest(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request_eventlet.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request_eventlet.py new file mode 100644 index 00000000000..dc8cc797779 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request_eventlet.py @@ -0,0 +1,353 @@ +# Copyright (C) 2009-2012 Nicira Networks, 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. + + +# System +import httplib +import logging +import new +import random +import unittest + +# Third party +import eventlet +from eventlet.green import urllib2 +from mock import Mock +from mock import patch + +# Local +import nicira_nvp_plugin.api_client.client_eventlet as nace +import nicira_nvp_plugin.api_client.request_eventlet as nare + +logging.basicConfig(level=logging.DEBUG) +lg = logging.getLogger("test_nvp_api_request_eventlet") + +REQUEST_TIMEOUT = 1 + + +def fetch(url): + return urllib2.urlopen(url).read() + + +class NvpApiRequestEventletTest(unittest.TestCase): + + def setUp(self): + + self.client = nace.NvpApiClientEventlet( + [("127.0.0.1", 4401, True)], "admin", "admin") + self.url = "/ws.v1/_debug" + self.req = nare.NvpApiRequestEventlet( + self.client, self.url) + + def tearDown(self): + self.client = None + self.req = None + + def test_construct_eventlet_api_request(self): + e = nare.NvpApiRequestEventlet(self.client, self.url) + self.assertTrue(e is not None) + + def test_apirequest_spawn(self): + def x(id): + eventlet.greenthread.sleep(random.random()) + lg.info('spawned: %d' % id) + + for i in range(10): + nare.NvpApiRequestEventlet._spawn(x, i) + + def test_apirequest_start(self): + for i in range(10): + a = nare.NvpApiRequestEventlet( + self.client, self.url, request_timeout=0.1) + a._handle_request = Mock() + a.start() + eventlet.greenthread.sleep(0.1) + logging.info('_handle_request called: %s' % + a._handle_request.called) + nare.NvpApiRequestEventlet.joinall() + + def test_join_with_handle_request(self): + self.req._handle_request = Mock() + self.req.start() + self.req.join() + self.assertTrue(self.req._handle_request.called) + + def test_join_without_handle_request(self): + self.req._handle_request = Mock() + self.req.join() + self.assertFalse(self.req._handle_request.called) + + def test_copy(self): + req = self.req.copy() + for att in [ + '_api_client', '_url', '_method', '_body', '_headers', + '_http_timeout', '_request_timeout', '_retries', + '_redirects', '_auto_login']: + self.assertTrue(getattr(req, att) is getattr(self.req, att)) + + def test_request_error(self): + self.assertTrue(self.req.request_error is None) + + def test_run_and_handle_request(self): + self.req._request_timeout = None + self.req._handle_request = Mock() + self.req.start() + self.req.join() + self.assertTrue(self.req._handle_request.called) + + def test_run_and_timeout(self): + def my_handle_request(self): + lg.info('my_handle_request() self: %s' % self) + lg.info('my_handle_request() dir(self): %s' % dir(self)) + eventlet.greenthread.sleep(REQUEST_TIMEOUT * 2) + + self.req._request_timeout = REQUEST_TIMEOUT + self.req._handle_request = new.instancemethod( + my_handle_request, self.req, nare.NvpApiRequestEventlet) + self.req.start() + self.assertTrue(self.req.join() is None) + + def prep_issue_request(self): + mysock = Mock() + mysock.gettimeout.return_value = 4242 + + myresponse = Mock() + myresponse.read.return_value = 'body' + myresponse.getheaders.return_value = 'headers' + myresponse.status = httplib.MOVED_PERMANENTLY + + myconn = Mock() + myconn.request.return_value = None + myconn.sock = mysock + myconn.getresponse.return_value = myresponse + myconn.__str__ = Mock() + myconn.__str__.return_value = 'myconn string' + + req = self.req + req._request_timeout = REQUEST_TIMEOUT = 1 + req._redirect_params = Mock() + req._redirect_params.return_value = (myconn, 'url') + req._request_str = Mock() + req._request_str.return_value = 'http://cool/cool' + + client = self.client + client.need_login = False + client._auto_login = False + client._auth_cookie = False + client.acquire_connection = Mock() + client.acquire_connection.return_value = myconn + client.release_connection = Mock() + + return (mysock, myresponse, myconn) + + def test_issue_request_trigger_exception(self): + (mysock, myresponse, myconn) = self.prep_issue_request() + self.client.acquire_connection.return_value = None + + self.req._issue_request() + lg.info('request_error: %s' % self.req._request_error) + self.assertTrue(isinstance(self.req._request_error, Exception)) + self.assertTrue(self.client.acquire_connection.called) + + def test_issue_request_handle_none_sock(self): + (mysock, myresponse, myconn) = self.prep_issue_request() + myconn.sock = None + self.req.start() + self.assertTrue(self.req.join() is None) + self.assertTrue(self.client.acquire_connection.called) + + def test_issue_request_exceed_maximum_retries(self): + (mysock, myresponse, myconn) = self.prep_issue_request() + self.req.start() + self.assertTrue(self.req.join() is None) + self.assertTrue(self.client.acquire_connection.called) + + def test_issue_request_trigger_non_redirect(self): + (mysock, myresponse, myconn) = self.prep_issue_request() + myresponse.status = httplib.OK + self.req.start() + self.assertTrue(self.req.join() is None) + self.assertTrue(self.client.acquire_connection.called) + + def test_issue_request_trigger_internal_server_error(self): + (mysock, myresponse, myconn) = self.prep_issue_request() + self.req._redirect_params.return_value = (myconn, None) + self.req.start() + self.assertTrue(self.req.join() is None) + self.assertTrue(self.client.acquire_connection.called) + + def test_redirect_params_break_on_location(self): + myconn = Mock() + (conn, retval) = self.req._redirect_params( + myconn, [('location', None)]) + self.assertTrue(retval is None) + + def test_redirect_params_parse_a_url(self): + myconn = Mock() + (conn, retval) = self.req._redirect_params( + myconn, [('location', '/path/a/b/c')]) + self.assertTrue(retval is not None) + + def test_redirect_params_invalid_redirect_location(self): + myconn = Mock() + (conn, retval) = self.req._redirect_params( + myconn, [('location', '+path/a/b/c')]) + self.assertTrue(retval is None) + + def test_redirect_params_invalid_scheme(self): + myconn = Mock() + (conn, retval) = self.req._redirect_params( + myconn, [('location', 'invalidscheme://hostname:1/path')]) + self.assertTrue(retval is None) + + def test_redirect_params_setup_https_with_cooki(self): + with patch('nicira_nvp_plugin.api_client.client_eventlet' + '.NvpApiClientEventlet') as mock: + api_client = mock.return_value + api_client.wait_for_login.return_value = None + api_client.auth_cookie = 'mycookie' + api_client.acquire_connection.return_value = True + myconn = Mock() + (conn, retval) = self.req._redirect_params( + myconn, [('location', 'https://host:1/path')]) + + self.assertTrue(retval is not None) + self.assertTrue(api_client.wait_for_login.called) + self.assertTrue(api_client.acquire_connection.called) + + def test_redirect_params_setup_htttps_and_query(self): + with patch('nicira_nvp_plugin.api_client.client_eventlet' + '.NvpApiClientEventlet') as mock: + api_client = mock.return_value + api_client.wait_for_login.return_value = None + api_client.auth_cookie = 'mycookie' + api_client.acquire_connection.return_value = True + myconn = Mock() + (conn, retval) = self.req._redirect_params(myconn, [ + ('location', 'https://host:1/path?q=1')]) + + self.assertTrue(retval is not None) + self.assertTrue(api_client.wait_for_login.called) + self.assertTrue(api_client.acquire_connection.called) + + def test_redirect_params_setup_https_connection_no_cookie(self): + with patch('nicira_nvp_plugin.api_client.client_eventlet' + '.NvpApiClientEventlet') as mock: + api_client = mock.return_value + api_client.wait_for_login.return_value = None + api_client.auth_cookie = None + api_client.acquire_connection.return_value = True + myconn = Mock() + (conn, retval) = self.req._redirect_params(myconn, [ + ('location', 'https://host:1/path')]) + + self.assertTrue(retval is not None) + self.assertTrue(api_client.wait_for_login.called) + self.assertTrue(api_client.acquire_connection.called) + + def test_redirect_params_setup_https_and_query_no_cookie(self): + with patch('nicira_nvp_plugin.api_client.client_eventlet' + '.NvpApiClientEventlet') as mock: + api_client = mock.return_value + api_client.wait_for_login.return_value = None + api_client.auth_cookie = None + api_client.acquire_connection.return_value = True + myconn = Mock() + (conn, retval) = self.req._redirect_params( + myconn, [('location', 'https://host:1/path?q=1')]) + self.assertTrue(retval is not None) + self.assertTrue(api_client.wait_for_login.called) + self.assertTrue(api_client.acquire_connection.called) + + def test_redirect_params_path_only_with_query(self): + with patch('nicira_nvp_plugin.api_client.client_eventlet' + '.NvpApiClientEventlet') as mock: + api_client = mock.return_value + api_client.wait_for_login.return_value = None + api_client.auth_cookie = None + api_client.acquire_connection.return_value = True + myconn = Mock() + (conn, retval) = self.req._redirect_params(myconn, [ + ('location', '/path?q=1')]) + self.assertTrue(retval is not None) + + def test_handle_request_auto_login(self): + self.req._auto_login = True + self.req._api_client = Mock() + self.req._api_client.need_login = True + self.req._request_str = Mock() + self.req._request_str.return_value = 'http://cool/cool' + self.req.spawn = Mock() + self.req._handle_request() + + def test_handle_request_auto_login_unauth(self): + self.req._auto_login = True + self.req._api_client = Mock() + self.req._api_client.need_login = True + self.req._request_str = Mock() + self.req._request_str.return_value = 'http://cool/cool' + + import socket + resp = httplib.HTTPResponse(socket.socket()) + resp.status = httplib.UNAUTHORIZED + mywaiter = Mock() + mywaiter.wait = Mock() + mywaiter.wait.return_value = resp + self.req.spawn = Mock(return_value=mywaiter) + self.req._handle_request() + + # NvpLoginRequestEventlet tests. + def test_construct_eventlet_login_request(self): + r = nare.NvpLoginRequestEventlet(self.client, 'user', 'password') + self.assertTrue(r is not None) + + def test_session_cookie_session_cookie_retrieval(self): + r = nare.NvpLoginRequestEventlet(self.client, 'user', 'password') + r.successful = Mock() + r.successful.return_value = True + r.value = Mock() + r.value.get_header = Mock() + r.value.get_header.return_value = 'cool' + self.assertTrue(r.session_cookie() is not None) + + def test_session_cookie_not_retrieved(self): + r = nare.NvpLoginRequestEventlet(self.client, 'user', 'password') + r.successful = Mock() + r.successful.return_value = False + r.value = Mock() + r.value.get_header = Mock() + r.value.get_header.return_value = 'cool' + self.assertTrue(r.session_cookie() is None) + + # NvpGetApiProvidersRequestEventlet tests. + def test_construct_eventlet_get_api_providers_request(self): + r = nare.NvpGetApiProvidersRequestEventlet(self.client) + self.assertTrue(r is not None) + + def test_api_providers_none_api_providers(self): + r = nare.NvpGetApiProvidersRequestEventlet(self.client) + r.successful = Mock(return_value=False) + self.assertTrue(r.api_providers() is None) + + def test_api_providers_non_none_api_providers(self): + r = nare.NvpGetApiProvidersRequestEventlet(self.client) + r.value = Mock() + r.value.body = '''{ + "results": [ + { "roles": [ + { "role": "api_provider", + "listen_addr": "pssl:1.1.1.1:1" }]}]}''' + r.successful = Mock(return_value=True) + lg.info('%s' % r.api_providers()) + self.assertTrue(r.api_providers() is not None) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_port.py b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_port.py new file mode 100644 index 00000000000..e2569f0ac0a --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/tests/test_port.py @@ -0,0 +1,509 @@ +# Copyright 2012 Nicira Networks, 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. +# +# @author: Somik Behera, Nicira Networks, Inc. + +import json +import logging +import os +import unittest + +from quantum.common import exceptions as exception +from nicira_nvp_plugin.QuantumPlugin import NvpPlugin +from nicira_nvp_plugin import NvpApiClient +from nicira_nvp_plugin import nvplib + +logging.basicConfig(level=logging.DEBUG) +LOG = logging.getLogger("test_port") + + +class NvpTests(unittest.TestCase): + def setUp(self): + self.quantum = NvpPlugin() + self.BRIDGE_TZ_UUID = self._create_tz("bridge") + self.networks = [] + self.ports = [] + self.transport_nodes = [] + self.cis_uuids = [] + + def tearDown(self): + self._delete_tz(self.BRIDGE_TZ_UUID) + + for (net_id, p) in self.ports: + self.quantum.unplug_interface("quantum-test-tenant", net_id, p) + self.quantum.delete_port("quantum-test-tenant", net_id, p) + for n in self.networks: + self.quantum.delete_network("quantum-test-tenant", n) + for t in self.transport_nodes: + nvplib.do_single_request("DELETE", "/ws.v1/transport-node/%s" % t, + controller=self.quantum.controller) + for c in self.cis_uuids: + nvplib.do_single_request("DELETE", + "/ws.v1/cluster-interconnect-service/%s" % c, + controller=self.quantum.controller) + + def _create_tz(self, name): + post_uri = "/ws.v1/transport-zone" + body = {"display_name": name, + "tags": [{"tag": "plugin-test"}]} + try: + resp_obj = self.quantum.api_client.request("POST", + post_uri, json.dumps(body)) + except NvpApiClient.NvpApiException as e: + LOG.error("Unknown API Error: %s" % str(e)) + raise exception.QuantumException() + return json.loads(resp_obj)["uuid"] + + def _delete_tz(self, uuid): + post_uri = "/ws.v1/transport-zone/%s" % uuid + try: + resp_obj = self.quantum.api_client.request("DELETE", post_uri) + except NvpApiClient.NvpApiException as e: + LOG.error("Unknown API Error: %s" % str(e)) + raise exception.QuantumException() + + def test_create_and_delete_lots_of_ports(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + + nports = 250 + + ids = [] + for i in xrange(0, nports): + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + ids.append(port_id) + + # Test that we get the correct number of ports back + ports = self.quantum.get_all_ports("quantum-test-tenant", net_id) + self.assertTrue(len(ports) == nports) + + # Verify that each lswitch has matching tags + net = nvplib.get_network(self.quantum.controller, net_id) + tags = [] + net_tags = [t["tag"] for t in net["tags"]] + if len(tags) == 0: + tags = net_tags + else: + for t in net_tags: + self.assertTrue(t in tags) + + for port_id in ids: + resp = self.quantum.delete_port("quantum-test-tenant", net_id, + port_id) + try: + self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id) + except exception.PortNotFound: + continue + # Shouldn't be reached + self.assertFalse(True) + + self.quantum.delete_network("quantum-test-tenant", net_id) + + def test_create_and_delete_port(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + resp = self.quantum.delete_port("quantum-test-tenant", net_id, port_id) + self.quantum.delete_network("quantum-test-tenant", net_id) + + def test_create_and_delete_port_with_portsec(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + + params = {} + params["NICIRA:allowed_address_pairs"] = [ + { + "ip_address": "172.168.17.5", + "mac_address": "10:9a:dd:61:4e:89" + }, + { + "ip_address": "172.168.17.6", + "mac_address": "10:9a:dd:61:4e:88" + } + ] + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE", **params) + port_id = resp["port-id"] + resp = self.quantum.delete_port("quantum-test-tenant", net_id, port_id) + self.quantum.delete_network("quantum-test-tenant", net_id) + self.assertTrue(True) + + def test_create_update_and_delete_port(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id) + resp = self.quantum.delete_port("quantum-test-tenant", net_id, + port_id) + self.quantum.delete_network("quantum-test-tenant", + net_id) + self.assertTrue(True) + + def test_create_plug_unplug_iface(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id) + old_vic = resp["attachment"] + self.assertTrue(old_vic == "None") + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id, + "nova-instance-test-%s" % os.getpid()) + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id) + new_vic = resp["attachment"] + + self.assertTrue(old_vic != new_vic) + self.quantum.unplug_interface("quantum-test-tenant", net_id, port_id) + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id) + new_vic = resp["attachment"] + self.assertTrue(old_vic == new_vic) + resp = self.quantum.delete_port("quantum-test-tenant", net_id, port_id) + self.quantum.delete_network("quantum-test-tenant", net_id) + self.assertTrue(True) + + def test_create_multi_port_attachment(self): + resp = self.quantum.create_custom_network( + "quantum-test-tenant", "quantum-Private-TenantA", + self.BRIDGE_TZ_UUID, self.quantum.controller) + net_id = resp["net-id"] + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id1 = resp["port-id"] + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id1) + old_vic = resp["attachment"] + self.assertTrue(old_vic == "None") + + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id1, + "nova-instance-test-%s" % os.getpid()) + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id1) + new_vic = resp["attachment"] + self.assertTrue(old_vic != new_vic) + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id2 = resp["port-id"] + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id2) + old_vic2 = resp["attachment"] + self.assertTrue(old_vic2 == "None") + + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id2, + "nova-instance-test2-%s" % os.getpid()) + resp = self.quantum.get_port_details("quantum-test-tenant", net_id, + port_id2) + new_vic = resp["attachment"] + self.assertTrue(old_vic2 != new_vic) + + resp = self.quantum.get_all_ports("quantum-test-tenant", net_id) + + resp = self.quantum.get_network_details("quantum-test-tenant", net_id) + + resp = self.quantum.delete_port("quantum-test-tenant", net_id, + port_id1) + resp = self.quantum.delete_port("quantum-test-tenant", net_id, + port_id2) + self.quantum.delete_network("quantum-test-tenant", net_id) + self.assertTrue(True) + + def test_negative_get_all_ports(self): + try: + self.quantum.get_all_ports("quantum-test-tenant", "xxx-no-net-id") + except exception.NetworkNotFound: + self.assertTrue(True) + return + + self.assertTrue(False) + + def test_negative_create_port1(self): + try: + self.quantum.create_port("quantum-test-tenant", "xxx-no-net-id", + "ACTIVE") + except exception.NetworkNotFound: + self.assertTrue(True) + return + + self.assertTrue(False) + + def test_negative_create_port2(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.create_port("quantum-test-tenant", resp1["net-id"], + "INVALID") + except exception.StateInvalid: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + return + + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + self.assertTrue(False) + + def test_negative_update_port1(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.update_port("quantum-test-tenant", resp1["net-id"], + "port_id_fake", state="ACTIVE") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + return + + self.assertTrue(False) + + def test_negative_update_port2(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.update_port("quantum-test-tenant", resp1["net-id"], + "port_id_fake", state="INVALID") + except exception.StateInvalid: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + return + + self.assertTrue(False) + + def test_negative_update_port3(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.update_port("quantum-test-tenant", resp1["net-id"], + "port_id_fake", state="ACTIVE") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + return + + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + self.assertTrue(False) + + def test_negative_delete_port1(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.delete_port("quantum-test-tenant", resp1["net-id"], + "port_id_fake") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + return + + self.assertTrue(False) + + def test_negative_delete_port2(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.delete_port("quantum-test-tenant", resp1["net-id"], + "port_id_fake") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + return + + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + self.assertTrue(False) + + def test_negative_get_port_details(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.get_port_details("quantum-test-tenant", + resp1["net-id"], + "port_id_fake") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", + resp1["net-id"]) + return + + self.quantum.delete_network("quantum-test-tenant", resp1["net-id"]) + self.assertTrue(False) + + def test_negative_plug_interface(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.plug_interface("quantum-test-tenant", + resp1["net-id"], + "port_id_fake", "iface_id_fake") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", + resp1["net-id"]) + return + + self.assertTrue(False) + + def test_negative_unplug_interface(self): + resp1 = self.quantum.create_network("quantum-test-tenant", + "quantum-Private-TenantB") + try: + self.quantum.unplug_interface("quantum-test-tenant", + resp1["net-id"], "port_id_fake") + except exception.PortNotFound: + self.assertTrue(True) + self.quantum.delete_network("quantum-test-tenant", + resp1["net-id"]) + return + + self.assertTrue(False) + + def test_get_port_status_invalid_lswitch(self): + try: + nvplib.get_port_status(self.quantum.controller, + "invalid-lswitch", + "invalid-port") + except exception.NetworkNotFound: + return + # Shouldn't be reached + self.assertTrue(False) + + def test_get_port_status_invalid_port(self): + resp = self.quantum.create_custom_network("quantum-test-tenant", + "quantum-Private-TenantA", self.BRIDGE_TZ_UUID, + self.quantum.controller) + net_id = resp["net-id"] + self.networks.append(net_id) + + try: + nvplib.get_port_status(self.quantum.controller, net_id, + "invalid-port") + except exception.PortNotFound: + return + # Shouldn't be reached + self.assertTrue(False) + + def test_get_port_status_returns_the_right_stuff(self): + resp = self.quantum.create_custom_network("quantum-test-tenant", + "quantum-Private-TenantA", self.BRIDGE_TZ_UUID, + self.quantum.controller) + net_id = resp["net-id"] + self.networks.append(net_id) + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + self.ports.append((net_id, port_id)) + res = nvplib.get_port_status(self.quantum.controller, net_id, port_id) + self.assertTrue(res in ['UP', 'DOWN', 'PROVISIONING']) + + def test_get_port_stats_invalid_lswitch(self): + try: + nvplib.get_port_stats(self.quantum.controller, + "invalid-lswitch", + "invalid-port") + except exception.NetworkNotFound: + return + # Shouldn't be reached + self.assertTrue(False) + + def test_get_port_stats_invalid_port(self): + resp = self.quantum.create_custom_network("quantum-test-tenant", + "quantum-Private-TenantA", self.BRIDGE_TZ_UUID, + self.quantum.controller) + net_id = resp["net-id"] + self.networks.append(net_id) + + try: + nvplib.get_port_stats(self.quantum.controller, net_id, + "invalid-port") + except exception.PortNotFound: + return + # Shouldn't be reached + self.assertTrue(False) + + def test_get_port_stats_returns_the_right_stuff(self): + resp = self.quantum.create_custom_network("quantum-test-tenant", + "quantum-Private-TenantA", self.BRIDGE_TZ_UUID, + self.quantum.controller) + net_id = resp["net-id"] + self.networks.append(net_id) + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + self.ports.append((net_id, port_id)) + res = nvplib.get_port_stats(self.quantum.controller, net_id, port_id) + self.assertTrue("tx_errors" in res) + self.assertTrue("tx_bytes" in res) + self.assertTrue("tx_packets" in res) + self.assertTrue("rx_errors" in res) + self.assertTrue("rx_bytes" in res) + self.assertTrue("rx_packets" in res) + + def test_port_filters_by_attachment(self): + resp = self.quantum.create_custom_network("quantum-test-tenant", + "quantum-Private-TenantA", self.BRIDGE_TZ_UUID, + self.quantum.controller) + net_id = resp["net-id"] + self.networks.append(net_id) + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + port_id1 = port_id + self.ports.append((net_id, port_id)) + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id, + "attachment1") + + resp = self.quantum.create_port("quantum-test-tenant", net_id, + "ACTIVE") + port_id = resp["port-id"] + port_id2 = port_id + self.ports.append((net_id, port_id)) + self.quantum.plug_interface("quantum-test-tenant", net_id, port_id, + "attachment2") + + # Make sure we get all the ports that we created back + ports = self.quantum.get_all_ports("quantum-test-tenant", net_id) + self.assertTrue(len(ports) == 2) + + # Make sure we only get the filtered ones back + ports = self.quantum.get_all_ports("quantum-test-tenant", net_id, + filter_opts={"attachment": "attachment2"}) + self.assertTrue(len(ports) == 1) + self.assertTrue(ports[0]["port-id"] == port_id2) + + # Make sure we don't get any back with an invalid filter + ports = self.quantum.get_all_ports("quantum-test-tenant", net_id, + filter_opts={"attachment": "invalidattachment"}) + self.assertTrue(len(ports) == 0) diff --git a/setup.py b/setup.py index dace785c618..f971de6a4fe 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ init_path = 'etc/init.d' ovs_plugin_config_path = 'etc/quantum/plugins/openvswitch' cisco_plugin_config_path = 'etc/quantum/plugins/cisco' linuxbridge_plugin_config_path = 'etc/quantum/plugins/linuxbridge' +nvp_plugin_config_path = 'etc/quantum/plugins/nicira' DataFiles = [ (config_path, @@ -87,6 +88,8 @@ DataFiles = [ 'etc/quantum/plugins/cisco/db_conn.ini']), (linuxbridge_plugin_config_path, ['etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini']), + (nvp_plugin_config_path, + ['etc/quantum/plugins/nicira/nvp.ini']), ] setup(