Add Quantum support for NVP Layer-2 gateways

Blueprint nvp-nwgw-api

This patch adds an API extension, the relevant DB logic, and the NVP
plugin logic for managing a NVP-specific feature, Layer-2 Network
Gateway, through the Quantum API.
The proposed extension is meant to be used with the NVP plugin only.

Change-Id: I73a8f1782c345ca7f6dec2db36ba6f9299b30d04
This commit is contained in:
Salvatore Orlando 2012-11-25 17:11:42 -08:00 committed by Salvatore Orlando
parent 68a129632e
commit 3e8b32263d
18 changed files with 1888 additions and 159 deletions

View File

@ -21,6 +21,10 @@ reconnect_interval = 2
# Timeout in seconds before idle sql connections are reaped
# sql_idle_timeout = 3600
[QUOTAS]
# number of network gateways allowed per tenant, -1 means unlimited
# quota_network_gateway = 5
[NVP]
# Maximum number of ports for each bridged logical switch
# max_lp_per_bridged_ls = 64
@ -56,6 +60,16 @@ reconnect_interval = 2
# with external gateways
# default_l3_gw_service_uuid =
# UUID of the default layer 2 gateway service to use for this cluster
# This is optional. It should be filled for providing a predefined gateway
# tenant case use for connecting their networks.
# default_l2_gw_service_uuid =
# Name of the default interface name to be used on network-gateway.
# This value will be used for any device associated with a network
# gateway for which an interface name was not specified
# default_iface_name = breth0
# This parameter describes a connection to a single NVP controller. Format:
# <ip>:<port>:<user>:<pw>:<req_timeout>:<http_timeout>:<retries>:<redirects>
# <ip> is the ip address of the controller

View File

@ -0,0 +1,97 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 OpenStack LLC
#
# 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_network_gw
Revision ID: 363468ac592c
Revises: 38335592a0dc
Create Date: 2013-02-07 03:19:14.455372
"""
# revision identifiers, used by Alembic.
revision = '363468ac592c'
down_revision = '38335592a0dc'
# Change to ['*'] if this migration applies to all plugins
migration_for_plugins = [
'quantum.plugins.nicira.nicira_nvp_plugin.QuantumPluginV2.NvpPluginV2'
]
from alembic import op
import sqlalchemy as sa
from quantum.db import migration
def upgrade(active_plugin=None, options=None):
if not migration.should_run(active_plugin, migration_for_plugins):
return
op.create_table('networkgateways',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('tenant_id', sa.String(length=36),
nullable=True),
sa.Column('shared', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'))
op.create_table('networkgatewaydevices',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('network_gateway_id', sa.String(length=36),
nullable=True),
sa.Column('interface_name', sa.String(length=64),
nullable=True),
sa.ForeignKeyConstraint(['network_gateway_id'],
['networkgateways.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'))
op.create_table('networkconnections',
sa.Column('tenant_id', sa.String(length=255),
nullable=True),
sa.Column('network_gateway_id', sa.String(length=36),
nullable=True),
sa.Column('network_id', sa.String(length=36),
nullable=True),
sa.Column('segmentation_type',
sa.Enum('flat', 'vlan',
name="net_conn_seg_type"),
nullable=True),
sa.Column('segmentation_id', sa.Integer(),
nullable=True),
sa.Column('port_id', sa.String(length=36),
nullable=False),
sa.ForeignKeyConstraint(['network_gateway_id'],
['networkgateways.id'],
ondelete='CASCADE'),
sa.ForeignKeyConstraint(['network_id'], ['networks.id'],
ondelete='CASCADE'),
sa.ForeignKeyConstraint(['port_id'], ['ports.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('port_id'),
sa.UniqueConstraint('network_gateway_id',
'segmentation_type',
'segmentation_id'))
def downgrade(active_plugin=None, options=None):
if not migration.should_run(active_plugin, migration_for_plugins):
return
op.drop_table('networkconnections')
op.drop_table('networkgatewaydevices')
op.drop_table('networkgateways')

View File

@ -30,6 +30,7 @@ import webob.exc
from quantum.api.v2 import attributes as attr
from quantum.api.v2 import base
from quantum.common import constants
from quantum import context as q_context
from quantum.common import exceptions as q_exc
from quantum.common import rpc as q_rpc
from quantum.common import topics
@ -55,15 +56,20 @@ from quantum import policy
from quantum.plugins.nicira.nicira_nvp_plugin.common import config
from quantum.plugins.nicira.nicira_nvp_plugin.common import (exceptions
as nvp_exc)
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_networkgw
as networkgw)
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_qos
as ext_qos)
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_db
from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient
from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
from quantum.plugins.nicira.nicira_nvp_plugin import (nicira_networkgw_db
as networkgw_db)
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_qos_db as qos_db
from quantum.plugins.nicira.nicira_nvp_plugin import nvp_cluster
from quantum.plugins.nicira.nicira_nvp_plugin.nvp_plugin_version import (
PLUGIN_VERSION)
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_qos_db as qos_db
from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient
from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
LOG = logging.getLogger("QuantumPlugin")
NVP_FLOATINGIP_NAT_RULES_ORDER = 200
NVP_EXTGW_NAT_RULES_ORDER = 255
@ -108,11 +114,74 @@ def parse_config():
'nvp_controller_connection':
nvp_conf[cluster_name].nvp_controller_connection,
'default_l3_gw_service_uuid':
nvp_conf[cluster_name].default_l3_gw_service_uuid})
nvp_conf[cluster_name].default_l3_gw_service_uuid,
'default_l2_gw_service_uuid':
nvp_conf[cluster_name].default_l2_gw_service_uuid,
'default_interface_name':
nvp_conf[cluster_name].default_interface_name})
LOG.debug(_("Cluster options:%s"), clusters_options)
return cfg.CONF.NVP, clusters_options
def parse_clusters_opts(clusters_opts, concurrent_connections,
nvp_gen_timeout, default_cluster_name):
# Will store the first cluster in case is needed for default
# cluster assignment
clusters = {}
first_cluster = None
for c_opts in clusters_opts:
# Password is guaranteed to be the same across all controllers
# in the same NVP cluster.
cluster = nvp_cluster.NVPCluster(c_opts['name'])
try:
for ctrl_conn in c_opts['nvp_controller_connection']:
args = ctrl_conn.split(':')
try:
args.extend([c_opts['default_tz_uuid'],
c_opts['nvp_cluster_uuid'],
c_opts['nova_zone_id'],
c_opts['default_l3_gw_service_uuid'],
c_opts['default_l2_gw_service_uuid'],
c_opts['default_interface_name']])
cluster.add_controller(*args)
except Exception:
LOG.exception(_("Invalid connection parameters for "
"controller %(ctrl)s in "
"cluster %(cluster)s"),
{'ctrl': ctrl_conn,
'cluster': c_opts['name']})
raise nvp_exc.NvpInvalidConnection(
conn_params=ctrl_conn)
except TypeError:
msg = _("No controller connection specified in cluster "
"configuration. Please ensure at least a value for "
"'nvp_controller_connection' is specified in the "
"[CLUSTER:%s] section") % c_opts['name']
LOG.exception(msg)
raise nvp_exc.NvpPluginException(err_desc=msg)
api_providers = [(x['ip'], x['port'], True)
for x in cluster.controllers]
cluster.api_client = NvpApiClient.NVPApiHelper(
api_providers, cluster.user, cluster.password,
request_timeout=cluster.request_timeout,
http_timeout=cluster.http_timeout,
retries=cluster.retries,
redirects=cluster.redirects,
concurrent_connections=concurrent_connections,
nvp_gen_timeout=nvp_gen_timeout)
if not clusters:
first_cluster = cluster
clusters[c_opts['name']] = cluster
if default_cluster_name and default_cluster_name in clusters:
default_cluster = clusters[default_cluster_name]
else:
default_cluster = first_cluster
return (clusters, default_cluster)
class NVPRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
# Set RPC API version to 1.0 by default.
@ -131,8 +200,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
l3_db.L3_NAT_db_mixin,
portsecurity_db.PortSecurityDbMixin,
securitygroups_db.SecurityGroupDbMixin,
nvp_sec.NVPSecurityGroups,
networkgw_db.NetworkGatewayMixin,
qos_db.NVPQoSDbMixin,
nvp_sec.NVPSecurityGroups,
nvp_meta.NvpMetadataAccess):
"""
NvpPluginV2 is a Quantum plugin that provides L2 Virtual Network
@ -140,10 +210,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
"""
supported_extension_aliases = ["provider", "quotas", "port-security",
"router", "security-group", "nvp-qos"]
"router", "security-group", "nvp-qos",
"network-gateway"]
__native_bulk_support = True
# Default controller cluster
# Map nova zones to cluster for easy retrieval
novazone_cluster_map = {}
# Default controller cluster (to be used when nova zone id is unspecified)
@ -168,6 +239,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
self._nvp_create_port,
l3_db.DEVICE_OWNER_FLOATINGIP:
self._nvp_create_fip_port,
l3_db.DEVICE_OWNER_ROUTER_INTF:
self._nvp_create_router_port,
networkgw_db.DEVICE_OWNER_NET_GW_INTF:
self._nvp_create_l2_gw_port,
'default': self._nvp_create_port},
'delete': {l3_db.DEVICE_OWNER_ROUTER_GW:
self._nvp_delete_ext_gw_port,
@ -175,66 +250,61 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
self._nvp_delete_router_port,
l3_db.DEVICE_OWNER_FLOATINGIP:
self._nvp_delete_fip_port,
l3_db.DEVICE_OWNER_ROUTER_INTF:
self._nvp_delete_port,
networkgw_db.DEVICE_OWNER_NET_GW_INTF:
self._nvp_delete_port,
'default': self._nvp_delete_port}
}
self.nvp_opts, self.clusters_opts = parse_config()
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 = nvp_cluster.NVPCluster(c_opts['name'])
for controller_connection in c_opts['nvp_controller_connection']:
args = controller_connection.split(':')
try:
args.extend([c_opts['default_tz_uuid'],
c_opts['nvp_cluster_uuid'],
c_opts['nova_zone_id'],
c_opts['default_l3_gw_service_uuid']])
cluster.add_controller(*args)
except Exception:
LOG.exception(_("Invalid connection parameters for "
"controller %(conn)s in cluster %(name)s"),
{'conn': controller_connection,
'name': c_opts['name']})
raise nvp_exc.NvpInvalidConnection(
conn_params=controller_connection)
if not self.clusters_opts:
msg = _("No cluster specified in NVP plugin configuration. "
"Unable to start. Please ensure at least a "
"[CLUSTER:<cluster_name>] section is specified in "
"the NVP Plugin configuration file.")
LOG.error(msg)
raise nvp_exc.NvpPluginException(err_desc=msg)
api_providers = [(x['ip'], x['port'], True)
for x in cluster.controllers]
cluster.api_client = NvpApiClient.NVPApiHelper(
api_providers, cluster.user, cluster.password,
request_timeout=cluster.request_timeout,
http_timeout=cluster.http_timeout,
retries=cluster.retries,
redirects=cluster.redirects,
concurrent_connections=self.nvp_opts['concurrent_connections'],
nvp_gen_timeout=self.nvp_opts['nvp_gen_timeout'])
if len(self.clusters) == 0:
first_cluster = cluster
self.clusters[c_opts['name']] = cluster
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 def_cluster_name not 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]
self.clusters, self.default_cluster = parse_clusters_opts(
self.clusters_opts, self.nvp_opts.concurrent_connections,
self.nvp_opts.nvp_gen_timeout, self.nvp_opts.default_cluster_name)
db.configure_db()
# Extend the fault map
self._extend_fault_map()
# Set up RPC interface for DHCP agent
self.setup_rpc()
# TODO(salvatore-orlando): Handle default gateways in multiple clusters
self._ensure_default_network_gateway()
def _ensure_default_network_gateway(self):
# Add the gw in the db as default, and unset any previous default
def_l2_gw_uuid = self.default_cluster.default_l2_gw_service_uuid
try:
ctx = q_context.get_admin_context()
self._unset_default_network_gateways(ctx)
if not def_l2_gw_uuid:
return
try:
def_network_gw = self._get_network_gateway(ctx,
def_l2_gw_uuid)
except sa_exc.NoResultFound:
# Create in DB only - don't go on NVP
def_gw_data = {'id': def_l2_gw_uuid,
'name': 'default L2 gateway service',
'devices': []}
gw_res_name = networkgw.RESOURCE_NAME.replace('-', '_')
def_network_gw = super(
NvpPluginV2, self).create_network_gateway(
ctx, {gw_res_name: def_gw_data})
# In any case set is as default
self._set_default_network_gateway(ctx, def_network_gw['id'])
except Exception:
# This is fatal - abort startup
LOG.exception(_("Unable to process default l2 gw service:%s"),
def_l2_gw_uuid)
raise
def _build_ip_address_list(self, context, fixed_ips, subnet_ids=None):
""" Build ip_addresses data structure for logical router port
@ -326,6 +396,40 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
ip.subnet_id).cidr)
return cidrs
def _nvp_find_lswitch_for_port(self, context, port_data):
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:
cluster = self._find_target_cluster(port_data)
return self._handle_lswitch_selection(
cluster, network, network_binding, max_ports,
allow_extra_lswitches)
except NvpApiClient.NvpApiException:
err_desc = _(("An exception occured while selecting logical "
"switch for the port"))
LOG.exception(err_desc)
raise nvp_exc.NvpPluginException(err_desc=err_desc)
def _nvp_create_port_helper(self, cluster, ls_uuid, port_data,
do_port_security=True):
return nvplib.create_lport(cluster, ls_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'],
port_data[psec.PORTSECURITY],
port_data[ext_sg.SECURITYGROUPS],
port_data[ext_qos.QUEUE])
def _nvp_create_port(self, context, port_data):
""" Driver for creating a logical switch port on NVP platform """
# FIXME(salvatore-orlando): On the NVP platform we do not really have
@ -339,54 +443,30 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
port_data['network_id'])
# No need to actually update the DB state - the default is down
return port_data
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:
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'],
port_data[psec.PORTSECURITY],
port_data[ext_sg.SECURITYGROUPS],
port_data[ext_qos.QUEUE])
selected_lswitch = self._nvp_find_lswitch_for_port(context,
port_data)
lport = self._nvp_create_port_helper(cluster,
selected_lswitch['uuid'],
port_data,
True)
nicira_db.add_quantum_nvp_port_mapping(
context.session, port_data['id'], lport['uuid'])
d_owner = port_data['device_owner']
if (not d_owner in (l3_db.DEVICE_OWNER_ROUTER_GW,
if (not port_data['device_owner'] in
(l3_db.DEVICE_OWNER_ROUTER_GW,
l3_db.DEVICE_OWNER_ROUTER_INTF)):
nvplib.plug_interface(cluster, lswitch_uuid,
nvplib.plug_interface(cluster, selected_lswitch['uuid'],
lport['uuid'], "VifAttachment",
port_data['id'])
LOG.debug(_("_nvp_create_port completed for port %(port_name)s "
"on network %(net_id)s. The new port id is "
"%(port_id)s. NVP port id is %(nvp_port_id)s"),
{'port_name': port_data['name'],
'net_id': port_data['network_id'],
'port_id': port_data['id'],
'nvp_port_id': lport['uuid']})
except Exception:
# failed to create port in NVP delete port from quantum_db
LOG.exception(_("An exception occured while plugging "
"the interface"))
raise
LOG.debug(_("_nvp_create_port completed for port %(name)s "
"on network %(network_id)s. The new port id is "
"%(id)s."), port_data)
except NvpApiClient.NvpApiException:
msg = (_("An exception occured while plugging the interface "
"into network:%s") % port_data['network_id'])
LOG.exception(msg)
raise q_exc.QuantumException(message=msg)
def _nvp_delete_port(self, context, port_data):
# FIXME(salvatore-orlando): On the NVP platform we do not really have
@ -441,6 +521,35 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
# Delete logical switch port
self._nvp_delete_port(context, port_data)
def _nvp_create_router_port(self, context, port_data):
""" Driver for creating a switch port to be connected to a router """
# No router ports on external networks!
if self._network_is_external(context, port_data['network_id']):
raise nvp_exc.NvpPluginException(
err_msg=(_("It is not allowed to create router interface "
"ports on external networks as '%s'") %
port_data['network_id']))
try:
selected_lswitch = self._nvp_find_lswitch_for_port(context,
port_data)
cluster = self._find_target_cluster(port_data)
# Do not apply port security here!
lport = self._nvp_create_port_helper(cluster,
selected_lswitch['uuid'],
port_data,
False)
nicira_db.add_quantum_nvp_port_mapping(
context.session, port_data['id'], lport['uuid'])
LOG.debug(_("_nvp_create_port completed for port %(name)s on "
"network %(network_id)s. The new port id is %(id)s."),
port_data)
except Exception:
# failed to create port in NVP delete port from quantum_db
LOG.exception(_("An exception occured while plugging "
"the interface"))
super(NvpPluginV2, self).delete_port(context, port_data["id"])
raise
def _find_router_gw_port(self, context, port_data):
router_id = port_data['device_id']
cluster = self._find_target_cluster(port_data)
@ -534,6 +643,46 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
{'ext_net_id': port_data['network_id'],
'router_id': router_id})
def _nvp_create_l2_gw_port(self, context, port_data):
""" Create a switch port, and attach it to a L2 gateway attachment """
# FIXME(salvatore-orlando): On the NVP platform we do not really have
# external networks. So if as user tries and create a "regular" VIF
# port on an external network we are unable to actually create.
# However, in order to not break unit tests, we need to still create
# the DB object and return success
if self._network_is_external(context, port_data['network_id']):
LOG.error(_("NVP plugin does not support regular VIF ports on "
"external networks. Port %s will be down."),
port_data['network_id'])
# No need to actually update the DB state - the default is down
return port_data
try:
cluster = self._find_target_cluster(port_data)
selected_lswitch = self._nvp_find_lswitch_for_port(context,
port_data)
lport = self._nvp_create_port_helper(cluster,
selected_lswitch['uuid'],
port_data,
True)
nicira_db.add_quantum_nvp_port_mapping(
context.session, port_data['id'], lport['uuid'])
nvplib.plug_l2_gw_service(
cluster,
port_data['network_id'],
lport['uuid'],
port_data['device_id'],
int(port_data.get('gw:segmentation_id') or 0))
LOG.debug(_("_nvp_create_port completed for port %(name)s "
"on network %(network_id)s. The new port id "
"is %(id)s."), port_data)
except NvpApiClient.NvpApiException:
# failed to create port in NVP delete port from quantum_db
msg = (_("An exception occured while plugging the gateway "
"interface into network:%s") % port_data['network_id'])
LOG.exception(msg)
super(NvpPluginV2, self).delete_port(context, port_data["id"])
raise q_exc.QuantumException(message=msg)
def _nvp_create_fip_port(self, context, port_data):
# As we do not create ports for floating IPs in NVP,
# this is a no-op driver
@ -1222,12 +1371,28 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
LOG.warn(_("Unable to retrieve port status for:%s."), nvp_port_id)
return ret_port
def delete_port(self, context, id, l3_port_check=True):
def delete_port(self, context, id, l3_port_check=True,
nw_gw_port_check=True):
"""
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: None
:raises: exception.PortInUse
:raises: exception.PortNotFound
:raises: exception.NetworkNotFound
"""
# if needed, check to see if this is a port owned by
# a l3 router. If so, we should prevent deletion here
if l3_port_check:
self.prevent_l3_port_deletion(context, id)
quantum_db_port = self._get_port(context, id)
# Perform the same check for ports owned by layer-2 gateways
if nw_gw_port_check:
self.prevent_network_gateway_port_deletion(context,
quantum_db_port)
port_delete_func = self._port_drivers['delete'].get(
quantum_db_port.device_owner,
self._port_drivers['delete']['default'])
@ -1759,6 +1924,72 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
port_id)
super(NvpPluginV2, self).disassociate_floatingips(context, port_id)
def create_network_gateway(self, context, network_gateway):
""" Create a layer-2 network gateway
Create the gateway service on NVP platform and corresponding data
structures in Quantum datase
"""
# Need to re-do authZ checks here in order to avoid creation on NVP
gw_data = network_gateway[networkgw.RESOURCE_NAME.replace('-', '_')]
tenant_id = self._get_tenant_id_for_create(context, gw_data)
cluster = self._find_target_cluster(gw_data)
devices = gw_data['devices']
# Populate default physical network where not specified
for device in devices:
if not device.get('interface_name'):
device['interface_name'] = cluster.default_interface_name
try:
nvp_res = nvplib.create_l2_gw_service(cluster, tenant_id,
gw_data['name'],
devices)
nvp_uuid = nvp_res.get('uuid')
except Exception:
raise nvp_exc.NvpPluginException(_("Create_l2_gw_service did not "
"return an uuid for the newly "
"created resource:%s") %
nvp_res)
gw_data['id'] = nvp_uuid
return super(NvpPluginV2, self).create_network_gateway(context,
network_gateway)
def delete_network_gateway(self, context, id):
""" Remove a layer-2 network gateway
Remove the gateway service from NVP platform and corresponding data
structures in Quantum datase
"""
with context.session.begin(subtransactions=True):
try:
super(NvpPluginV2, self).delete_network_gateway(context, id)
nvplib.delete_l2_gw_service(self.default_cluster, id)
except NvpApiClient.ResourceNotFound:
# Do not cause a 500 to be returned to the user if
# the corresponding NVP resource does not exist
LOG.exception(_("Unable to remove gateway service from "
"NVP plaform - the resource was not found"))
def _ensure_tenant_on_net_gateway(self, context, net_gateway):
if not net_gateway['tenant_id']:
net_gateway['tenant_id'] = context.tenant_id
return net_gateway
def get_network_gateway(self, context, id, fields=None):
# Ensure the tenant_id attribute is populated on the returned gateway
#return self._ensure_tenant_on_net_gateway(
# context, super(NvpPluginV2, self).get_network_gateway(
# context, id, fields))
return super(NvpPluginV2, self).get_network_gateway(context,
id, fields)
def get_network_gateways(self, context, filters=None, fields=None):
# Ensure the tenant_id attribute is populated on returned gateways
net_gateways = super(NvpPluginV2,
self).get_network_gateways(context,
filters,
fields)
return net_gateways
def get_plugin_version(self):
return PLUGIN_VERSION

View File

@ -61,7 +61,14 @@ cluster_opts = [
cfg.StrOpt('default_l3_gw_service_uuid',
help=_("Unique identifier of the NVP L3 Gateway service "
"which will be used for implementing routers and "
"floating IPs"))
"floating IPs")),
cfg.StrOpt('default_l2_gw_service_uuid',
help=_("Unique identifier of the NVP L2 Gateway service "
"which will be used by default for network gateways")),
cfg.StrOpt('default_interface_name', default='breth0',
help=_("Name of the interface on a L2 Gateway transport node"
"which should be used by default when setting up a "
"network connection")),
]
# Register the configuration options

View File

@ -38,6 +38,12 @@ class NvpNoMorePortsException(NvpPluginException):
"Maximum number of ports reached")
class NvpPortAlreadyAttached(q_exc.Conflict):
message = _("Unable to plug an interface into the port %(port_id)s "
"for network %(net_id)s. This interface is already plugged "
"into port %(att_port_id)s")
class NvpNatRuleMismatch(NvpPluginException):
message = _("While retrieving NAT rules, %(actual_rules)s were found "
"whereas rules in the (%(min_rules)s,%(max_rules)s) interval "

View File

@ -0,0 +1,173 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 VMware. 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: Salvatore Orlando, VMware
from abc import abstractmethod
from quantum.api import extensions
from quantum.api.v2 import attributes
from quantum.api.v2 import base
from quantum import manager
from quantum.openstack.common import cfg
from quantum import quota
RESOURCE_NAME = "network-gateway"
COLLECTION_NAME = "%ss" % RESOURCE_NAME
EXT_ALIAS = RESOURCE_NAME
DEVICE_ID_ATTR = 'id'
IFACE_NAME_ATTR = 'interface_name'
# Attribute Map for Network Gateway Resource
# TODO(salvatore-orlando): add admin state as other quantum resources
RESOURCE_ATTRIBUTE_MAP = {
COLLECTION_NAME: {
'id': {'allow_post': False, 'allow_put': False,
'is_visible': True},
'name': {'allow_post': True, 'allow_put': True,
'validate': {'type:string': None},
'is_visible': True, 'default': ''},
'default': {'allow_post': False, 'allow_put': False,
'is_visible': True},
'devices': {'allow_post': True, 'allow_put': False,
'validate': {'type:device_list': None},
'is_visible': True},
'tenant_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:string': None},
'required_by_policy': True,
'is_visible': True}
}
}
def _validate_device_list(data, valid_values=None):
""" Validate the list of service definitions. """
if not data:
# Devices must be provided
msg = _("Cannot create a gateway with an empty device list")
return msg
try:
for device in data:
err_msg = attributes._validate_dict(
device,
key_specs={DEVICE_ID_ATTR:
{'type:regex': attributes.UUID_PATTERN,
'required': True},
IFACE_NAME_ATTR:
{'type:string': None,
'required': False}})
if err_msg:
return err_msg
except TypeError:
return (_("%s: provided data are not iterable") %
_validate_device_list.__name__)
nw_gw_quota_opts = [
cfg.IntOpt('quota_network_gateway',
default=5,
help=_('number of network gateways allowed per tenant, '
'-1 for unlimited'))
]
cfg.CONF.register_opts(nw_gw_quota_opts, 'QUOTAS')
attributes.validators['type:device_list'] = _validate_device_list
class Nvp_networkgw(object):
""" API extension for Layer-2 Gateway support.
The Layer-2 gateway feature allows for connecting quantum networks
with external networks at the layer-2 level. No assumption is made on
the location of the external network, which might not even be directly
reachable from the hosts where the VMs are deployed.
This is achieved by instantiating 'network gateways', and then connecting
Quantum network to them.
"""
@classmethod
def get_name(cls):
return "Quantum-NVP Network Gateway"
@classmethod
def get_alias(cls):
return EXT_ALIAS
@classmethod
def get_description(cls):
return "Connects Quantum networks with external networks at layer 2"
@classmethod
def get_namespace(cls):
return "http://docs.openstack.org/ext/quantum/network-gateway/api/v1.0"
@classmethod
def get_updated(cls):
return "2012-11-30T10:00:00-00:00"
@classmethod
def get_resources(cls):
""" Returns Ext Resources """
plugin = manager.QuantumManager.get_plugin()
params = RESOURCE_ATTRIBUTE_MAP.get(COLLECTION_NAME, dict())
member_actions = {'connect_network': 'PUT',
'disconnect_network': 'PUT'}
# register quotas for network gateways
quota.QUOTAS.register_resource_by_name(RESOURCE_NAME)
controller = base.create_resource(COLLECTION_NAME,
RESOURCE_NAME,
plugin, params,
member_actions=member_actions)
return [extensions.ResourceExtension(COLLECTION_NAME,
controller,
member_actions=member_actions)]
class NetworkGatewayPluginBase(object):
@abstractmethod
def create_network_gateway(self, context, network_gateway):
pass
@abstractmethod
def update_network_gateway(self, context, id, network_gateway):
pass
@abstractmethod
def get_network_gateway(self, context, id, fields=None):
pass
@abstractmethod
def delete_network_gateway(self, context, id):
pass
@abstractmethod
def get_network_gateways(self, context, filters=None, fields=None):
pass
@abstractmethod
def connect_network(self, context, network_gateway_id,
network_mapping_info):
pass
@abstractmethod
def disconnect_network(self, context, network_gateway_id,
network_mapping_info):
pass

View File

@ -15,12 +15,11 @@
# 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.openstack.common import log as logging
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_networkgw_db
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_models
LOG = logging.getLogger(__name__)
@ -71,3 +70,16 @@ def get_nvp_port_id(session, quantum_id):
return mapping['nvp_id']
except exc.NoResultFound:
return
def unset_default_network_gateways(session):
with session.begin(subtransactions=True):
session.query(nicira_networkgw_db.NetworkGateway).update(
{nicira_networkgw_db.NetworkGateway.default: False})
def set_default_network_gateway(session, gw_id):
with session.begin(subtransactions=True):
gw = (session.query(nicira_networkgw_db.NetworkGateway).
filter_by(id=gw_id).one())
gw['default'] = True

View File

@ -0,0 +1,356 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 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: Salvatore Orlando, VMware
#
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.orm import exc as sa_orm_exc
from webob import exc as web_exc
from quantum.api.v2 import attributes
from quantum.api.v2 import base
from quantum.common import exceptions
from quantum.db import db_base_plugin_v2
from quantum.db import model_base
from quantum.db import models_v2
from quantum.openstack.common import uuidutils
from quantum.openstack.common import log as logging
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import nvp_networkgw
from quantum import policy
LOG = logging.getLogger(__name__)
DEVICE_OWNER_NET_GW_INTF = 'network:gateway-interface'
NETWORK_ID = 'network_id'
SEGMENTATION_TYPE = 'segmentation_type'
SEGMENTATION_ID = 'segmentation_id'
ALLOWED_CONNECTION_ATTRIBUTES = set((NETWORK_ID,
SEGMENTATION_TYPE,
SEGMENTATION_ID))
class GatewayInUse(exceptions.InUse):
message = _("Network Gateway '%(gateway_id)s' still has active mappings "
"with one or more quantum networks.")
class NetworkGatewayPortInUse(exceptions.InUse):
message = _("Port '%(port_id)s' is owned by '%(device_owner)s' and "
"therefore cannot be deleted directly via the port API.")
class GatewayConnectionInUse(exceptions.InUse):
message = _("The specified mapping '%(mapping)s' is already in use on "
"network gateway '%(gateway_id)s'.")
class MultipleGatewayConnections(exceptions.QuantumException):
message = _("Multiple network connections found on '%(gateway_id)s' "
"with provided criteria.")
class GatewayConnectionNotFound(exceptions.NotFound):
message = _("The connection %(network_mapping_info)s was not found on the "
"network gateway '%(network_gateway_id)s'")
class NetworkGatewayUnchangeable(exceptions.InUse):
message = _("The network gateway %(gateway_id)s "
"cannot be updated or deleted")
# Add exceptions to HTTP Faults mappings
base.FAULT_MAP.update({GatewayInUse: web_exc.HTTPConflict,
NetworkGatewayPortInUse: web_exc.HTTPConflict,
GatewayConnectionInUse: web_exc.HTTPConflict,
GatewayConnectionNotFound: web_exc.HTTPNotFound,
MultipleGatewayConnections: web_exc.HTTPConflict})
class NetworkConnection(model_base.BASEV2, models_v2.HasTenant):
""" Defines a connection between a network gateway and a network """
# We use port_id as the primary key as one can connect a gateway
# to a network in multiple ways (and we cannot use the same port form
# more than a single gateway)
network_gateway_id = sa.Column(sa.String(36),
sa.ForeignKey('networkgateways.id',
ondelete='CASCADE'))
network_id = sa.Column(sa.String(36),
sa.ForeignKey('networks.id', ondelete='CASCADE'))
segmentation_type = sa.Column(
sa.Enum('flat', 'vlan',
name='networkconnections_segmentation_type'))
segmentation_id = sa.Column(sa.Integer)
__table_args__ = (sa.UniqueConstraint(network_gateway_id,
segmentation_type,
segmentation_id),)
# Also, storing port id comes back useful when disconnecting a network
# from a gateway
port_id = sa.Column(sa.String(36),
sa.ForeignKey('ports.id', ondelete='CASCADE'),
primary_key=True)
class NetworkGatewayDevice(model_base.BASEV2):
id = sa.Column(sa.String(36), primary_key=True)
network_gateway_id = sa.Column(sa.String(36),
sa.ForeignKey('networkgateways.id',
ondelete='CASCADE'))
interface_name = sa.Column(sa.String(64))
class NetworkGateway(model_base.BASEV2, models_v2.HasId,
models_v2.HasTenant):
""" Defines the data model for a network gateway """
name = sa.Column(sa.String(255))
# Tenant id is nullable for this resource
tenant_id = sa.Column(sa.String(36))
default = sa.Column(sa.Boolean())
devices = orm.relationship(NetworkGatewayDevice,
backref='networkgateways',
cascade='all,delete')
network_connections = orm.relationship(NetworkConnection)
class NetworkGatewayMixin(nvp_networkgw.NetworkGatewayPluginBase):
resource = nvp_networkgw.RESOURCE_NAME.replace('-', '_')
def _get_network_gateway(self, context, gw_id):
return self._get_by_id(context, NetworkGateway, gw_id)
def _make_network_gateway_dict(self, network_gateway, fields=None):
device_list = []
for d in network_gateway['devices']:
device_list.append({'id': d['id'],
'interface_name': d['interface_name']})
res = {'id': network_gateway['id'],
'name': network_gateway['name'],
'default': network_gateway['default'],
'devices': device_list,
'tenant_id': network_gateway['tenant_id']}
# NOTE(salvatore-orlando):perhaps return list of connected networks
return self._fields(res, fields)
def _validate_network_mapping_info(self, network_mapping_info):
network_id = network_mapping_info.get(NETWORK_ID)
if not network_id:
raise exceptions.InvalidInput(
error_message=_("A network identifier must be specified "
"when connecting a network to a network "
"gateway. Unable to complete operation"))
connection_attrs = set(network_mapping_info.keys())
if not connection_attrs.issubset(ALLOWED_CONNECTION_ATTRIBUTES):
raise exceptions.InvalidInput(
error_message=(_("Invalid keys found among the ones provided "
"in request body: %(connection_attrs)s."),
connection_attrs))
seg_type = network_mapping_info.get(SEGMENTATION_TYPE)
seg_id = network_mapping_info.get(SEGMENTATION_ID)
if not seg_type and seg_id:
msg = _("In order to specify a segmentation id the "
"segmentation type must be specified as well")
raise exceptions.InvalidInput(error_message=msg)
elif seg_type and seg_type.lower() == 'flat' and seg_id:
msg = _("Cannot specify a segmentation id when "
"the segmentation type is flat")
raise exceptions.InvalidInput(error_message=msg)
return network_id
def _retrieve_gateway_connections(self, context, gateway_id, mapping_info,
only_one=False):
filters = {'network_gateway_id': [gateway_id]}
for k, v in mapping_info.iteritems():
if v and k != NETWORK_ID:
filters[k] = [v]
query = self._get_collection_query(context,
NetworkConnection,
filters)
return only_one and query.one() or query.all()
def _unset_default_network_gateways(self, context):
with context.session.begin(subtransactions=True):
context.session.query(NetworkGateway).update(
{NetworkGateway.default: False})
def _set_default_network_gateway(self, context, gw_id):
with context.session.begin(subtransactions=True):
gw = (context.session.query(NetworkGateway).
filter_by(id=gw_id).one())
gw['default'] = True
def prevent_network_gateway_port_deletion(self, context, port):
""" Pre-deletion check.
Ensures a port will not be deleted if is being used by a network
gateway. In that case an exception will be raised.
"""
if port['device_owner'] == DEVICE_OWNER_NET_GW_INTF:
raise NetworkGatewayPortInUse(port_id=port['id'],
device_owner=port['device_owner'])
def create_network_gateway(self, context, network_gateway):
gw_data = network_gateway[self.resource]
tenant_id = self._get_tenant_id_for_create(context, gw_data)
with context.session.begin(subtransactions=True):
gw_db = NetworkGateway(
id=gw_data.get('id', uuidutils.generate_uuid()),
tenant_id=tenant_id,
name=gw_data.get('name'))
# Device list is guaranteed to be a valid list
gw_db.devices.extend([NetworkGatewayDevice(**device)
for device in gw_data['devices']])
context.session.add(gw_db)
LOG.debug(_("Created network gateway with id:%s"), gw_db['id'])
return self._make_network_gateway_dict(gw_db)
def update_network_gateway(self, context, id, network_gateway):
gw_data = network_gateway[self.resource]
with context.session.begin(subtransactions=True):
gw_db = self._get_network_gateway(context, id)
if gw_db.default:
raise NetworkGatewayUnchangeable(gateway_id=id)
# Ensure there is something to update before doing it
db_values_set = set([v for (k, v) in gw_db.iteritems()])
if not set(gw_data.values()).issubset(db_values_set):
gw_db.update(gw_data)
LOG.debug(_("Updated network gateway with id:%s"), id)
return self._make_network_gateway_dict(gw_db)
def get_network_gateway(self, context, id, fields=None):
gw_db = self._get_network_gateway(context, id)
return self._make_network_gateway_dict(gw_db, fields)
def delete_network_gateway(self, context, id):
with context.session.begin(subtransactions=True):
gw_db = self._get_network_gateway(context, id)
if gw_db.network_connections:
raise GatewayInUse(gateway_id=id)
if gw_db.default:
raise NetworkGatewayUnchangeable(gateway_id=id)
context.session.delete(gw_db)
LOG.debug(_("Network gateway '%s' was destroyed."), id)
def get_network_gateways(self, context, filters=None, fields=None):
return self._get_collection(context, NetworkGateway,
self._make_network_gateway_dict,
filters=filters, fields=fields)
def connect_network(self, context, network_gateway_id,
network_mapping_info):
network_id = self._validate_network_mapping_info(network_mapping_info)
LOG.debug(_("Connecting network '%(network_id)s' to gateway "
"'%(network_gateway_id)s'"),
{'network_id': network_id,
'network_gateway_id': network_gateway_id})
with context.session.begin(subtransactions=True):
gw_db = self._get_network_gateway(context, network_gateway_id)
tenant_id = self._get_tenant_id_for_create(context, gw_db)
# TODO(salvatore-orlando): Leverage unique constraint instead
# of performing another query!
if self._retrieve_gateway_connections(context,
network_gateway_id,
network_mapping_info):
raise GatewayConnectionInUse(mapping=network_mapping_info,
gateway_id=network_gateway_id)
# TODO(salvatore-orlando): This will give the port a fixed_ip,
# but we actually do not need any. Instead of wasting an IP we
# should have a way to say a port shall not be associated with
# any subnet
try:
# We pass the segmentation type and id too - the plugin
# might find them useful as the network connection object
# does not exist yet.
# NOTE: they're not extended attributes, rather extra data
# passed in the port structure to the plugin
# TODO(salvatore-orlando): Verify optimal solution for
# ownership of the gateway port
port = self.create_port(context, {
'port':
{'tenant_id': tenant_id,
'network_id': network_id,
'mac_address': attributes.ATTR_NOT_SPECIFIED,
'admin_state_up': True,
'fixed_ips': [],
'device_id': network_gateway_id,
'device_owner': DEVICE_OWNER_NET_GW_INTF,
'name': '',
'gw:segmentation_type':
network_mapping_info.get('segmentation_type'),
'gw:segmentation_id':
network_mapping_info.get('segmentation_id')}})
except exceptions.NetworkNotFound:
err_msg = (_("Requested network '%(network_id)s' not found."
"Unable to create network connection on "
"gateway '%(network_gateway_id)s") %
{'network_id': network_id,
'network_gateway_id': network_gateway_id})
LOG.error(err_msg)
raise exceptions.InvalidInput(error_message=err_msg)
port_id = port['id']
LOG.debug(_("Gateway port for '%(network_gateway_id)s' "
"created on network '%(network_id)s':%(port_id)s"),
{'network_gateway_id': network_gateway_id,
'network_id': network_id,
'port_id': port_id})
# Create NetworkConnection record
network_mapping_info['port_id'] = port_id
network_mapping_info['tenant_id'] = tenant_id
gw_db.network_connections.append(
NetworkConnection(**network_mapping_info))
port_id = port['id']
# now deallocate the ip from the port
for fixed_ip in port.get('fixed_ips', []):
db_base_plugin_v2.QuantumDbPluginV2._delete_ip_allocation(
context, network_id,
fixed_ip['subnet_id'],
fixed_ip['ip_address'])
LOG.debug(_("Ensured no Ip addresses are configured on port %s"),
port_id)
return {'connection_info':
{'network_gateway_id': network_gateway_id,
'network_id': network_id,
'port_id': port_id}}
def disconnect_network(self, context, network_gateway_id,
network_mapping_info):
network_id = self._validate_network_mapping_info(network_mapping_info)
LOG.debug(_("Disconnecting network '%(network_id)s' from gateway "
"'%(network_gateway_id)s'"),
{'network_id': network_id,
'network_gateway_id': network_gateway_id})
with context.session.begin(subtransactions=True):
# Uniquely identify connection, otherwise raise
try:
net_connection = self._retrieve_gateway_connections(
context, network_gateway_id,
network_mapping_info, only_one=True)
except sa_orm_exc.NoResultFound:
raise GatewayConnectionNotFound(
network_mapping_info=network_mapping_info,
network_gateway_id=network_gateway_id)
except sa_orm_exc.MultipleResultsFound:
raise MultipleGatewayConnections(
gateway_id=network_gateway_id)
# Remove gateway port from network
# FIXME(salvatore-orlando): Ensure state of port in NVP is
# consistent with outcome of transaction
self.delete_port(context, net_connection['port_id'],
nw_gw_port_check=False)
# Remove NetworkConnection record
context.session.delete(net_connection)

View File

@ -56,8 +56,9 @@ class NVPCluster(object):
def add_controller(self, ip, port, user, password, request_timeout,
http_timeout, retries, redirects, default_tz_uuid,
uuid=None, zone=None,
default_l3_gw_service_uuid=None):
uuid=None, zone=None, default_l3_gw_service_uuid=None,
default_l2_gw_service_uuid=None,
default_interface_name=None):
"""Add a new set of controller parameters.
:param ip: IP address of controller.
@ -70,13 +71,16 @@ class NVPCluster(object):
:param redirects: maximum number of server redirect responses to
follow.
:param default_tz_uuid: default transport zone uuid.
:param default_next_hop: default next hop for routers in this cluster.
:param uuid: UUID of this cluster (used in MDI configs).
:param zone: Zone of this cluster (used in MDI configs).
:param default_l3_gw_service_uuid: Default l3 gateway service
:param default_l2_gw_service_uuid: Default l2 gateway service
:param default_interface_name: Default interface name for l2 gateways
"""
keys = ['ip', 'user', 'password', 'default_tz_uuid',
'default_l3_gw_service_uuid', 'uuid', 'zone']
'default_l3_gw_service_uuid', 'default_l2_gw_service_uuid',
'default_interface_name', 'uuid', 'zone']
controller_dict = dict([(k, locals()[k]) for k in keys])
default_tz_uuid = controller_dict.get('default_tz_uuid')
if not re.match(attributes.UUID_PATTERN, default_tz_uuid):
@ -97,6 +101,17 @@ class NVPCluster(object):
"might not work properly in this cluster"),
{'l3_gw_service_uuid': l3_gw_service_uuid,
'cluster_name': self.name})
# default_l2_gw_node_uuid is an optional parameter
# validate only if specified
l2_gw_service_uuid = controller_dict.get('default_l2_gw_node_uuid')
if l2_gw_service_uuid and not re.match(attributes.UUID_PATTERN,
l2_gw_service_uuid):
LOG.warning(_("default_l2_gw_node_uuid:%(l2_gw_service_uuid)s "
"is not a valid UUID in the cluster "
"%(cluster_name)s."),
{'l2_gw_service_uuid': l2_gw_service_uuid,
'cluster_name': self.name})
int_keys = [
'port', 'request_timeout', 'http_timeout', 'retries', 'redirects']
for k in int_keys:
@ -155,6 +170,14 @@ class NVPCluster(object):
def default_l3_gw_service_uuid(self):
return self.controllers[0]['default_l3_gw_service_uuid']
@property
def default_l2_gw_service_uuid(self):
return self.controllers[0]['default_l2_gw_service_uuid']
@property
def default_interface_name(self):
return self.controllers[0]['default_interface_name']
@property
def zone(self):
return self.controllers[0]['zone']

View File

@ -46,14 +46,14 @@ DEF_TRANSPORT_TYPE = "stt"
URI_PREFIX = "/ws.v1"
# Resources exposed by NVP API
LSWITCH_RESOURCE = "lswitch"
LSWITCHPORT_RESOURCE = "lport-%s" % LSWITCH_RESOURCE
LSWITCHPORT_RESOURCE = "lport/%s" % LSWITCH_RESOURCE
LROUTER_RESOURCE = "lrouter"
LROUTERPORT_RESOURCE = "lport-%s" % LROUTER_RESOURCE
LROUTERNAT_RESOURCE = "nat-lrouter"
LQUEUE_RESOURCE = "lqueue"
# Current quantum version
LROUTERPORT_RESOURCE = "lport/%s" % LROUTER_RESOURCE
LROUTERNAT_RESOURCE = "nat/lrouter"
LQUEUE_RESOURCE = "lqueue"
GWSERVICE_RESOURCE = "gateway-service"
QUANTUM_VERSION = "2013.1"
# Constants for NAT rules
MATCH_KEYS = ["destination_ip_addresses", "destination_port_max",
"destination_port_min", "source_ip_addresses",
@ -114,8 +114,11 @@ def _build_uri_path(resource,
resource_id=None,
parent_resource_id=None,
fields=None,
relations=None, filters=None, is_attachment=False):
resources = resource.split('-')
relations=None,
filters=None,
types=None,
is_attachment=False):
resources = resource.split('/')
res_path = resources[0] + (resource_id and "/%s" % resource_id or '')
if len(resources) > 1:
# There is also a parent resource to account for in the uri
@ -127,6 +130,7 @@ def _build_uri_path(resource,
params = []
params.append(fields and "fields=%s" % fields)
params.append(relations and "relations=%s" % relations)
params.append(types and "types=%s" % types)
if filters:
params.extend(['%s=%s' % (k, v) for (k, v) in filters.iteritems()])
uri_path = "%s/%s" % (URI_PREFIX, res_path)
@ -326,6 +330,42 @@ def update_lswitch(cluster, lswitch_id, display_name,
return obj
def create_l2_gw_service(cluster, tenant_id, display_name, devices):
""" Create a NVP Layer-2 Network Gateway Service.
:param cluster: The target NVP cluster
:param tenant_id: Identifier of the Openstack tenant for which
the gateway service.
:param display_name: Descriptive name of this gateway service
:param devices: List of transport node uuids (and network
interfaces on them) to use for the network gateway service
:raise NvpApiException: if there is a problem while communicating
with the NVP controller
"""
tags = [{"tag": tenant_id, "scope": "os_tid"}]
# NOTE(salvatore-orlando): This is a little confusing, but device_id in
# NVP is actually the identifier a physical interface on the gateway
# device, which in the Quantum API is referred as interface_name
gateways = [{"transport_node_uuid": device['id'],
"device_id": device['interface_name'],
"type": "L2Gateway"} for device in devices]
gwservice_obj = {
"display_name": display_name,
"tags": tags,
"gateways": gateways,
"type": "L2GatewayServiceConfig"
}
try:
return json.loads(do_single_request(
"POST", _build_uri_path(GWSERVICE_RESOURCE),
json.dumps(gwservice_obj), cluster=cluster))
except NvpApiClient.NvpApiException:
# just log and re-raise - let the caller handle it
LOG.exception(_("An exception occured while communicating with "
"the NVP controller for cluster:%s"), cluster.name)
raise
def create_lrouter(cluster, tenant_id, display_name, nexthop):
""" Create a NVP logical router on the specified cluster.
@ -375,6 +415,19 @@ def delete_lrouter(cluster, lrouter_id):
raise
def delete_l2_gw_service(cluster, gateway_id):
try:
do_single_request("DELETE",
_build_uri_path(GWSERVICE_RESOURCE,
resource_id=gateway_id),
cluster=cluster)
except NvpApiClient.NvpApiException:
# just log and re-raise - let the caller handle it
LOG.exception(_("An exception occured while communicating with "
"the NVP controller for cluster:%s"), cluster.name)
raise
def get_lrouter(cluster, lrouter_id):
try:
return json.loads(do_single_request(HTTP_GET,
@ -389,6 +442,19 @@ def get_lrouter(cluster, lrouter_id):
raise
def get_l2_gw_service(cluster, gateway_id):
try:
return json.loads(do_single_request("GET",
_build_uri_path(GWSERVICE_RESOURCE,
resource_id=gateway_id),
cluster=cluster))
except NvpApiClient.NvpApiException:
# just log and re-raise - let the caller handle it
LOG.exception(_("An exception occured while communicating with "
"the NVP controller for cluster:%s"), cluster.name)
raise
def get_lrouters(cluster, tenant_id, fields=None, filters=None):
actual_filters = {}
if filters:
@ -405,6 +471,38 @@ def get_lrouters(cluster, tenant_id, fields=None, filters=None):
cluster)
def get_l2_gw_services(cluster, tenant_id=None,
fields=None, filters=None):
actual_filters = dict(filters or {})
if tenant_id:
actual_filters['tag'] = tenant_id
actual_filters['tag_scope'] = 'os_tid'
return get_all_query_pages(
_build_uri_path(GWSERVICE_RESOURCE,
filters=actual_filters),
cluster)
def update_l2_gw_service(cluster, gateway_id, display_name):
# TODO(salvatore-orlando): Allow updates for gateways too
gwservice_obj = get_l2_gw_service(cluster, gateway_id)
if not display_name:
# Nothing to update
return gwservice_obj
gwservice_obj["display_name"] = display_name
try:
return json.loads(do_single_request("PUT",
_build_uri_path(GWSERVICE_RESOURCE,
resource_id=gateway_id),
json.dumps(gwservice_obj),
cluster=cluster))
except NvpApiClient.NvpApiException:
# just log and re-raise - let the caller handle it
LOG.exception(_("An exception occured while communicating with "
"the NVP controller for cluster:%s"), cluster.name)
raise
def update_lrouter(cluster, lrouter_id, display_name, nexthop):
lrouter_obj = get_lrouter(cluster, lrouter_id)
if not display_name and not nexthop:
@ -829,31 +927,42 @@ def get_port_status(cluster, lswitch_id, port_id):
return constants.PORT_STATUS_DOWN
def _plug_interface(cluster, lswitch_id, lport_id, att_obj):
uri = _build_uri_path(LSWITCHPORT_RESOURCE, lport_id, lswitch_id,
is_attachment=True)
try:
resp_obj = do_single_request(HTTP_PUT, uri, json.dumps(att_obj),
cluster=cluster)
except NvpApiClient.NvpApiException:
LOG.exception(_("Exception while plugging an attachment:%(att)s "
"into NVP port:%(port)s for NVP logical switch "
"%(net)s"), {'net': lswitch_id,
'port': lport_id,
'att': att_obj})
raise
result = json.dumps(resp_obj)
return result
def plug_l2_gw_service(cluster, lswitch_id, lport_id,
gateway_id, vlan_id=None):
""" Plug a Layer-2 Gateway Attachment object in a logical port """
att_obj = {'type': 'L2GatewayAttachment',
'l2_gateway_service_uuid': gateway_id}
if vlan_id:
att_obj['vlan_id'] = vlan_id
return _plug_interface(cluster, lswitch_id, lport_id, att_obj)
def plug_interface(cluster, lswitch_id, port, type, attachment=None):
uri = "/ws.v1/lswitch/" + lswitch_id + "/lport/" + port + "/attachment"
""" Plug a VIF Attachment object in a logical port """
lport_obj = {}
if attachment:
lport_obj["vif_uuid"] = attachment
lport_obj["type"] = type
try:
resp_obj = do_single_request(HTTP_PUT, uri, json.dumps(lport_obj),
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)
except NvpApiClient.Conflict as e:
LOG.error(_("Conflict while making attachment to port, "
"Error: %s"), str(e))
raise exception.AlreadyAttached(att_id=attachment,
port_id=port,
net_id=lswitch_id,
att_port_id="UNKNOWN")
except NvpApiClient.NvpApiException as e:
raise exception.QuantumException()
result = json.dumps(resp_obj)
return result
return _plug_interface(cluster, lswitch_id, port, lport_obj)
#------------------------------------------------------------------------------
# Security Profile convenience functions.

View File

@ -0,0 +1,15 @@
{
"display_name": "%(display_name)s",
"_href": "/ws.v1/gateway-service/%(uuid)s",
"tags": %(tags_json)s,
"_schema": "/ws.v1/schema/L2GatewayServiceConfig",
"gateways": [
{
"transport_node_uuid": "%(transport_node_uuid)s",
"type": "L2Gateway",
"device_id": "%(device_id)s"
}
],
"type": "L2GatewayServiceConfig",
"uuid": "%(uuid)s"
}

View File

@ -1,10 +1,7 @@
{
"LogicalPortAttachment":
{
%(peer_port_href_field)s
%(peer_port_uuid_field)s
%(vif_uuid_field)s
"type": "%(type)s",
"schema": "/ws.v1/schema/%(type)s"
"type": "%(att_type)s",
"schema": "/ws.v1/schema/%(att_type)s"
}
}

View File

@ -0,0 +1,13 @@
{
"display_name": "%(display_name)s",
"tags": [{"scope": "os_tid", "tag": "%(tenant_id)s"}],
"gateways": [
{
"transport_node_uuid": "%(transport_node_uuid)s",
"device_id": "%(device_id)s",
"type": "L2Gateway"
}
],
"type": "L2GatewayServiceConfig",
"uuid": "%(uuid)s"
}

View File

@ -5,4 +5,5 @@ default_tz_uuid = fake_tz_uuid
nova_zone_id = whatever
nvp_cluster_uuid = fake_cluster_uuid
nvp_controller_connection=fake:443:admin:admin:30:10:2:2
default_l3_gw_uuid = whatever
default_l3_gw_service_uuid = whatever
default_l2_gw_service_uuid = whatever

View File

@ -42,9 +42,11 @@ class FakeClient:
LSWITCH_LPORT_ATT = 'lswitch_lportattachment'
LROUTER_LPORT_STATUS = 'lrouter_lportstatus'
LROUTER_LPORT_ATT = 'lrouter_lportattachment'
GWSERVICE_RESOURCE = 'gatewayservice'
RESOURCES = [LSWITCH_RESOURCE, LROUTER_RESOURCE, LQUEUE_RESOURCE,
LPORT_RESOURCE, NAT_RESOURCE, SECPROF_RESOURCE]
LPORT_RESOURCE, NAT_RESOURCE, SECPROF_RESOURCE,
GWSERVICE_RESOURCE]
FAKE_GET_RESPONSES = {
LSWITCH_RESOURCE: "fake_get_lswitch.json",
@ -56,7 +58,8 @@ class FakeClient:
LROUTER_LPORT_STATUS: "fake_get_lrouter_lport_status.json",
LROUTER_LPORT_ATT: "fake_get_lrouter_lport_att.json",
LROUTER_STATUS: "fake_get_lrouter_status.json",
LROUTER_NAT_RESOURCE: "fake_get_lrouter_nat.json"
LROUTER_NAT_RESOURCE: "fake_get_lrouter_nat.json",
GWSERVICE_RESOURCE: "fake_get_gwservice.json"
}
FAKE_POST_RESPONSES = {
@ -66,7 +69,8 @@ class FakeClient:
LROUTER_LPORT_RESOURCE: "fake_post_lrouter_lport.json",
LROUTER_NAT_RESOURCE: "fake_post_lrouter_nat.json",
SECPROF_RESOURCE: "fake_post_security_profile.json",
LQUEUE_RESOURCE: "fake_post_lqueue.json"
LQUEUE_RESOURCE: "fake_post_lqueue.json",
GWSERVICE_RESOURCE: "fake_post_gwservice.json"
}
FAKE_PUT_RESPONSES = {
@ -78,7 +82,8 @@ class FakeClient:
LSWITCH_LPORT_ATT: "fake_put_lswitch_lport_att.json",
LROUTER_LPORT_ATT: "fake_put_lrouter_lport_att.json",
SECPROF_RESOURCE: "fake_post_security_profile.json",
LQUEUE_RESOURCE: "fake_post_lqueue.json"
LQUEUE_RESOURCE: "fake_post_lqueue.json",
GWSERVICE_RESOURCE: "fake_post_gwservice.json"
}
MANAGED_RELATIONS = {
@ -97,6 +102,7 @@ class FakeClient:
_fake_lrouter_lportstatus_dict = {}
_fake_securityprofile_dict = {}
_fake_lqueue_dict = {}
_fake_gatewayservice_dict = {}
def __init__(self, fake_files_path):
self.fake_files_path = fake_files_path
@ -219,6 +225,20 @@ class FakeClient:
fake_nat['match_json'] = match_json
return fake_nat
def _add_gatewayservice(self, body):
fake_gwservice = json.loads(body)
fake_gwservice['uuid'] = str(uuidutils.generate_uuid())
fake_gwservice['tenant_id'] = self._get_tag(
fake_gwservice, 'os_tid')
# FIXME(salvatore-orlando): For simplicity we're managing only a
# single device. Extend the fake client for supporting multiple devices
first_gw = fake_gwservice['gateways'][0]
fake_gwservice['transport_node_uuid'] = first_gw['transport_node_uuid']
fake_gwservice['device_id'] = first_gw['device_id']
self._fake_gatewayservice_dict[fake_gwservice['uuid']] = (
fake_gwservice)
return fake_gwservice
def _build_relation(self, src, dst, resource_type, relation):
if not relation in self.MANAGED_RELATIONS[resource_type]:
return # Relation is not desired in output
@ -357,20 +377,20 @@ class FakeClient:
if (parent_func(res_uuid) and
_tag_match(res_uuid) and
_attr_match(res_uuid))]
return json.dumps({'results': items,
'result_count': len(items)})
def _show(self, resource_type, response_file,
uuid1, uuid2=None, relations=None):
target_uuid = uuid2 or uuid1
if resource_type.endswith('attachment'):
resource_type = resource_type[:resource_type.index('attachment')]
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:
@ -392,8 +412,11 @@ class FakeClient:
else:
return self._list(res_type, response_file, uuids[0],
query=parsedurl.query, relations=relations)
elif ('lswitch' in res_type or 'lrouter' in res_type
or self.SECPROF_RESOURCE in res_type):
elif ('lswitch' in res_type or
'lrouter' in res_type or
self.SECPROF_RESOURCE in res_type or
'gatewayservice' in res_type):
LOG.debug("UUIDS:%s", uuids)
if len(uuids) > 0:
return self._show(res_type, response_file, uuids[0],
relations=relations)
@ -443,6 +466,7 @@ class FakeClient:
relations['LogicalPortAttachment'] = json.loads(body)
resource['_relations'] = relations
body_2 = json.loads(body)
resource['att_type'] = body_2['type']
if body_2['type'] == "PatchAttachment":
# We need to do a trick here
if self.LROUTER_RESOURCE in res_type:
@ -462,6 +486,10 @@ class FakeClient:
elif body_2['type'] == "L3GatewayAttachment":
resource['attachment_gwsvc_uuid'] = (
body_2['l3_gateway_service_uuid'])
elif body_2['type'] == "L2GatewayAttachment":
resource['attachment_gwsvc_uuid'] = (
body_2['l2_gateway_service_uuid'])
if not is_attachment:
response = response_template % resource
else:
@ -502,3 +530,5 @@ class FakeClient:
self._fake_lswitch_lportstatus_dict.clear()
self._fake_lrouter_lportstatus_dict.clear()
self._fake_lqueue_dict.clear()
self._fake_securityprofile_dict.clear()
self._fake_gatewayservice_dict.clear()

View File

@ -0,0 +1,526 @@
#
# 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.
import contextlib
import mock
import unittest2 as unittest
import webtest
from webob import exc
from quantum.api import extensions
from quantum.api.extensions import PluginAwareExtensionManager
from quantum.common import config
from quantum.common.test_lib import test_config
from quantum import context
from quantum.db import api as db_api
from quantum.db import db_base_plugin_v2
from quantum import manager
from quantum.openstack.common import cfg
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_networkgw
as networkgw)
from quantum.plugins.nicira.nicira_nvp_plugin import nicira_networkgw_db
from quantum.tests.unit import test_api_v2
from quantum.tests.unit import test_db_plugin
from quantum.tests.unit import test_extensions
_uuid = test_api_v2._uuid
_get_path = test_api_v2._get_path
class TestExtensionManager(object):
def get_resources(self):
return networkgw.Nvp_networkgw.get_resources()
def get_actions(self):
return []
def get_request_extensions(self):
return []
class NetworkGatewayExtensionTestCase(unittest.TestCase):
def setUp(self):
plugin = '%s.%s' % (networkgw.__name__,
networkgw.NetworkGatewayPluginBase.__name__)
self._resource = networkgw.RESOURCE_NAME.replace('-', '_')
# Ensure 'stale' patched copies of the plugin are never returned
manager.QuantumManager._instance = None
# Ensure existing ExtensionManager is not used
extensions.PluginAwareExtensionManager._instance = None
# Create the default configurations
args = ['--config-file', test_api_v2.etcdir('quantum.conf.test')]
config.parse(args=args)
# Update the plugin and extensions path
cfg.CONF.set_override('core_plugin', plugin)
self._plugin_patcher = mock.patch(plugin, autospec=True)
self.plugin = self._plugin_patcher.start()
# Instantiate mock plugin and enable extensions
manager.QuantumManager.get_plugin().supported_extension_aliases = (
[networkgw.EXT_ALIAS])
ext_mgr = TestExtensionManager()
PluginAwareExtensionManager._instance = ext_mgr
self.ext_mdw = test_extensions.setup_extensions_middleware(ext_mgr)
self.api = webtest.TestApp(self.ext_mdw)
def tearDown(self):
self._plugin_patcher.stop()
self.api = None
self.plugin = None
cfg.CONF.reset()
def test_network_gateway_create(self):
nw_gw_id = _uuid()
data = {self._resource: {'name': 'nw-gw',
'tenant_id': _uuid(),
'devices': [{'id': _uuid(),
'interface_name': 'xxx'}]}}
return_value = data[self._resource].copy()
return_value.update({'id': nw_gw_id})
instance = self.plugin.return_value
instance.create_network_gateway.return_value = return_value
res = self.api.post_json(_get_path(networkgw.COLLECTION_NAME), data)
instance.create_network_gateway.assert_called_with(
mock.ANY, network_gateway=data)
self.assertEqual(res.status_int, exc.HTTPCreated.code)
self.assertTrue(self._resource in res.json)
nw_gw = res.json[self._resource]
self.assertEqual(nw_gw['id'], nw_gw_id)
def test_network_gateway_update(self):
nw_gw_name = 'updated'
data = {self._resource: {'name': nw_gw_name}}
nw_gw_id = _uuid()
return_value = {'id': nw_gw_id,
'name': nw_gw_name}
instance = self.plugin.return_value
instance.update_network_gateway.return_value = return_value
res = self.api.put_json(_get_path('%s/%s' % (networkgw.COLLECTION_NAME,
nw_gw_id)),
data)
instance.update_network_gateway.assert_called_with(
mock.ANY, nw_gw_id, network_gateway=data)
self.assertEqual(res.status_int, exc.HTTPOk.code)
self.assertTrue(self._resource in res.json)
nw_gw = res.json[self._resource]
self.assertEqual(nw_gw['id'], nw_gw_id)
self.assertEqual(nw_gw['name'], nw_gw_name)
def test_network_gateway_delete(self):
nw_gw_id = _uuid()
instance = self.plugin.return_value
res = self.api.delete(_get_path('%s/%s' % (networkgw.COLLECTION_NAME,
nw_gw_id)))
instance.delete_network_gateway.assert_called_with(mock.ANY,
nw_gw_id)
self.assertEqual(res.status_int, exc.HTTPNoContent.code)
def test_network_gateway_get(self):
nw_gw_id = _uuid()
return_value = {self._resource: {'name': 'test',
'devices':
[{'id': _uuid(),
'interface_name': 'xxx'}],
'id': nw_gw_id}}
instance = self.plugin.return_value
instance.get_network_gateway.return_value = return_value
res = self.api.get(_get_path('%s/%s' % (networkgw.COLLECTION_NAME,
nw_gw_id)))
instance.get_network_gateway.assert_called_with(mock.ANY,
nw_gw_id,
fields=mock.ANY)
self.assertEqual(res.status_int, exc.HTTPOk.code)
def test_network_gateway_list(self):
nw_gw_id = _uuid()
return_value = [{self._resource: {'name': 'test',
'devices':
[{'id': _uuid(),
'interface_name': 'xxx'}],
'id': nw_gw_id}}]
instance = self.plugin.return_value
instance.get_network_gateways.return_value = return_value
res = self.api.get(_get_path(networkgw.COLLECTION_NAME))
instance.get_network_gateways.assert_called_with(mock.ANY,
fields=mock.ANY,
filters=mock.ANY)
self.assertEqual(res.status_int, exc.HTTPOk.code)
def test_network_gateway_connect(self):
nw_gw_id = _uuid()
nw_id = _uuid()
gw_port_id = _uuid()
mapping_data = {'network_id': nw_id,
'segmentation_type': 'vlan',
'segmentation_id': '999'}
return_value = {'connection_info': {
'network_gateway_id': nw_gw_id,
'port_id': gw_port_id,
'network_id': nw_id}}
instance = self.plugin.return_value
instance.connect_network.return_value = return_value
res = self.api.put_json(_get_path('%s/%s/connect_network' %
(networkgw.COLLECTION_NAME,
nw_gw_id)),
mapping_data)
instance.connect_network.assert_called_with(mock.ANY,
nw_gw_id,
mapping_data)
self.assertEqual(res.status_int, exc.HTTPOk.code)
nw_conn_res = res.json['connection_info']
self.assertEqual(nw_conn_res['port_id'], gw_port_id)
self.assertEqual(nw_conn_res['network_id'], nw_id)
def test_network_gateway_disconnect(self):
nw_gw_id = _uuid()
nw_id = _uuid()
mapping_data = {'network_id': nw_id}
instance = self.plugin.return_value
res = self.api.put_json(_get_path('%s/%s/disconnect_network' %
(networkgw.COLLECTION_NAME,
nw_gw_id)),
mapping_data)
instance.disconnect_network.assert_called_with(mock.ANY,
nw_gw_id,
mapping_data)
self.assertEqual(res.status_int, exc.HTTPOk.code)
class NetworkGatewayDbTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
""" Unit tests for Network Gateway DB support """
def setUp(self):
test_config['plugin_name_v2'] = '%s.%s' % (
__name__, TestNetworkGatewayPlugin.__name__)
ext_mgr = TestExtensionManager()
test_config['extension_manager'] = ext_mgr
self.resource = networkgw.RESOURCE_NAME.replace('-', '_')
super(NetworkGatewayDbTestCase, self).setUp()
def _create_network_gateway(self, fmt, tenant_id, name=None,
devices=None, arg_list=None, **kwargs):
data = {self.resource: {'tenant_id': tenant_id,
'devices': devices}}
if name:
data[self.resource]['name'] = name
for arg in arg_list or ():
# Arg must be present and not empty
if arg in kwargs and kwargs[arg]:
data[self.resource][arg] = kwargs[arg]
nw_gw_req = self.new_create_request(networkgw.COLLECTION_NAME,
data, fmt)
if (kwargs.get('set_context') and tenant_id):
# create a specific auth context for this request
nw_gw_req.environ['quantum.context'] = context.Context(
'', tenant_id)
return nw_gw_req.get_response(self.ext_api)
@contextlib.contextmanager
def _network_gateway(self, name='gw1', devices=None,
fmt='json', tenant_id=_uuid()):
if not devices:
devices = [{'id': _uuid(), 'interface_name': 'xyz'}]
res = self._create_network_gateway(fmt, tenant_id, name=name,
devices=devices)
network_gateway = self.deserialize(fmt, res)
if res.status_int >= 400:
raise exc.HTTPClientError(code=res.status_int)
yield network_gateway
self._delete(networkgw.COLLECTION_NAME,
network_gateway[self.resource]['id'])
def _gateway_action(self, action, network_gateway_id, network_id,
segmentation_type, segmentation_id=None,
expected_status=exc.HTTPOk.code):
connection_data = {'network_id': network_id,
'segmentation_type': segmentation_type}
if segmentation_id:
connection_data['segmentation_id'] = segmentation_id
req = self.new_action_request(networkgw.COLLECTION_NAME,
connection_data,
network_gateway_id,
"%s_network" % action)
res = req.get_response(self.ext_api)
self.assertEqual(res.status_int, expected_status)
return self.deserialize('json', res)
def _test_connect_and_disconnect_network(self, segmentation_type,
segmentation_id=None):
with self._network_gateway() as gw:
with self.network() as net:
body = self._gateway_action('connect',
gw[self.resource]['id'],
net['network']['id'],
segmentation_type,
segmentation_id)
self.assertTrue('connection_info' in body)
connection_info = body['connection_info']
for attr in ('network_id', 'port_id',
'network_gateway_id'):
self.assertTrue(attr in connection_info)
# fetch port and confirm device_id
gw_port_id = connection_info['port_id']
port_body = self._show('ports', gw_port_id)
self.assertEquals(port_body['port']['device_id'],
gw[self.resource]['id'])
# Clean up - otherwise delete will fail
body = self._gateway_action('disconnect',
gw[self.resource]['id'],
net['network']['id'],
segmentation_type,
segmentation_id)
# Check associated port has been deleted too
body = self._show('ports', gw_port_id,
expected_code=exc.HTTPNotFound.code)
def test_create_network_gateway(self):
name = 'test-gw'
devices = [{'id': _uuid(), 'interface_name': 'xxx'},
{'id': _uuid(), 'interface_name': 'yyy'}]
keys = [('devices', devices), ('name', name)]
with self._network_gateway(name=name, devices=devices) as gw:
for k, v in keys:
self.assertEquals(gw[self.resource][k], v)
def _test_delete_network_gateway(self, exp_gw_count=0):
name = 'test-gw'
devices = [{'id': _uuid(), 'interface_name': 'xxx'},
{'id': _uuid(), 'interface_name': 'yyy'}]
with self._network_gateway(name=name, devices=devices):
# Nothing to do here - just let the gateway go
pass
# Verify nothing left on db
session = db_api.get_session()
gw_query = session.query(nicira_networkgw_db.NetworkGateway)
dev_query = session.query(nicira_networkgw_db.NetworkGatewayDevice)
self.assertEqual(exp_gw_count, len(gw_query.all()))
self.assertEqual(0, len(dev_query.all()))
def test_delete_network_gateway(self):
self._test_delete_network_gateway()
def test_update_network_gateway(self):
with self._network_gateway() as gw:
data = {self.resource: {'name': 'new_name'}}
req = self.new_update_request(networkgw.COLLECTION_NAME,
data,
gw[self.resource]['id'])
res = self.deserialize('json', req.get_response(self.ext_api))
self.assertEqual(res[self.resource]['name'],
data[self.resource]['name'])
def test_get_network_gateway(self):
with self._network_gateway(name='test-gw') as gw:
req = self.new_show_request(networkgw.COLLECTION_NAME,
gw[self.resource]['id'])
res = self.deserialize('json', req.get_response(self.ext_api))
self.assertEquals(res[self.resource]['name'],
gw[self.resource]['name'])
def test_list_network_gateways(self):
with self._network_gateway(name='test-gw-1') as gw1:
with self._network_gateway(name='test_gw_2') as gw2:
req = self.new_list_request(networkgw.COLLECTION_NAME)
res = self.deserialize('json', req.get_response(self.ext_api))
key = self.resource + 's'
self.assertEquals(len(res[key]), 2)
self.assertEquals(res[key][0]['name'],
gw1[self.resource]['name'])
self.assertEquals(res[key][1]['name'],
gw2[self.resource]['name'])
def test_connect_and_disconnect_network(self):
self._test_connect_and_disconnect_network('flat')
def test_connect_and_disconnect_network_with_segmentation_id(self):
self._test_connect_and_disconnect_network('vlan', 999)
def test_connect_network_multiple_times(self):
with self._network_gateway() as gw:
with self.network() as net_1:
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 777)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 777)
def test_connect_network_multiple_gateways(self):
with self._network_gateway() as gw_1:
with self._network_gateway() as gw_2:
with self.network() as net_1:
self._gateway_action('connect',
gw_1[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('connect',
gw_2[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('disconnect',
gw_1[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('disconnect',
gw_2[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
def test_connect_network_mapping_in_use_returns_409(self):
with self._network_gateway() as gw:
with self.network() as net_1:
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
with self.network() as net_2:
self._gateway_action('connect',
gw[self.resource]['id'],
net_2['network']['id'],
'vlan', 555,
expected_status=exc.HTTPConflict.code)
# Clean up - otherwise delete will fail
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
def test_connect_invalid_network_returns_400(self):
with self._network_gateway() as gw:
self._gateway_action('connect',
gw[self.resource]['id'],
'hohoho',
'vlan', 555,
expected_status=exc.HTTPBadRequest.code)
def test_connect_unspecified_network_returns_400(self):
with self._network_gateway() as gw:
self._gateway_action('connect',
gw[self.resource]['id'],
None,
'vlan', 555,
expected_status=exc.HTTPBadRequest.code)
def test_disconnect_network_ambiguous_returns_409(self):
with self._network_gateway() as gw:
with self.network() as net_1:
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 777)
# This should raise
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan',
expected_status=exc.HTTPConflict.code)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 777)
def test_delete_active_gateway_port_returns_409(self):
with self._network_gateway() as gw:
with self.network() as net_1:
body = self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
# fetch port id and try to delete it
gw_port_id = body['connection_info']['port_id']
self._delete('ports', gw_port_id,
expected_code=exc.HTTPConflict.code)
body = self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
def test_delete_network_gateway_active_connections_returns_409(self):
with self._network_gateway() as gw:
with self.network() as net_1:
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'flat')
self._delete(networkgw.COLLECTION_NAME,
gw[self.resource]['id'],
expected_code=exc.HTTPConflict.code)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'flat')
def test_disconnect_non_existing_connection_returns_404(self):
with self._network_gateway() as gw:
with self.network() as net_1:
self._gateway_action('connect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 999,
expected_status=exc.HTTPNotFound.code)
self._gateway_action('disconnect',
gw[self.resource]['id'],
net_1['network']['id'],
'vlan', 555)
class TestNetworkGatewayPlugin(db_base_plugin_v2.QuantumDbPluginV2,
nicira_networkgw_db.NetworkGatewayMixin):
""" Simple plugin class for testing db support for network gateway ext """
supported_extension_aliases = ["network-gateway"]
def delete_port(self, context, id, nw_gw_port_check=True):
if nw_gw_port_check:
port = self._get_port(context, id)
self.prevent_network_gateway_port_deletion(context, port)
super(TestNetworkGatewayPlugin, self).delete_port(context, id)

View File

@ -28,10 +28,13 @@ from quantum import context
from quantum.extensions import providernet as pnet
from quantum.extensions import securitygroup as secgrp
from quantum import manager
import quantum.plugins.nicira.nicira_nvp_plugin as nvp_plugin
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import nvp_networkgw
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_qos
as ext_qos)
from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
from quantum.tests.unit.nicira import fake_nvpapiclient
import quantum.tests.unit.nicira.test_networkgw as test_l2_gw
from quantum.tests.unit import test_extensions
import quantum.tests.unit.test_db_plugin as test_plugin
import quantum.tests.unit.test_extension_portsecurity as psec
@ -39,7 +42,7 @@ import quantum.tests.unit.test_extension_security_group as ext_sg
import quantum.tests.unit.test_l3_plugin as test_l3_plugin
LOG = logging.getLogger(__name__)
NICIRA_PKG_PATH = 'quantum.plugins.nicira.nicira_nvp_plugin'
NICIRA_PKG_PATH = nvp_plugin.__name__
NICIRA_EXT_PATH = "../../plugins/nicira/nicira_nvp_plugin/extensions"
@ -705,3 +708,32 @@ class NiciraQuantumNVPOutOfSync(test_l3_plugin.L3NatTestCaseBase,
router = self.deserialize('json', req.get_response(self.ext_api))
self.assertEquals(router['router']['status'],
constants.NET_STATUS_ERROR)
class TestNiciraNetworkGateway(test_l2_gw.NetworkGatewayDbTestCase,
NiciraPluginV2TestCase):
def setUp(self):
ext_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
NICIRA_EXT_PATH)
cfg.CONF.set_override('api_extensions_path', ext_path)
super(TestNiciraNetworkGateway, self).setUp()
def test_list_network_gateways(self):
with self._network_gateway(name='test-gw-1') as gw1:
with self._network_gateway(name='test_gw_2') as gw2:
req = self.new_list_request(nvp_networkgw.COLLECTION_NAME)
res = self.deserialize('json', req.get_response(self.ext_api))
# We expect the default gateway too
key = self.resource + 's'
self.assertEquals(len(res[key]), 3)
self.assertEquals(res[key][0]['default'],
True)
self.assertEquals(res[key][1]['name'],
gw1[self.resource]['name'])
self.assertEquals(res[key][2]['name'],
gw2[self.resource]['name'])
def test_delete_network_gateway(self):
# The default gateway must still be there
self._test_delete_network_gateway(1)

View File

@ -15,13 +15,12 @@
#
# @author: Salvatore Orlando, VMware
import json
import os
import mock
import unittest2 as unittest
from quantum.openstack.common import log as logging
from quantum.openstack.common import jsonutils as json
from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient
from quantum.plugins.nicira.nicira_nvp_plugin import nvp_cluster
from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
@ -29,12 +28,11 @@ import quantum.plugins.nicira.nicira_nvp_plugin as nvp_plugin
from quantum.tests.unit.nicira import fake_nvpapiclient
from quantum.tests.unit import test_api_v2
LOG = logging.getLogger(__name__)
NICIRA_PKG_PATH = nvp_plugin.__name__
_uuid = test_api_v2._uuid
class TestNvplibNatRules(unittest.TestCase):
class NvplibTestCase(unittest.TestCase):
def setUp(self):
# mock nvp api client
@ -43,6 +41,7 @@ class TestNvplibNatRules(unittest.TestCase):
self.mock_nvpapi = mock.patch('%s.NvpApiClient.NVPApiHelper'
% NICIRA_PKG_PATH, autospec=True)
instance = self.mock_nvpapi.start()
instance.return_value.login.return_value = "the_cookie"
def _fake_request(*args, **kwargs):
return self.fc.fake_request(*args, **kwargs)
@ -57,12 +56,15 @@ class TestNvplibNatRules(unittest.TestCase):
self.fake_cluster.request_timeout, self.fake_cluster.http_timeout,
self.fake_cluster.retries, self.fake_cluster.redirects)
super(TestNvplibNatRules, self).setUp()
super(NvplibTestCase, self).setUp()
def tearDown(self):
self.fc.reset_all()
self.mock_nvpapi.stop()
class TestNvplibNatRules(NvplibTestCase):
def _test_create_lrouter_dnat_rule(self, func):
tenant_id = 'pippo'
lrouter = nvplib.create_lrouter(self.fake_cluster,
@ -81,8 +83,8 @@ class TestNvplibNatRules(unittest.TestCase):
def test_create_lrouter_dnat_rule_v2(self):
resp_obj = self._test_create_lrouter_dnat_rule(
nvplib.create_lrouter_dnat_rule_v2)
self.assertEquals('DestinationNatRule', resp_obj['type'])
self.assertEquals('192.168.0.5',
self.assertEqual('DestinationNatRule', resp_obj['type'])
self.assertEqual('192.168.0.5',
resp_obj['match']['destination_ip_addresses'])
def test_create_lrouter_dnat_rule_v3(self):
@ -90,6 +92,91 @@ class TestNvplibNatRules(unittest.TestCase):
nvplib.create_lrouter_dnat_rule_v2)
# TODO(salvatore-orlando): Extend FakeNVPApiClient to deal with
# different versions of NVP API
self.assertEquals('DestinationNatRule', resp_obj['type'])
self.assertEquals('192.168.0.5',
self.assertEqual('DestinationNatRule', resp_obj['type'])
self.assertEqual('192.168.0.5',
resp_obj['match']['destination_ip_addresses'])
class NvplibL2GatewayTestCase(NvplibTestCase):
def _create_gw_service(self, node_uuid, display_name):
return nvplib.create_l2_gw_service(self.fake_cluster,
'fake-tenant',
display_name,
[{'id': node_uuid,
'interface_name': 'xxx'}])
def test_create_l2_gw_service(self):
display_name = 'fake-gateway'
node_uuid = _uuid()
response = self._create_gw_service(node_uuid, display_name)
self.assertEqual(response.get('type'), 'L2GatewayServiceConfig')
self.assertEqual(response.get('display_name'), display_name)
gateways = response.get('gateways', [])
self.assertEqual(len(gateways), 1)
self.assertEqual(gateways[0]['type'], 'L2Gateway')
self.assertEqual(gateways[0]['device_id'], 'xxx')
self.assertEqual(gateways[0]['transport_node_uuid'], node_uuid)
def test_update_l2_gw_service(self):
display_name = 'fake-gateway'
new_display_name = 'still-fake-gateway'
node_uuid = _uuid()
res1 = self._create_gw_service(node_uuid, display_name)
gw_id = res1['uuid']
res2 = nvplib.update_l2_gw_service(self.fake_cluster, gw_id,
new_display_name)
self.assertEqual(res2['display_name'], new_display_name)
def test_get_l2_gw_service(self):
display_name = 'fake-gateway'
node_uuid = _uuid()
gw_id = self._create_gw_service(node_uuid, display_name)['uuid']
response = nvplib.get_l2_gw_service(self.fake_cluster, gw_id)
self.assertEqual(response.get('type'), 'L2GatewayServiceConfig')
self.assertEqual(response.get('display_name'), display_name)
self.assertEqual(response.get('uuid'), gw_id)
def test_list_l2_gw_service(self):
gw_ids = []
for name in ('fake-1', 'fake-2'):
gw_ids.append(self._create_gw_service(_uuid(), name)['uuid'])
results = nvplib.get_l2_gw_services(self.fake_cluster)
self.assertEqual(len(results), 2)
self.assertItemsEqual(gw_ids, [r['uuid'] for r in results])
def test_delete_l2_gw_service(self):
display_name = 'fake-gateway'
node_uuid = _uuid()
gw_id = self._create_gw_service(node_uuid, display_name)['uuid']
nvplib.delete_l2_gw_service(self.fake_cluster, gw_id)
results = nvplib.get_l2_gw_services(self.fake_cluster)
self.assertEqual(len(results), 0)
def test_plug_l2_gw_port_attachment(self):
tenant_id = 'pippo'
node_uuid = _uuid()
lswitch = nvplib.create_lswitch(self.fake_cluster, tenant_id,
'fake-switch')
gw_id = self._create_gw_service(node_uuid, 'fake-gw')['uuid']
lport = nvplib.create_lport(self.fake_cluster,
lswitch['uuid'],
tenant_id,
_uuid(),
'fake-gw-port',
gw_id,
True)
json.loads(nvplib.plug_l2_gw_service(self.fake_cluster,
lswitch['uuid'],
lport['uuid'],
gw_id))
uri = nvplib._build_uri_path(nvplib.LSWITCHPORT_RESOURCE,
lport['uuid'],
lswitch['uuid'],
is_attachment=True)
resp_obj = json.loads(
nvplib.do_single_request("GET", uri,
cluster=self.fake_cluster))
self.assertIn('LogicalPortAttachment', resp_obj)
self.assertEqual(resp_obj['LogicalPortAttachment']['type'],
'L2GatewayAttachment')