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:
parent
68a129632e
commit
3e8b32263d
@ -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
|
||||
|
@ -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')
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 "
|
||||
|
@ -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
|
@ -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
|
||||
|
356
quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py
Normal file
356
quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py
Normal 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)
|
@ -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']
|
||||
|
@ -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.
|
||||
|
15
quantum/tests/unit/nicira/etc/fake_get_gwservice.json
Normal file
15
quantum/tests/unit/nicira/etc/fake_get_gwservice.json
Normal 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"
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
13
quantum/tests/unit/nicira/etc/fake_post_gwservice.json
Normal file
13
quantum/tests/unit/nicira/etc/fake_post_gwservice.json
Normal 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"
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
526
quantum/tests/unit/nicira/test_networkgw.py
Normal file
526
quantum/tests/unit/nicira/test_networkgw.py
Normal 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)
|
@ -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)
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user