Merge "Initial commit: nvp plugin"

This commit is contained in:
Jenkins 2012-02-25 17:43:44 +00:00 committed by Gerrit Code Review
commit c8c2929151
23 changed files with 3548 additions and 0 deletions

View 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

View File

@ -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>

View File

View File

@ -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."

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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: