# 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
# 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
--- /dev/null
+# 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')
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
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
'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.
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
"""
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)
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,
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)
-
- 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]
+ 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)
+
+ 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
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
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,
- l3_db.DEVICE_OWNER_ROUTER_INTF)):
- nvplib.plug_interface(cluster, lswitch_uuid,
+ if (not port_data['device_owner'] in
+ (l3_db.DEVICE_OWNER_ROUTER_GW,
+ l3_db.DEVICE_OWNER_ROUTER_INTF)):
+ 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
# 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)
{'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
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'])
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
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
"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 "
--- /dev/null
+# 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
# 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__)
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
--- /dev/null
+# 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)
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.
: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):
"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:
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']
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",
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
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)
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.
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,
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:
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:
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.
--- /dev/null
+{
+ "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"
+}
{
"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"
}
}
\ No newline at end of file
--- /dev/null
+{
+ "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"
+}
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
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",
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 = {
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 = {
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 = {
_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
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
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:
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)
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:
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:
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()
--- /dev/null
+#
+# 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)
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
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"
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)
#
# @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
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
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)
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,
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',
- resp_obj['match']['destination_ip_addresses'])
+ 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):
resp_obj = self._test_create_lrouter_dnat_rule(
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',
- resp_obj['match']['destination_ip_addresses'])
+ 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')