]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Initial commit: nvp plugin
authorBrad Hall <brad@nicira.com>
Fri, 17 Feb 2012 18:23:04 +0000 (10:23 -0800)
committerBrad Hall <brad@nicira.com>
Wed, 22 Feb 2012 22:40:51 +0000 (14:40 -0800)
blueprint: quantum-nvp-plugin

Change-Id: I07c5d7b305928c341ef1b35a0d9b3281abcb03ae

23 files changed:
MANIFEST.in
etc/quantum/plugins/nicira/nvp.ini [new file with mode: 0644]
quantum/plugins/nicira/__init__.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/README [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/__init__.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/api_client/__init__.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/api_client/client.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/api_client/client_eventlet.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/api_client/common.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/api_client/request.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/api_client/request_eventlet.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/cli.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_check.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_config.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_network.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_common.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_nvp_api_request_eventlet.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/tests/test_port.py [new file with mode: 0644]
setup.py

index 9d77adccb84feae64266c38eca4bef146964bb07..e207f42845ee8b6e126792de37f638828a192a0a 100644 (file)
@@ -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 (file)
index 0000000..f3e484f
--- /dev/null
@@ -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 = <insert default tz uuid>
+# This parameter is a space separated list of NVP_CONTROLLER_CONNECTIONS.
+NVP_CONTROLLER_CONNECTIONS = <space separated names of controller connections>
+# This parameter describes a connection to a single NVP controller.
+# <ip> is the ip address of the controller
+# <port> is the port of the controller (default NVP port is 443)
+# <user> is the user name for this controller
+# <pass> is the user password.
+# <request_timeout>: The total time limit on all operations for a controller
+#   request (including retries, redirects from unresponsive controllers).
+#   Default is 30.
+# <http_timeout>: How long to wait before aborting an unresponsive controller
+#   (and allow for retries to another controller).
+#   Default is 10.
+# <retries>: the maximum number of times to retry a particular request
+#   Default is 2.
+# <redirects>: 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
+<connection name>=<ip>:<port>:<user>:<pass>:<api_call_timeout>:<http_timeout>:<retries>:<redirects>
diff --git a/quantum/plugins/nicira/__init__.py b/quantum/plugins/nicira/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py b/quantum/plugins/nicira/nicira_nvp_plugin/NvpApiClient.py
new file mode 100644 (file)
index 0000000..dff3871
--- /dev/null
@@ -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 (file)
index 0000000..37cf844
--- /dev/null
@@ -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 <network_uuid, network_name> 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 (file)
index 0000000..4bc0df5
--- /dev/null
@@ -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 (file)
index 0000000..0697d2e
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..545387c
--- /dev/null
@@ -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 <dlapsley@nicira.com>, 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 (file)
index 0000000..d9493d0
--- /dev/null
@@ -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 (file)
index 0000000..2ed8fb1
--- /dev/null
@@ -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 (file)
index 0000000..51a27b3
--- /dev/null
@@ -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 (file)
index 0000000..7d31c99
--- /dev/null
@@ -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):<ip>:<port> => (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 (file)
index 0000000..9e548e0
--- /dev/null
@@ -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] <command> [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 (file)
index 0000000..74ba648
--- /dev/null
@@ -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>)."""
+    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 (file)
index 0000000..3bc0af9
--- /dev/null
@@ -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 (file)
index 0000000..2c0085c
--- /dev/null
@@ -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 = <default uuid>
+NVP_CONTROLLER_IP = <controller ip>
+PORT = <port>
+USER = <user>
+PASSWORD = <pass>
+''')
+        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'] == '<default uuid>')
+        self.assertTrue(
+            cluster1.controllers[0]['port'] == '<port>')
+        self.assertTrue(
+            cluster1.controllers[0]['user'] == '<user>')
+        self.assertTrue(
+            cluster1.controllers[0]['password'] == '<pass>')
+        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 = <default 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'] == '<default 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 = <controller ip>
+PORT = <port>
+USER = <user>
+PASSWORD = <pass>
+DEFAULT_TZ_UUID = <default 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'] == '<default 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 = <controller ip>
+PORT = <port>
+USER = <user>
+PASSWORD = <pass>
+DEFAULT_TZ_UUID = <default 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'] == '<default 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 = <default uuid>
+NVP_CONTROLLER_IP = <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 = <default 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 = <default uuid>
+NVP_CONTROLLER_IP = <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 = <default 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 (file)
index 0000000..8eacdc2
--- /dev/null
@@ -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 (file)
index 0000000..d84e9ef
--- /dev/null
@@ -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 (file)
index 0000000..56aad5c
--- /dev/null
@@ -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 (file)
index 0000000..dc8cc79
--- /dev/null
@@ -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 (file)
index 0000000..e2569f0
--- /dev/null
@@ -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)
index dace785c61836baa7bad9dad50b21abb14b50cf1..f971de6a4fe0519303173a9c10521f9792e62257 100644 (file)
--- 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(