Provider network implementation for NVP plugin.

blueprint nvp-provider-net

Implements the provider network extension support. The list of valid network
types has been updated to reflect the types supported by the nvp plugin.
This was necessary otherwise validation would have always failed.
Multiple logical switches might be associated with a quantum network; the
first logical switch will always have the same id as the quantum network.
Also now raises exception when port limit on overlay network is reached.

This patch also adds a check for the maximum number of ports on 'standard'
overlay networks, and performs some code refactoring for improving
maintanability. For instance the NVPCluster class has been moved into its own
module.

Change-Id: Ib26d327daf748cfcba9ca74e8dc2e8e89c676c2e
This commit is contained in:
Salvatore Orlando 2012-12-18 07:44:44 -08:00
parent 37c25d219a
commit bcae703bc7
13 changed files with 919 additions and 415 deletions

View File

@ -24,13 +24,19 @@ reconnect_interval = 2
# sql_idle_timeout = 3600
[NVP]
# The number of logical ports to create per bridged logical switch
# Maximum number of ports for each bridged logical switch
# max_lp_per_bridged_ls = 64
# Maximum number of ports for each overlay (stt, gre) logical switch
# max_lp_per_overlay_ls = 256
# Time from when a connection pool is switched to another controller
# during failure.
# failover_time = 5
# Number of connects to each controller node.
# concurrent_connections = 3
# Name of the default cluster where requests should be sent if a nova zone id
# is not specified. If it is empty or reference a non-existent cluster
# the first cluster specified in this configuration file will be used
# default_cluster_name =
#[CLUSTER:example]
# This is uuid of the default NVP Transport zone that will be used for

View File

@ -19,7 +19,9 @@ NETWORK_TYPE = 'provider:network_type'
PHYSICAL_NETWORK = 'provider:physical_network'
SEGMENTATION_ID = 'provider:segmentation_id'
NETWORK_TYPE_VALUES = ['flat', 'gre', 'local', 'vlan']
# TODO(salvatore-orlando): Devise a solution for allowing plugins
# to alter the set of allowed values
NETWORK_TYPE_VALUES = ['flat', 'gre', 'local', 'vlan', 'stt']
EXTENDED_ATTRIBUTES_2_0 = {
'networks': {

View File

@ -20,45 +20,54 @@
# @author: Aaron Rosen, Nicira Networks, Inc.
import ConfigParser
import json
import hashlib
import logging
import netaddr
import os
import sys
import traceback
import urllib
import uuid
import webob.exc
# FIXME(salvatore-orlando): get rid of relative imports
from common import config
from quantum.plugins.nicira.nicira_nvp_plugin.api_client import client_eventlet
import NvpApiClient
import nvplib
from nvp_plugin_version import PLUGIN_VERSION
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_models
from quantum.api.v2 import attributes
from quantum.api.v2 import base
from quantum.common import constants
from quantum.common import exceptions as exception
from quantum.common import exceptions as q_exc
from quantum.common import rpc as q_rpc
from quantum.common import topics
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base
from quantum.db import models_v2
from quantum.extensions import providernet as pnet
from quantum.openstack.common import cfg
from quantum.openstack.common import rpc
from quantum import policy
from quantum.plugins.nicira.nicira_nvp_plugin.common import (exceptions
as nvp_exc)
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_db
from quantum.plugins.nicira.nicira_nvp_plugin import nvp_cluster
import NvpApiClient
import nvplib
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")
LOG = logging.getLogger("QuantumPlugin")
# Provider network extension - allowed network types for the NVP Plugin
class NetworkTypes:
""" Allowed provider network types for the NVP Plugin """
STT = 'stt'
GRE = 'gre'
FLAT = 'flat'
VLAN = 'vlan'
def parse_config():
"""Parse the supplied plugin configuration.
@ -77,11 +86,7 @@ def parse_config():
"sql_idle_timeout": cfg.CONF.DATABASE.sql_idle_timeout,
"sql_dbpool_enable": cfg.CONF.DATABASE.sql_dbpool_enable
}
nvp_options = {'max_lp_per_bridged_ls': cfg.CONF.NVP.max_lp_per_bridged_ls}
nvp_options.update({'failover_time': cfg.CONF.NVP.failover_time})
nvp_options.update({'concurrent_connections':
cfg.CONF.NVP.concurrent_connections})
nvp_options = cfg.CONF.NVP
nvp_conf = config.ClusterConfigOptions(cfg.CONF)
cluster_names = config.register_cluster_groups(nvp_conf)
nvp_conf.log_opt_values(LOG, logging.DEBUG)
@ -116,125 +121,16 @@ class NVPRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
return q_rpc.PluginRpcDispatcher([self])
class NVPCluster(object):
"""Encapsulates controller connection and api_client for a cluster.
Accessed within the NvpPluginV2 class.
Each element in the self.controllers list is a dictionary that
contains the following keys:
ip, port, user, password, default_tz_uuid, uuid, zone
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, ip, port, user, password, request_timeout,
http_timeout, retries, redirects,
default_tz_uuid, uuid=None, zone=None):
"""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.
:param uuid: UUID of this cluster (used in MDI configs).
:param zone: Zone of this cluster (used in MDI configs).
"""
keys = [
'ip', 'user', 'password', 'default_tz_uuid', 'uuid', 'zone']
controller_dict = dict([(k, locals()[k]) for k in keys])
int_keys = [
'port', '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']
@property
def zone(self):
return self.controllers[0]['zone']
@property
def uuid(self):
return self.controllers[0]['uuid']
class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
"""
NvpPluginV2 is a Quantum plugin that provides L2 Virtual Network
functionality using NVP.
"""
supported_extension_aliases = ["provider"]
# Default controller cluster
default_cluster = None
def __init__(self, loglevel=None):
if loglevel:
logging.basicConfig(level=loglevel)
@ -242,11 +138,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
NvpApiClient.LOG.setLevel(loglevel)
self.db_opts, self.nvp_opts, self.clusters_opts = parse_config()
self.clusters = []
self.clusters = {}
for c_opts in self.clusters_opts:
# Password is guaranteed to be the same across all controllers
# in the same NVP cluster.
cluster = NVPCluster(c_opts['name'])
cluster = nvp_cluster.NVPCluster(c_opts['name'])
for controller_connection in c_opts['nvp_controller_connection']:
args = controller_connection.split(':')
try:
@ -259,7 +155,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
"controller %(conn)s in cluster %(name)s"),
{'conn': controller_connection,
'name': c_opts['name']})
raise
raise nvp_exc.NvpInvalidConnection(
conn_params=controller_connection)
api_providers = [(x['ip'], x['port'], True)
for x in cluster.controllers]
@ -269,29 +166,185 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
http_timeout=cluster.http_timeout,
retries=cluster.retries,
redirects=cluster.redirects,
failover_time=self.nvp_opts['failover_time'],
concurrent_connections=self.nvp_opts['concurrent_connections'])
failover_time=self.nvp_opts.failover_time,
concurrent_connections=self.nvp_opts.concurrent_connections)
# TODO(salvatore-orlando): do login at first request,
# not when plugin, is instantiated
# not when plugin is instantiated
cluster.api_client.login()
self.clusters[c_opts['name']] = cluster
# TODO(pjb): What if the cluster isn't reachable this
# instant? It isn't good to fall back to invalid cluster
# strings.
# Default for future-versions
self.clusters.append(cluster)
# Connect and configure ovs_quantum db
# Connect and configure nvp_quantum db
options = {
'sql_connection': self.db_opts['sql_connection'],
'sql_max_retries': self.db_opts['sql_max_retries'],
'reconnect_interval': self.db_opts['reconnect_interval'],
'base': models_v2.model_base.BASEV2,
}
def_cluster_name = self.nvp_opts.default_cluster_name
if def_cluster_name and def_cluster_name in self.clusters:
self.default_cluster = self.clusters[def_cluster_name]
else:
first_cluster_name = self.clusters.keys()[0]
if not def_cluster_name:
LOG.info(_("Default cluster name not specified. "
"Using first cluster:%s"), first_cluster_name)
elif not def_cluster_name in self.clusters:
LOG.warning(_("Default cluster name %(def_cluster_name)s. "
"Using first cluster:%(first_cluster_name)s")
% locals())
# otherwise set 1st cluster as default
self.default_cluster = self.clusters[first_cluster_name]
db.configure_db(options)
# Extend the fault map
self._extend_fault_map()
# Set up RPC interface for DHCP agent
self.setup_rpc()
def _extend_fault_map(self):
""" Extends the Quantum Fault Map
Exceptions specific to the NVP Plugin are mapped to standard
HTTP Exceptions
"""
base.FAULT_MAP.update({nvp_exc.NvpInvalidNovaZone:
webob.exc.HTTPBadRequest,
nvp_exc.NvpNoMorePortsException:
webob.exc.HTTPBadRequest})
def _novazone_to_cluster(self, novazone_id):
if novazone_id in self.novazone_cluster_map:
return self.novazone_cluster_map[novazone_id]
LOG.debug(_("Looking for nova zone: %s") % novazone_id)
for x in self.clusters:
LOG.debug(_("Looking for nova zone %(novazone_id)s in "
"cluster: %(x)s") % locals())
if x.zone == str(novazone_id):
self.novazone_cluster_map[x.zone] = x
return x
LOG.error(_("Unable to find cluster config entry for nova zone: %s") %
novazone_id)
raise nvp_exc.NvpInvalidNovaZone(nova_zone=novazone_id)
def _find_target_cluster(self, resource):
""" Return cluster where configuration should be applied
If the resource being configured has a paremeter expressing
the zone id (nova_id), then select corresponding cluster,
otherwise return default cluster.
"""
if 'nova_id' in resource:
return self._novazone_to_cluster(resource['nova_id'])
else:
return self.default_cluster
def _check_provider_view_auth(self, context, network):
return policy.check(context,
"extension:provider_network:view",
network)
def _enforce_provider_set_auth(self, context, network):
return policy.enforce(context,
"extension:provider_network:set",
network)
def _handle_provider_create(self, context, attrs):
# NOTE(salvatore-orlando): This method has been borrowed from
# the OpenvSwtich plugin, altough changed to match NVP specifics.
network_type = attrs.get(pnet.NETWORK_TYPE)
physical_network = attrs.get(pnet.PHYSICAL_NETWORK)
segmentation_id = attrs.get(pnet.SEGMENTATION_ID)
network_type_set = attributes.is_attr_set(network_type)
physical_network_set = attributes.is_attr_set(physical_network)
segmentation_id_set = attributes.is_attr_set(segmentation_id)
if not (network_type_set or physical_network_set or
segmentation_id_set):
return
# Authorize before exposing plugin details to client
self._enforce_provider_set_auth(context, attrs)
err_msg = None
if not network_type_set:
err_msg = _("%s required") % pnet.NETWORK_TYPE
elif network_type in (NetworkTypes.GRE, NetworkTypes.STT,
NetworkTypes.FLAT):
if segmentation_id_set:
err_msg = _("Segmentation ID cannot be specified with "
"flat network type")
elif network_type == NetworkTypes.VLAN:
if not segmentation_id_set:
err_msg = _("Segmentation ID must be specified with "
"vlan network type")
elif (segmentation_id_set and
(segmentation_id < 1 or segmentation_id > 4094)):
err_msg = _("%s out of range (1 to 4094)") % segmentation_id
else:
# Verify segment is not already allocated
binding = nicira_db.get_network_binding_by_vlanid(
context.session, segmentation_id)
if binding:
raise q_exc.VlanIdInUse(vlan_id=segmentation_id,
physical_network=physical_network)
else:
err_msg = _("%(net_type_param)s %(net_type_value)s not "
"supported") % {'net_type_param': pnet.NETWORK_TYPE,
'net_type_value': network_type}
if err_msg:
raise q_exc.InvalidInput(error_message=err_msg)
# TODO(salvatore-orlando): Validate tranport zone uuid
# which should be specified in physical_network
def _extend_network_dict_provider(self, context, network, binding=None):
if self._check_provider_view_auth(context, network):
if not binding:
binding = nicira_db.get_network_binding(context.session,
network['id'])
# With NVP plugin 'normal' overlay networks will have no binding
# TODO(salvatore-orlando) make sure users can specify a distinct
# tz_uuid as 'provider network' for STT net type
if binding:
network[pnet.NETWORK_TYPE] = binding.binding_type
network[pnet.PHYSICAL_NETWORK] = binding.tz_uuid
network[pnet.SEGMENTATION_ID] = binding.vlan_id
def _handle_lswitch_selection(self, cluster, network,
network_binding, max_ports,
allow_extra_lswitches):
lswitches = nvplib.get_lswitches(cluster, network.id)
try:
# TODO find main_ls too!
return [ls for ls in lswitches
if (ls['_relations']['LogicalSwitchStatus']
['lport_count'] < max_ports)].pop(0)
except IndexError:
# Too bad, no switch available
LOG.debug(_("No switch has available ports (%d checked)") %
len(lswitches))
if allow_extra_lswitches:
main_ls = [ls for ls in lswitches if ls['uuid'] == network.id]
tag_dict = dict((x['scope'], x['tag']) for x in main_ls[0]['tags'])
if not 'multi_lswitch' in tag_dict:
nvplib.update_lswitch(cluster,
main_ls[0]['uuid'],
main_ls[0]['display_name'],
network['tenant_id'],
tags=[{'tag': 'True',
'scope': 'multi_lswitch'}])
selected_lswitch = nvplib.create_lswitch(
cluster, network.tenant_id,
"%s-ext-%s" % (network.name, len(lswitches)),
network_binding.binding_type,
network_binding.tz_uuid,
network_binding.vlan_id,
network.id)
return selected_lswitch
else:
LOG.error(_("Maximum number of logical ports reached for "
"logical network %s") % network.id)
raise nvp_exc.NvpNoMorePortsException(network=network.id)
def setup_rpc(self):
# RPC support for dhcp
self.topic = topics.PLUGIN
@ -302,15 +355,6 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
# Consume from all consumers in a thread
self.conn.consume_in_thread()
@property
def cluster(self):
if len(self.clusters):
return self.clusters[0]
return None
def clear_state(self):
nvplib.clear_state(self.clusters[0])
def get_all_networks(self, tenant_id, **kwargs):
networks = []
for c in self.clusters:
@ -337,24 +381,40 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
}
:raises: exception.NoImplementedError
"""
net_data = network['network'].copy()
# Process the provider network extension
self._handle_provider_create(context, net_data)
# Replace ATTR_NOT_SPECIFIED with None before sending to NVP
for attr, value in network['network'].iteritems():
if value == attributes.ATTR_NOT_SPECIFIED:
net_data[attr] = None
# FIXME(arosen) implement admin_state_up = False in NVP
if network['network']['admin_state_up'] is False:
if net_data['admin_state_up'] is False:
LOG.warning(_("Network with admin_state_up=False are not yet "
"supported by this plugin. Ignoring setting for "
"network %s"),
network['network'].get('name', '<unknown>'))
"network %s") % net_data.get('name', '<unknown>'))
tenant_id = self._get_tenant_id_for_create(context, net_data)
target_cluster = self._find_target_cluster(net_data)
lswitch = nvplib.create_lswitch(target_cluster,
tenant_id,
net_data.get('name'),
net_data.get(pnet.NETWORK_TYPE),
net_data.get(pnet.PHYSICAL_NETWORK),
net_data.get(pnet.SEGMENTATION_ID))
network['network']['id'] = lswitch['uuid']
tenant_id = self._get_tenant_id_for_create(context, network)
# TODO(salvatore-orlando): if the network is shared this should be
# probably stored into the lswitch with a tag
# TODO(salvatore-orlando): Important - provider networks support
# (might require a bridged TZ)
net = nvplib.create_network(network['network']['tenant_id'],
network['network']['name'],
clusters=self.clusters)
network['network']['id'] = net['net-id']
return super(NvpPluginV2, self).create_network(context, network)
with context.session.begin(subtransactions=True):
new_net = super(NvpPluginV2, self).create_network(context,
network)
if net_data.get(pnet.NETWORK_TYPE):
net_binding = nicira_db.add_network_binding(
context.session, new_net['id'],
net_data.get(pnet.NETWORK_TYPE),
net_data.get(pnet.PHYSICAL_NETWORK),
net_data.get(pnet.SEGMENTATION_ID))
self._extend_network_dict_provider(context, new_net,
net_binding)
return new_net
def delete_network(self, context, id):
"""
@ -365,29 +425,31 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
:raises: exception.NetworkInUse
:raises: exception.NetworkNotFound
"""
super(NvpPluginV2, self).delete_network(context, id)
# FIXME(salvatore-orlando): Failures here might lead NVP
# and quantum state to diverge
pairs = self._get_lswitch_cluster_pairs(id, context.tenant_id)
for (cluster, switches) in pairs:
nvplib.delete_networks(cluster, id, switches)
LOG.debug(_("delete_network() completed for tenant: %s"),
LOG.debug(_("delete_network completed for tenant: %s"),
context.tenant_id)
def _get_lswitch_cluster_pairs(self, netw_id, tenant_id):
"""Figure out the set of lswitches on each cluster that maps to this
network id"""
pairs = []
for c in self.clusters:
for c in self.clusters.itervalues():
lswitches = []
try:
ls = nvplib.get_network(c, netw_id)
lswitches.append(ls['uuid'])
except exception.NetworkNotFound:
results = nvplib.get_lswitches(c, netw_id)
lswitches.extend([ls['uuid'] for ls in results])
except q_exc.NetworkNotFound:
continue
pairs.append((c, lswitches))
if len(pairs) == 0:
raise exception.NetworkNotFound(net_id=netw_id)
raise q_exc.NetworkNotFound(net_id=netw_id)
LOG.debug(_("Returning pairs for network: %s"), pairs)
return pairs
@ -414,55 +476,43 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
:raises: exception.NetworkNotFound
:raises: exception.QuantumException
"""
result = {}
lswitch_query = "&uuid=%s" % id
# always look for the tenant_id in the resource itself rather than
# the context, as with shared networks context.tenant_id and
# network['tenant_id'] might differ on GETs
# goto to the plugin DB and fecth the network
network = self._get_network(context, id)
# TODO(salvatore-orlando): verify whether the query on os_tid is
# redundant or not.
if context.is_admin is False:
tenant_query = ("&tag=%s&tag_scope=os_tid"
% network['tenant_id'])
else:
tenant_query = ""
# Then fetch the correspondiong logical switch in NVP as well
# TODO(salvatore-orlando): verify whether the step on NVP
# can be completely avoided
lswitch_url_path = (
"/ws.v1/lswitch?"
"fields=uuid,display_name%s%s"
% (tenant_query, lswitch_query))
# verify the fabric status of the corresponding
# logical switch(es) in nvp
try:
for c in self.clusters:
lswitch_results = nvplib.get_all_query_pages(
lswitch_url_path, c)
if lswitch_results:
result['lswitch-display-name'] = (
lswitch_results[0]['display_name'])
# FIXME(salvatore-orlando): This is not going to work unless
# nova_id is stored in db once multiple clusters are enabled
cluster = self._find_target_cluster(network)
lswitches = nvplib.get_lswitches(cluster, id)
net_op_status = constants.NET_STATUS_ACTIVE
quantum_status = network.status
for lswitch in lswitches:
lswitch_status = lswitch.get('LogicalSwitchStatus', None)
# FIXME(salvatore-orlando): Being unable to fetch the
# logical switch status should be an exception.
if (lswitch_status and
not lswitch_status.get('fabric_status', None)):
net_op_status = constants.NET_STATUS_DOWN
break
LOG.debug(_("Current network status:%(net_op_status)s; "
"Status in Quantum DB:%(quantum_status)s")
% locals())
if net_op_status != network.status:
# update the network status
with context.session.begin(subtransactions=True):
network.status = net_op_status
except Exception:
LOG.error(_("Unable to get switches: %s"), traceback.format_exc())
raise exception.QuantumException()
err_msg = _("Unable to get lswitches")
LOG.exception(err_msg)
raise nvp_exc.NvpPluginException(err_msg=err_msg)
if 'lswitch-display-name' not in result:
raise exception.NetworkNotFound(net_id=id)
# Fetch network in quantum
quantum_db = super(NvpPluginV2, self).get_network(context, id, fields)
d = {'id': id,
'name': result['lswitch-display-name'],
'tenant_id': network['tenant_id'],
'admin_state_up': True,
'status': constants.NET_STATUS_ACTIVE,
'shared': network['shared'],
'subnets': quantum_db.get('subnets', [])}
LOG.debug(_("get_network() completed for tenant %(tenant_id)s: %(d)s"),
{'tenant_id': context.tenant_id, 'd': d})
return d
# Don't do field selection here otherwise we won't be able
# to add provider networks fields
net_result = self._make_network_dict(network, None)
self._extend_network_dict_provider(context, net_result)
return self._fields(net_result, fields)
def get_networks(self, context, filters=None, fields=None):
"""
@ -491,6 +541,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
nvp_lswitches = []
quantum_lswitches = (
super(NvpPluginV2, self).get_networks(context, filters))
for net in quantum_lswitches:
self._extend_network_dict_provider(context, net)
if context.is_admin and not filters.get("tenant_id"):
tenant_filter = ""
@ -501,19 +553,20 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
else:
tenant_filter = "&tag=%s&tag_scope=os_tid" % context.tenant_id
lswitch_filters = "uuid,display_name,fabric_status"
lswitch_filters = "uuid,display_name,fabric_status,tags"
lswitch_url_path = (
"/ws.v1/lswitch?fields=%s&relations=LogicalSwitchStatus%s"
% (lswitch_filters, tenant_filter))
try:
for c in self.clusters:
for c in self.clusters.itervalues():
res = nvplib.get_all_query_pages(
lswitch_url_path, c)
nvp_lswitches.extend(res)
except Exception:
LOG.error(_("Unable to get switches: %s"), traceback.format_exc())
raise exception.QuantumException()
err_msg = _("Unable to get logical switches")
LOG.exception(err_msg)
raise nvp_exc.NvpPluginException(err_msg=err_msg)
# TODO (Aaron) This can be optimized
if filters.get("id"):
@ -525,9 +578,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
nvp_lswitches = filtered_lswitches
for quantum_lswitch in quantum_lswitches:
Found = False
for nvp_lswitch in nvp_lswitches:
if nvp_lswitch["uuid"] == quantum_lswitch["id"]:
# TODO(salvatore-orlando): watch out for "extended" lswitches
if nvp_lswitch['uuid'] == quantum_lswitch["id"]:
if (nvp_lswitch["_relations"]["LogicalSwitchStatus"]
["fabric_status"]):
quantum_lswitch["status"] = constants.NET_STATUS_ACTIVE
@ -535,12 +588,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
quantum_lswitch["status"] = constants.NET_STATUS_DOWN
quantum_lswitch["name"] = nvp_lswitch["display_name"]
nvp_lswitches.remove(nvp_lswitch)
Found = True
break
if not Found:
raise Exception(_("Quantum and NVP Databases are out of "
"Sync!"))
else:
raise nvp_exc.NvpOutOfSyncException()
# do not make the case in which switches are found in NVP
# but not in Quantum catastrophic.
if len(nvp_lswitches):
@ -587,9 +637,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
if network["network"].get("admin_state_up"):
if network['network']["admin_state_up"] is False:
raise exception.NotImplementedError("admin_state_up=False "
"networks are not "
"supported.")
raise q_exc.NotImplementedError(_("admin_state_up=False "
"networks are not "
"supported."))
params = {}
params["network"] = network["network"]
pairs = self._get_lswitch_cluster_pairs(id, context.tenant_id)
@ -598,7 +648,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
if network['network'].get("name"):
for (cluster, switches) in pairs:
for switch in switches:
result = nvplib.update_network(cluster, switch, **params)
nvplib.update_lswitch(cluster, switch,
network['network']['name'])
LOG.debug(_("update_network() completed for tenant: %s"),
context.tenant_id)
@ -653,7 +704,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
lport_fields_str = ("tags,admin_status_enabled,display_name,"
"fabric_status_up")
try:
for c in self.clusters:
for c in self.clusters.itervalues():
lport_query_path = (
"/ws.v1/lswitch/%s/lport?fields=%s&%s%stag_scope=q_port_id"
"&relations=LogicalPortStatus" %
@ -667,8 +718,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
nvp_lports[tag["tag"]] = port
except Exception:
LOG.error(_("Unable to get ports: %s"), traceback.format_exc())
raise exception.QuantumException()
err_msg = _("Unable to get ports")
LOG.exception(err_msg)
raise nvp_exc.NvpPluginException(err_msg=err_msg)
lports = []
for quantum_lport in quantum_lports:
@ -690,8 +742,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
del nvp_lports[quantum_lport["id"]]
lports.append(quantum_lport)
except KeyError:
raise Exception(_("Quantum and NVP Databases are out of "
"Sync!"))
LOG.debug(_("Quantum logical port %s was not found on NVP"),
quantum_lport['id'])
# do not make the case in which ports are found in NVP
# but not in Quantum catastrophic.
if len(nvp_lports):
@ -721,8 +775,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
"admin_state_up": Sets admin state of port. if down, port
does not forward packets.
"status": dicates whether port is currently operational
(limit values to "ACTIVE", "DOWN", "BUILD", and
"ERROR"?)
(limit values to "ACTIVE", "DOWN", "BUILD", and "ERROR")
"fixed_ips": list of subnet ID's and IP addresses to be used on
this port
"device_id": identifies the device (e.g., virtual server) using
@ -732,46 +785,70 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
:raises: exception.NetworkNotFound
:raises: exception.StateInvalid
"""
tenant_id = self._get_tenant_id_for_create(context, port['port'])
# Set admin_state_up False since not created in NVP set
# TODO(salvatore-orlando) : verify whether subtransactions can help
# us avoiding multiple operations on the db. This might also allow
# us to use the same identifier for the NVP and the Quantum port
# Set admin_state_up False since not created in NVP yet
port["port"]["admin_state_up"] = False
# First we allocate port in quantum database
try:
quantum_db = super(NvpPluginV2, self).create_port(context, port)
except Exception as e:
raise e
quantum_db = super(NvpPluginV2, self).create_port(context, port)
# Update fields obtained from quantum db
# Update fields obtained from quantum db (eg: MAC address)
port["port"].update(quantum_db)
# We want port to be up in NVP
port["port"]["admin_state_up"] = True
params = {}
params["max_lp_per_bridged_ls"] = \
self.nvp_opts["max_lp_per_bridged_ls"]
params["port"] = port["port"]
params["clusters"] = self.clusters
tenant_id = self._get_tenant_id_for_create(context, port["port"])
port_data = port['port']
# Fetch the network and network binding from Quantum db
network = self._get_network(context, port_data['network_id'])
network_binding = nicira_db.get_network_binding(
context.session, port_data['network_id'])
max_ports = self.nvp_opts.max_lp_per_overlay_ls
allow_extra_lswitches = False
if (network_binding and
network_binding.binding_type in (NetworkTypes.FLAT,
NetworkTypes.VLAN)):
max_ports = self.nvp_opts.max_lp_per_bridged_ls
allow_extra_lswitches = True
try:
port["port"], nvp_port_id = nvplib.create_port(tenant_id,
**params)
nvplib.plug_interface(self.clusters, port["port"]["network_id"],
nvp_port_id, "VifAttachment",
port["port"]["id"])
except Exception as e:
# failed to create port in NVP delete port from quantum_db
q_net_id = port_data['network_id']
cluster = self._find_target_cluster(port_data)
selected_lswitch = self._handle_lswitch_selection(
cluster, network, network_binding, max_ports,
allow_extra_lswitches)
lswitch_uuid = selected_lswitch['uuid']
lport = nvplib.create_lport(cluster,
lswitch_uuid,
port_data['tenant_id'],
port_data['id'],
port_data['name'],
port_data['device_id'],
port_data['admin_state_up'],
port_data['mac_address'],
port_data['fixed_ips'])
# Get NVP ls uuid for quantum network
nvplib.plug_interface(cluster, selected_lswitch['uuid'],
lport['uuid'], "VifAttachment",
port_data['id'])
except nvp_exc.NvpNoMorePortsException as e:
LOG.error(_("Number of available ports for network %s exhausted"),
port_data['network_id'])
super(NvpPluginV2, self).delete_port(context, port["port"]["id"])
raise e
except Exception:
# failed to create port in NVP delete port from quantum_db
err_msg = _("An exception occured while plugging the interface "
"in NVP for port %s") % port_data['id']
LOG.exception(err_msg)
super(NvpPluginV2, self).delete_port(context, port["port"]["id"])
raise nvp_exc.NvpPluginException(err_desc=err_msg)
d = {"port-id": port["port"]["id"],
"port-op-status": port["port"]["status"]}
LOG.debug(_("create_port completed on NVP for tenant %(tenant_id)s: "
"(%(id)s)") % port_data)
LOG.debug(_("create_port() completed for tenant %(tenant_id)s: %(d)s"),
locals())
# update port with admin_state_up True
# update port on Quantum DB with admin_state_up True
port_update = {"port": {"admin_state_up": True}}
return super(NvpPluginV2, self).update_port(context,
port["port"]["id"],
@ -803,23 +880,17 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
"""
params = {}
quantum_db = super(NvpPluginV2, self).get_port(context, id)
port_quantum = super(NvpPluginV2, self).get_port(context, id)
port_nvp, cluster = (
nvplib.get_port_by_quantum_tag(self.clusters,
quantum_db["network_id"], id))
LOG.debug(_("Update port request: %s"), params)
nvplib.get_port_by_quantum_tag(self.clusters.itervalues(),
port_quantum["network_id"], id))
params["cluster"] = cluster
params["port"] = port["port"]
params["port"]["id"] = quantum_db["id"]
params["port"]["tenant_id"] = quantum_db["tenant_id"]
result = nvplib.update_port(quantum_db["network_id"],
port_nvp["uuid"], **params)
LOG.debug(_("update_port() completed for tenant: %s"),
context.tenant_id)
params["port"] = port_quantum
LOG.debug(_("Update port request: %s"), params)
nvplib.update_port(port_quantum['network_id'],
port_nvp['uuid'], **params)
return super(NvpPluginV2, self).update_port(context, id, port)
def delete_port(self, context, id):
@ -835,10 +906,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
:raises: exception.NetworkNotFound
"""
port, cluster = nvplib.get_port_by_quantum_tag(self.clusters,
'*', id)
# TODO(salvatore-orlando): pass only actual cluster
port, cluster = nvplib.get_port_by_quantum_tag(
self.clusters.itervalues(), '*', id)
if port is None:
raise exception.PortNotFound(port_id=id)
raise q_exc.PortNotFound(port_id=id)
# TODO(bgh): if this is a bridged network and the lswitch we just got
# back will have zero ports after the delete we should garbage collect
# the lswitch.
@ -867,9 +939,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
quantum_db = super(NvpPluginV2, self).get_port(context, id, fields)
#TODO: pass only the appropriate cluster here
#Look for port in all lswitches
port, cluster = (
nvplib.get_port_by_quantum_tag(self.clusters,
quantum_db["network_id"], id))
nvplib.get_port_by_quantum_tag(self.clusters.itervalues(),
"*", id))
quantum_db["admin_state_up"] = port["admin_status_enabled"]
if port["_relations"]["LogicalPortStatus"]["fabric_status_up"]:

View File

@ -28,7 +28,7 @@ from common import _conn_str
eventlet.monkey_patch()
logging.basicConfig(level=logging.INFO)
lg = logging.getLogger('nvp_api_client')
LOG = logging.getLogger(__name__)
# Default parameters.
DEFAULT_FAILOVER_TIME = 5
@ -156,19 +156,19 @@ class NvpApiClientEventlet(object):
api_providers are configured.
'''
if not self._api_providers:
lg.warn(_("[%d] no API providers currently available."), rid)
LOG.warn(_("[%d] no API providers currently available."), rid)
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.warn(_("[%d] Waiting for failover timer to expire."), rid)
LOG.warn(_("[%d] Waiting for failover timer to expire."), rid)
time.sleep(self._issue_conn_barrier - now)
# Print out a warning if all connections are in use.
if self._conn_pool[self._active_conn_pool_idx].empty():
lg.debug(_("[%d] Waiting to acquire client connection."), rid)
LOG.debug(_("[%d] Waiting to acquire client connection."), rid)
# Try to acquire a connection (block in get() until connection
# available or timeout occurs).
@ -178,19 +178,19 @@ class NvpApiClientEventlet(object):
if active_conn_pool_idx != self._active_conn_pool_idx:
# active_conn_pool became inactive while we were waiting.
# Put connection back on old pool and try again.
lg.warn(_("[%(rid)d] Active pool expired while waiting for "
"connection: %(conn)s"),
{'rid': rid, 'conn': _conn_str(conn)})
LOG.warn(_("[%(rid)d] Active pool expired while waiting for "
"connection: %(conn)s"),
{'rid': rid, 'conn': _conn_str(conn)})
self._conn_pool[active_conn_pool_idx].put(conn)
return self.acquire_connection(rid=rid)
# Check if the connection has been idle too long.
now = time.time()
if getattr(conn, 'last_used', now) < now - self.CONN_IDLE_TIMEOUT:
lg.info(_("[%(rid)d] Connection %(conn)s idle for %(sec)0.2f "
"seconds; reconnecting."),
{'rid': rid, 'conn': _conn_str(conn),
'sec': now - conn.last_used})
LOG.info(_("[%(rid)d] Connection %(conn)s idle for %(sec)0.2f "
"seconds; reconnecting."),
{'rid': rid, 'conn': _conn_str(conn),
'sec': now - conn.last_used})
conn = self._create_connection(*self._conn_params(conn))
# Stash conn pool so conn knows where to go when it releases.
@ -198,9 +198,9 @@ class NvpApiClientEventlet(object):
conn.last_used = now
qsize = self._conn_pool[self._active_conn_pool_idx].qsize()
lg.debug(_("[%(rid)d] Acquired connection %(conn)s. %(qsize)d "
"connection(s) available."),
{'rid': rid, 'conn': _conn_str(conn), 'qsize': qsize})
LOG.debug(_("[%(rid)d] Acquired connection %(conn)s. %(qsize)d "
"connection(s) available."),
{'rid': rid, 'conn': _conn_str(conn), 'qsize': qsize})
return conn
def release_connection(self, http_conn, bad_state=False, rid=-1):
@ -213,9 +213,9 @@ class NvpApiClientEventlet(object):
:param rid: request id passed in from request eventlet.
'''
if self._conn_params(http_conn) not in self._api_providers:
lg.warn(_("[%(rid)d] Released connection '%(conn)s' is not an "
"API provider for the cluster"),
{'rid': rid, 'conn': _conn_str(http_conn)})
LOG.warn(_("[%(rid)d] Released connection '%(conn)s' is not an "
"API provider for the cluster"),
{'rid': rid, 'conn': _conn_str(http_conn)})
return
# Retrieve "home" connection pool.
@ -223,9 +223,9 @@ class NvpApiClientEventlet(object):
conn_pool = self._conn_pool[conn_pool_idx]
if bad_state:
# Reconnect to provider.
lg.warn(_("[%(rid)d] Connection returned in bad state, "
"reconnecting to %(conn)s"),
{'rid': rid, 'conn': _conn_str(http_conn)})
LOG.warn(_("[%(rid)d] Connection returned in bad state, "
"reconnecting to %(conn)s"),
{'rid': rid, 'conn': _conn_str(http_conn)})
http_conn = self._create_connection(*self._conn_params(http_conn))
http_conn.idx = conn_pool_idx
@ -233,20 +233,20 @@ class NvpApiClientEventlet(object):
# This pool is no longer in a good state. Switch to next pool.
self._active_conn_pool_idx += 1
self._active_conn_pool_idx %= len(self._conn_pool)
lg.warn(_("[%(rid)d] Switched active_conn_pool from "
"%(idx)d to %(pool_idx)d."),
{'rid': rid, 'idx': http_conn.idx,
'pool_idx': self._active_conn_pool_idx})
LOG.warn(_("[%(rid)d] Switched active_conn_pool from "
"%(idx)d to %(pool_idx)d."),
{'rid': rid, 'idx': http_conn.idx,
'pool_idx': self._active_conn_pool_idx})
# No connections to the new provider allowed until after this
# timer has expired (allow time for synchronization).
self._issue_conn_barrier = time.time() + self._failover_time
conn_pool.put(http_conn)
lg.debug(_("[%(rid)d] Released connection %(conn)s. "
"%(qsize)d connection(s) available."),
{'rid': rid, 'conn': _conn_str(http_conn),
'qsize': conn_pool.qsize()})
LOG.debug(_("[%(rid)d] Released connection %(conn)s. "
"%(qsize)d connection(s) available."),
{'rid': rid, 'conn': _conn_str(http_conn),
'qsize': conn_pool.qsize()})
@property
def need_login(self):
@ -263,7 +263,7 @@ class NvpApiClientEventlet(object):
self.login()
self._doing_login_sem.release()
else:
lg.debug(_("Waiting for auth to complete"))
LOG.debug(_("Waiting for auth to complete"))
self._doing_login_sem.acquire()
self._doing_login_sem.release()
return self._cookie
@ -277,13 +277,13 @@ class NvpApiClientEventlet(object):
if ret:
if isinstance(ret, Exception):
lg.error(_('NvpApiClient: login error "%s"'), ret)
LOG.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)
LOG.debug(_("Saving new authentication cookie '%s'"), cookie)
self._cookie = cookie
self._need_login = False

View File

@ -40,8 +40,10 @@ database_opts = [
nvp_opts = [
cfg.IntOpt('max_lp_per_bridged_ls', default=64),
cfg.IntOpt('max_lp_per_overlay_ls', default=256),
cfg.IntOpt('concurrent_connections', default=5),
cfg.IntOpt('failover_time', default=240)
cfg.IntOpt('failover_time', default=240),
cfg.StrOpt('default_cluster_name')
]
cluster_opts = [

View File

@ -0,0 +1,42 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 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.
""" NVP Plugin exceptions """
from quantum.common import exceptions as q_exc
class NvpPluginException(q_exc.QuantumException):
message = _("An unexpected error occurred in the NVP Plugin:%(err_desc)s")
class NvpInvalidConnection(NvpPluginException):
message = _("Invalid NVP connection parameters: %(conn_params)s")
class NvpInvalidNovaZone(NvpPluginException):
message = _("Unable to find cluster config entry "
"for nova zone: %(nova_zone)s")
class NvpNoMorePortsException(NvpPluginException):
message = _("Unable to create port on network %(network)s. "
"Maximum number of ports reached")
class NvpOutOfSyncException(NvpPluginException):
message = _("Quantum state has diverged from the networking backend!")

View File

@ -0,0 +1,54 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nicira, 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
from sqlalchemy.orm import exc
import quantum.db.api as db
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_models
LOG = logging.getLogger(__name__)
def get_network_binding(session, network_id):
session = session or db.get_session()
try:
binding = (session.query(nicira_models.NvpNetworkBinding).
filter_by(network_id=network_id).
one())
return binding
except exc.NoResultFound:
return
def get_network_binding_by_vlanid(session, vlan_id):
session = session or db.get_session()
try:
binding = (session.query(nicira_models.NvpNetworkBinding).
filter_by(vlan_id=vlan_id).
one())
return binding
except exc.NoResultFound:
return
def add_network_binding(session, network_id, binding_type, tz_uuid, vlan_id):
with session.begin(subtransactions=True):
binding = nicira_models.NvpNetworkBinding(network_id, binding_type,
tz_uuid, vlan_id)
session.add(binding)
return binding

View File

@ -0,0 +1,49 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nicira, 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 sqlalchemy import Column, Enum, ForeignKey, Integer, String
from quantum.db.models_v2 import model_base
class NvpNetworkBinding(model_base.BASEV2):
"""Represents a binding of a virtual network with a transport zone.
This model class associates a Quantum network with a transport zone;
optionally a vlan ID might be used if the binding type is 'bridge'
"""
__tablename__ = 'nvp_network_bindings'
network_id = Column(String(36),
ForeignKey('networks.id', ondelete="CASCADE"),
primary_key=True)
# 'flat', 'vlan', stt' or 'gre'
binding_type = Column(Enum('flat', 'vlan', 'stt', 'gre'), nullable=False)
tz_uuid = Column(String(36))
vlan_id = Column(Integer)
def __init__(self, network_id, binding_type, tz_uuid, vlan_id):
self.network_id = network_id
self.binding_type = binding_type
self.tz_uuid = tz_uuid
self.vlan_id = vlan_id
def __repr__(self):
return "<NetworkBinding(%s,%s,%s,%s)>" % (self.network_id,
self.binding_type,
self.tz_uuid,
self.vlan_id)

View File

@ -0,0 +1,131 @@
# Copyright 2012 Nicira, 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.
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
class NVPCluster(object):
"""Encapsulates controller connection and api_client for a cluster.
Accessed within the NvpPluginV2 class.
Each element in the self.controllers list is a dictionary that
contains the following keys:
ip, port, user, password, default_tz_uuid, uuid, zone
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, ip, port, user, password, request_timeout,
http_timeout, retries, redirects,
default_tz_uuid, uuid=None, zone=None):
"""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.
:param uuid: UUID of this cluster (used in MDI configs).
:param zone: Zone of this cluster (used in MDI configs).
"""
keys = [
'ip', 'user', 'password', 'default_tz_uuid', 'uuid', 'zone']
controller_dict = dict([(k, locals()[k]) for k in keys])
int_keys = [
'port', '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']
@property
def zone(self):
return self.controllers[0]['zone']
@property
def uuid(self):
return self.controllers[0]['uuid']

View File

@ -25,6 +25,7 @@
from copy import copy
import functools
import itertools
import json
import hashlib
import logging
@ -33,6 +34,7 @@ import re
import uuid
from eventlet import semaphore
import NvpApiClient
#FIXME(danwent): I'd like this file to get to the point where it has
@ -40,6 +42,17 @@ import NvpApiClient
from quantum.common import constants
from quantum.common import exceptions as exception
# HTTP METHODS CONSTANTS
HTTP_GET = "GET"
HTTP_POST = "POST"
# Default transport type for logical switches
DEF_TRANSPORT_TYPE = "stt"
# Prefix to be used for all NVP API calls
URI_PREFIX = "/ws.v1"
# Resources exposed by NVP API
LSWITCH_RESOURCE = "lswitch"
LPORT_RESOURCE = "lport"
LOCAL_LOGGING = False
if LOCAL_LOGGING:
from logging.handlers import SysLogHandler
@ -52,8 +65,8 @@ if LOCAL_LOGGING:
LOG.addHandler(syslog)
LOG.setLevel(logging.DEBUG)
else:
LOG = logging.getLogger("nvplib")
LOG.setLevel(logging.INFO)
LOG = logging.getLogger(__name__)
LOG.setLevel(logging.DEBUG)
# TODO(bgh): it would be more efficient to use a bitmap
taken_context_ids = []
@ -63,12 +76,42 @@ _net_type_cache = {} # cache of {net_id: network_type}
_lqueue_cache = {}
def _build_uri_path(resource,
resource_id=None,
parent_resource_id=None,
fields=None,
relations=None, filters=None):
# TODO(salvatore-orlando): This is ugly. do something more clever
# and aovid the if statement
if resource == LPORT_RESOURCE:
res_path = ("%s/%s/%s" % (LSWITCH_RESOURCE,
parent_resource_id,
resource) +
(resource_id and "/%s" % resource_id or ''))
else:
res_path = resource + (resource_id and
"/%s" % resource_id or '')
params = []
params.append(fields and "fields=%s" % fields)
params.append(relations and "relations=%s" % relations)
if filters:
params.extend(['%s=%s' % (k, v) for (k, v) in filters.iteritems()])
uri_path = "%s/%s" % (URI_PREFIX, res_path)
query_string = reduce(lambda x, y: "%s&%s" % (x, y),
itertools.ifilter(lambda x: x is not None, params),
"")
if query_string:
uri_path += "?%s" % query_string
return uri_path
def get_cluster_version(cluster):
"""Return major/minor version #"""
# Get control-cluster nodes
uri = "/ws.v1/control-cluster/node?_page_length=1&fields=uuid"
try:
res = do_single_request("GET", uri, cluster=cluster)
res = do_single_request(HTTP_GET, uri, cluster=cluster)
res = json.loads(res)
except NvpApiClient.NvpApiException:
raise exception.QuantumException()
@ -79,7 +122,7 @@ def get_cluster_version(cluster):
# running different version so we just need the first node version.
uri = "/ws.v1/control-cluster/node/%s/status" % node_uuid
try:
res = do_single_request("GET", uri, cluster=cluster)
res = do_single_request(HTTP_GET, uri, cluster=cluster)
res = json.loads(res)
except NvpApiClient.NvpApiException:
raise exception.QuantumException()
@ -97,7 +140,7 @@ def get_all_query_pages(path, c):
while need_more_results:
page_cursor_str = (
"_page_cursor=%s" % page_cursor if page_cursor else "")
res = do_single_request("GET", "%s%s%s" %
res = do_single_request(HTTP_GET, "%s%s%s" %
(path, query_marker, page_cursor_str),
cluster=c)
body = json.loads(res)
@ -155,48 +198,83 @@ def find_lswitch_by_portid(clusters, port_id):
return (None, None)
def get_network(cluster, net_id):
path = "/ws.v1/lswitch/%s" % net_id
def get_lswitches(cluster, quantum_net_id):
lswitch_uri_path = _build_uri_path(LSWITCH_RESOURCE, quantum_net_id,
relations="LogicalSwitchStatus")
results = []
try:
resp_obj = do_single_request("GET", path, cluster=cluster)
network = json.loads(resp_obj)
LOG.warning(_("### nw:%s"), network)
except NvpApiClient.ResourceNotFound:
raise exception.NetworkNotFound(net_id=net_id)
except NvpApiClient.NvpApiException:
raise exception.QuantumException()
LOG.debug(_("Got network '%(net_id)s': %(network)s"), locals())
return network
def create_lswitch(cluster, lswitch_obj):
LOG.info(_("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),
resp_obj = do_single_request(HTTP_GET,
lswitch_uri_path,
cluster=cluster)
ls = json.loads(resp_obj)
results.append(ls)
for tag in ls['tags']:
if (tag['scope'] == "multi_lswitch" and
tag['tag'] == "True"):
# Fetch extra logical switches
extra_lswitch_uri_path = _build_uri_path(
LSWITCH_RESOURCE,
fields="uuid,display_name,tags,lport_count",
relations="LogicalSwitchStatus",
filters={'tag': quantum_net_id,
'tag_scope': 'quantum_net_id'})
extra_switches = get_all_query_pages(extra_lswitch_uri_path,
cluster)
results.extend(extra_switches)
return results
except NvpApiClient.NvpApiException:
# TODO(salvatore-olrando): Do a better exception handling
# and re-raising
LOG.exception(_("An error occured while fetching logical switches "
"for Quantum network %s"), quantum_net_id)
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 create_lswitch(cluster, tenant_id, display_name,
transport_type=None,
transport_zone_uuid=None,
vlan_id=None,
quantum_net_id=None,
**kwargs):
nvp_binding_type = transport_type
if transport_type in ('flat', 'vlan'):
nvp_binding_type = 'bridge'
transport_zone_config = {"zone_uuid": (transport_zone_uuid or
cluster.default_tz_uuid),
"transport_type": (nvp_binding_type or
DEF_TRANSPORT_TYPE)}
lswitch_obj = {"display_name": display_name,
"transport_zones": [transport_zone_config],
"tags": [{"tag": tenant_id, "scope": "os_tid"}]}
if nvp_binding_type == 'bridge' and vlan_id:
transport_zone_config["binding_config"] = {"vlan_translation":
[{"transport": vlan_id}]}
if quantum_net_id:
lswitch_obj["tags"].append({"tag": quantum_net_id,
"scope": "quantum_net_id"})
if "tags" in kwargs:
lswitch_obj["tags"].extend(kwargs["tags"])
uri = _build_uri_path(LSWITCH_RESOURCE)
try:
lswitch_res = do_single_request(HTTP_POST, uri,
json.dumps(lswitch_obj),
cluster=cluster)
except NvpApiClient.NvpApiException:
raise exception.QuantumException()
lswitch = json.loads(lswitch_res)
LOG.debug(_("Created logical switch: %s") % lswitch['uuid'])
return lswitch
def update_network(cluster, lswitch_id, **params):
uri = "/ws.v1/lswitch/" + lswitch_id
lswitch_obj = {}
if params["network"]["name"]:
lswitch_obj["display_name"] = params["network"]["name"]
def update_lswitch(cluster, lswitch_id, display_name,
tenant_id=None, **kwargs):
uri = _build_uri_path(LSWITCH_RESOURCE, resource_id=lswitch_id)
# TODO(salvatore-orlando): Make sure this operation does not remove
# any other important tag set on the lswtich object
lswitch_obj = {"display_name": display_name,
"tags": [{"tag": tenant_id, "scope": "os_tid"}]}
if "tags" in kwargs:
lswitch_obj["tags"].extend(kwargs["tags"])
try:
resp_obj = do_single_request("PUT", uri, json.dumps(lswitch_obj),
cluster=cluster)
@ -262,26 +340,6 @@ def delete_networks(cluster, net_id, lswitch_ids):
raise exception.QuantumException()
def create_network(tenant_id, net_name, **kwargs):
clusters = kwargs["clusters"]
# Default to the primary cluster
cluster = clusters[0]
transport_zone = kwargs.get("transport_zone",
cluster.default_tz_uuid)
transport_type = kwargs.get("transport_type", "stt")
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(cluster, lswitch_obj)
net['net-op-status'] = constants.NET_STATUS_ACTIVE
return net
def query_ports(cluster, network, relations=None, fields="*", filters=None):
uri = "/ws.v1/lswitch/" + network + "/lport?"
if relations:
@ -328,7 +386,7 @@ def get_port_by_quantum_tag(clusters, lswitch, quantum_tag):
if len(res["results"]) == 1:
return (res["results"][0], c)
LOG.error(_("Port or Network not found, Error: %s"), str(e))
LOG.error(_("Port or Network not found"))
raise exception.PortNotFound(port_id=quantum_tag, net_id=lswitch)
@ -403,38 +461,32 @@ def update_port(network, port_id, **params):
return obj
def create_port(tenant, **params):
clusters = params["clusters"]
dest_cluster = clusters[0] # primary cluster
ls_uuid = params["port"]["network_id"]
def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id,
display_name, device_id, admin_status_enabled,
mac_address=None, fixed_ips=None):
""" Creates a logical port on the assigned logical switch """
# device_id can be longer than 40 so we rehash it
device_id = hashlib.sha1(params["port"]["device_id"]).hexdigest()
hashed_device_id = hashlib.sha1(device_id).hexdigest()
lport_obj = dict(
admin_status_enabled=params["port"]["admin_state_up"],
display_name=params["port"]["name"],
tags=[dict(scope='os_tid', tag=tenant),
dict(scope='q_port_id', tag=params["port"]["id"]),
dict(scope='vm_id', tag=device_id)]
admin_status_enabled=admin_status_enabled,
display_name=display_name,
tags=[dict(scope='os_tid', tag=tenant_id),
dict(scope='q_port_id', tag=quantum_port_id),
dict(scope='vm_id', tag=hashed_device_id)],
)
path = "/ws.v1/lswitch/" + ls_uuid + "/lport"
path = _build_uri_path(LPORT_RESOURCE, parent_resource_id=lswitch_uuid)
try:
resp_obj = do_single_request("POST", path, json.dumps(lport_obj),
cluster=dest_cluster)
resp_obj = do_single_request("POST", path,
json.dumps(lport_obj),
cluster=cluster)
except NvpApiClient.ResourceNotFound as e:
LOG.error("Network not found, Error: %s" % str(e))
raise exception.NetworkNotFound(net_id=params["port"]["network_id"])
except NvpApiClient.NvpApiException as e:
raise exception.QuantumException()
LOG.error("Logical switch not found, Error: %s" % str(e))
raise
result = json.loads(resp_obj)
result['port-op-status'] = get_port_status(dest_cluster, ls_uuid,
result['uuid'])
params["port"].update({"admin_state_up": result["admin_status_enabled"],
"status": result["port-op-status"]})
return (params["port"], result['uuid'])
LOG.debug("Created logical port %s on logical swtich %s"
% (result['uuid'], lswitch_uuid))
return result
def get_port_status(cluster, lswitch_id, port_id):
@ -455,10 +507,8 @@ def get_port_status(cluster, lswitch_id, port_id):
return constants.PORT_STATUS_DOWN
def plug_interface(clusters, lswitch_id, port, type, attachment=None):
dest_cluster = clusters[0] # primary cluster
def plug_interface(cluster, lswitch_id, port, type, attachment=None):
uri = "/ws.v1/lswitch/" + lswitch_id + "/lport/" + port + "/attachment"
lport_obj = {}
if attachment:
lport_obj["vif_uuid"] = attachment
@ -466,7 +516,7 @@ def plug_interface(clusters, lswitch_id, port, type, attachment=None):
lport_obj["type"] = type
try:
resp_obj = do_single_request("PUT", uri, json.dumps(lport_obj),
cluster=dest_cluster)
cluster=cluster)
except NvpApiClient.ResourceNotFound as e:
LOG.error(_("Port or Network not found, Error: %s"), str(e))
raise exception.PortNotFound(port_id=port, net_id=lswitch_id)

View File

@ -4,7 +4,9 @@
"_relations": {"LogicalSwitchStatus":
{"fabric_status": true,
"type": "LogicalSwitchStatus",
"lport_count": %(lport_count)d,
"_href": "/ws.v1/lswitch/%(uuid)s/status",
"_schema": "/ws.v1/schema/LogicalSwitchStatus"}},
"type": "LogicalSwitchConfig",
"tags": %(tags_json)s,
"uuid": "%(uuid)s"}

View File

@ -77,6 +77,7 @@ class FakeClient:
zone_uuid = fake_lswitch['transport_zones'][0]['zone_uuid']
fake_lswitch['zone_uuid'] = zone_uuid
fake_lswitch['tenant_id'] = self._get_tag(fake_lswitch, 'os_tid')
fake_lswitch['lport_count'] = 0
return fake_lswitch
def _add_lport(self, body, ls_uuid):
@ -92,6 +93,7 @@ class FakeClient:
self._fake_lport_dict[fake_lport['uuid']] = fake_lport
fake_lswitch = self._fake_lswitch_dict[ls_uuid]
fake_lswitch['lport_count'] += 1
fake_lport_status = fake_lport.copy()
fake_lport_status['ls_tenant_id'] = fake_lswitch['tenant_id']
fake_lport_status['ls_uuid'] = fake_lswitch['uuid']
@ -115,7 +117,6 @@ class FakeClient:
def _list(self, resource_type, response_file,
switch_uuid=None, query=None):
(tag_filter, attr_filter) = self._get_filters(query)
with open("%s/%s" % (self.fake_files_path, response_file)) as f:
response_template = f.read()
res_dict = getattr(self, '_fake_%s_dict' % resource_type)
@ -143,7 +144,9 @@ class FakeClient:
res_dict[res_uuid].get('ls_uuid') == switch_uuid):
return True
return False
for item in res_dict.itervalues():
if 'tags' in item:
item['tags_json'] = json.dumps(item['tags'])
items = [json.loads(response_template % res_dict[res_uuid])
for res_uuid in res_dict
if (_lswitch_match(res_uuid) and
@ -159,6 +162,10 @@ class FakeClient:
with open("%s/%s" % (self.fake_files_path, response_file)) as f:
response_template = f.read()
res_dict = getattr(self, '_fake_%s_dict' % resource_type)
for item in res_dict.itervalues():
if 'tags' in item:
item['tags_json'] = json.dumps(item['tags'])
items = [json.loads(response_template % res_dict[res_uuid])
for res_uuid in res_dict if res_uuid == target_uuid]
if items:

View File

@ -13,14 +13,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import mock
import webob.exc
import quantum.common.test_lib as test_lib
from quantum import context
from quantum.extensions import providernet as pnet
from quantum import manager
from quantum.openstack.common import cfg
from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
from quantum.tests.unit.nicira import fake_nvpapiclient
import quantum.tests.unit.test_db_plugin as test_plugin
LOG = logging.getLogger(__name__)
NICIRA_PKG_PATH = 'quantum.plugins.nicira.nicira_nvp_plugin'
@ -28,6 +36,26 @@ class NiciraPluginV2TestCase(test_plugin.QuantumDbPluginV2TestCase):
_plugin_name = ('%s.QuantumPlugin.NvpPluginV2' % NICIRA_PKG_PATH)
def _create_network(self, fmt, name, admin_status_up,
arg_list=None, providernet_args=None, **kwargs):
data = {'network': {'name': name,
'admin_state_up': admin_status_up,
'tenant_id': self._tenant_id}}
attributes = kwargs
if providernet_args:
attributes.update(providernet_args)
for arg in (('admin_state_up', 'tenant_id', 'shared') +
(arg_list or ())):
# Arg must be present and not empty
if arg in kwargs and kwargs[arg]:
data['network'][arg] = kwargs[arg]
network_req = self.new_create_request('networks', data, fmt)
if (kwargs.get('set_context') and 'tenant_id' in kwargs):
# create a specific auth context for this request
network_req.environ['quantum.context'] = context.Context(
'', kwargs['tenant_id'])
return network_req.get_response(self.api)
def setUp(self):
etc_path = os.path.join(os.path.dirname(__file__), 'etc')
test_lib.test_config['config_files'] = [os.path.join(etc_path,
@ -61,9 +89,66 @@ class TestNiciraV2HTTPResponse(test_plugin.TestV2HTTPResponse,
class TestNiciraPortsV2(test_plugin.TestPortsV2, NiciraPluginV2TestCase):
pass
def test_exhaust_ports_overlay_network(self):
cfg.CONF.set_override('max_lp_per_overlay_ls', 1, group='NVP')
with self.network(name='testnet',
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK,
pnet.SEGMENTATION_ID)) as net:
with self.subnet(network=net) as sub:
with self.port(subnet=sub):
# creating another port should see an exception
self._create_port('json', net['network']['id'], 400)
def test_exhaust_ports_bridged_network(self):
cfg.CONF.set_override('max_lp_per_bridged_ls', 1, group="NVP")
providernet_args = {pnet.NETWORK_TYPE: 'flat',
pnet.PHYSICAL_NETWORK: 'tzuuid'}
with self.network(name='testnet',
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK,
pnet.SEGMENTATION_ID)) as net:
with self.subnet(network=net) as sub:
with self.port(subnet=sub):
with self.port(subnet=sub):
plugin = manager.QuantumManager.get_plugin()
ls = nvplib.get_lswitches(plugin.default_cluster,
net['network']['id'])
self.assertEqual(len(ls), 2)
class TestNiciraNetworksV2(test_plugin.TestNetworksV2,
NiciraPluginV2TestCase):
pass
def _test_create_bridge_network(self, vlan_id=None):
net_type = vlan_id and 'vlan' or 'flat'
name = 'bridge_net'
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
('status', 'ACTIVE'), ('shared', False),
(pnet.NETWORK_TYPE, net_type),
(pnet.PHYSICAL_NETWORK, 'tzuuid'),
(pnet.SEGMENTATION_ID, vlan_id)]
providernet_args = {pnet.NETWORK_TYPE: net_type,
pnet.PHYSICAL_NETWORK: 'tzuuid'}
if vlan_id:
providernet_args[pnet.SEGMENTATION_ID] = vlan_id
with self.network(name=name,
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK,
pnet.SEGMENTATION_ID)) as net:
for k, v in keys:
self.assertEquals(net['network'][k], v)
def test_create_bridge_network(self):
self._test_create_bridge_network()
def test_create_bridge_vlan_network(self):
self._test_create_bridge_network(vlan_id=123)
def test_create_bridge_vlan_network_outofrange_returns_400(self):
with self.assertRaises(webob.exc.HTTPClientError) as ctx_manager:
self._test_create_bridge_network(vlan_id=5000)
self.assertEquals(ctx_manager.exception.code, 400)