From 3e8b32263dfa5cf076674339986729a650259fe4 Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Sun, 25 Nov 2012 17:11:42 -0800 Subject: [PATCH] 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 --- etc/quantum/plugins/nicira/nvp.ini | 14 + .../versions/363468ac592c_nvp_network_gw.py | 97 ++++ .../nicira/nicira_nvp_plugin/QuantumPlugin.py | 429 ++++++++++---- .../nicira/nicira_nvp_plugin/common/config.py | 9 +- .../nicira_nvp_plugin/common/exceptions.py | 6 + .../extensions/nvp_networkgw.py | 173 ++++++ .../nicira/nicira_nvp_plugin/nicira_db.py | 18 +- .../nicira_nvp_plugin/nicira_networkgw_db.py | 356 ++++++++++++ .../nicira/nicira_nvp_plugin/nvp_cluster.py | 31 +- .../nicira/nicira_nvp_plugin/nvplib.py | 161 +++++- .../unit/nicira/etc/fake_get_gwservice.json | 15 + .../etc/fake_get_lswitch_lport_att.json | 7 +- .../unit/nicira/etc/fake_post_gwservice.json | 13 + quantum/tests/unit/nicira/etc/nvp.ini.test | 3 +- .../tests/unit/nicira/fake_nvpapiclient.py | 46 +- quantum/tests/unit/nicira/test_networkgw.py | 526 ++++++++++++++++++ .../tests/unit/nicira/test_nicira_plugin.py | 34 +- quantum/tests/unit/nicira/test_nvplib.py | 109 +++- 18 files changed, 1888 insertions(+), 159 deletions(-) create mode 100644 quantum/db/migration/alembic_migrations/versions/363468ac592c_nvp_network_gw.py create mode 100644 quantum/plugins/nicira/nicira_nvp_plugin/extensions/nvp_networkgw.py create mode 100644 quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py create mode 100644 quantum/tests/unit/nicira/etc/fake_get_gwservice.json create mode 100644 quantum/tests/unit/nicira/etc/fake_post_gwservice.json create mode 100644 quantum/tests/unit/nicira/test_networkgw.py diff --git a/etc/quantum/plugins/nicira/nvp.ini b/etc/quantum/plugins/nicira/nvp.ini index 54775489b9..7221151edc 100644 --- a/etc/quantum/plugins/nicira/nvp.ini +++ b/etc/quantum/plugins/nicira/nvp.ini @@ -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: # ::::::: # is the ip address of the controller diff --git a/quantum/db/migration/alembic_migrations/versions/363468ac592c_nvp_network_gw.py b/quantum/db/migration/alembic_migrations/versions/363468ac592c_nvp_network_gw.py new file mode 100644 index 0000000000..2cbb5a1749 --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/363468ac592c_nvp_network_gw.py @@ -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') diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py index 5875b1030e..da5224444f 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py @@ -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:] 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, - 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 @@ -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 diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/common/config.py b/quantum/plugins/nicira/nicira_nvp_plugin/common/config.py index b26ae26ada..8c8a4e49cd 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/common/config.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/common/config.py @@ -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 diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/common/exceptions.py b/quantum/plugins/nicira/nicira_nvp_plugin/common/exceptions.py index 64d365fa4c..2bacc94511 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/common/exceptions.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/common/exceptions.py @@ -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 " diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/extensions/nvp_networkgw.py b/quantum/plugins/nicira/nicira_nvp_plugin/extensions/nvp_networkgw.py new file mode 100644 index 0000000000..36feb17484 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/extensions/nvp_networkgw.py @@ -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 diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nicira_db.py b/quantum/plugins/nicira/nicira_nvp_plugin/nicira_db.py index 58b67b5567..70aa16f603 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/nicira_db.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nicira_db.py @@ -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 diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py b/quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py new file mode 100644 index 0000000000..d01eedaed5 --- /dev/null +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py @@ -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) diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nvp_cluster.py b/quantum/plugins/nicira/nicira_nvp_plugin/nvp_cluster.py index e664c5fa63..6210c95dc3 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/nvp_cluster.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nvp_cluster.py @@ -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'] diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py index 7198929d9e..d7129287ca 100644 --- a/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py +++ b/quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py @@ -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. diff --git a/quantum/tests/unit/nicira/etc/fake_get_gwservice.json b/quantum/tests/unit/nicira/etc/fake_get_gwservice.json new file mode 100644 index 0000000000..5c8f9a376d --- /dev/null +++ b/quantum/tests/unit/nicira/etc/fake_get_gwservice.json @@ -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" +} diff --git a/quantum/tests/unit/nicira/etc/fake_get_lswitch_lport_att.json b/quantum/tests/unit/nicira/etc/fake_get_lswitch_lport_att.json index f8240a228b..cd1788b021 100644 --- a/quantum/tests/unit/nicira/etc/fake_get_lswitch_lport_att.json +++ b/quantum/tests/unit/nicira/etc/fake_get_lswitch_lport_att.json @@ -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" } } \ No newline at end of file diff --git a/quantum/tests/unit/nicira/etc/fake_post_gwservice.json b/quantum/tests/unit/nicira/etc/fake_post_gwservice.json new file mode 100644 index 0000000000..72292fddcd --- /dev/null +++ b/quantum/tests/unit/nicira/etc/fake_post_gwservice.json @@ -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" +} diff --git a/quantum/tests/unit/nicira/etc/nvp.ini.test b/quantum/tests/unit/nicira/etc/nvp.ini.test index 18b240c47e..d3b832309a 100644 --- a/quantum/tests/unit/nicira/etc/nvp.ini.test +++ b/quantum/tests/unit/nicira/etc/nvp.ini.test @@ -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 diff --git a/quantum/tests/unit/nicira/fake_nvpapiclient.py b/quantum/tests/unit/nicira/fake_nvpapiclient.py index 86e5b10798..039e22930c 100644 --- a/quantum/tests/unit/nicira/fake_nvpapiclient.py +++ b/quantum/tests/unit/nicira/fake_nvpapiclient.py @@ -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() diff --git a/quantum/tests/unit/nicira/test_networkgw.py b/quantum/tests/unit/nicira/test_networkgw.py new file mode 100644 index 0000000000..de9305c0a9 --- /dev/null +++ b/quantum/tests/unit/nicira/test_networkgw.py @@ -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) diff --git a/quantum/tests/unit/nicira/test_nicira_plugin.py b/quantum/tests/unit/nicira/test_nicira_plugin.py index d567053f3d..5411fd1d1b 100644 --- a/quantum/tests/unit/nicira/test_nicira_plugin.py +++ b/quantum/tests/unit/nicira/test_nicira_plugin.py @@ -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) diff --git a/quantum/tests/unit/nicira/test_nvplib.py b/quantum/tests/unit/nicira/test_nvplib.py index 24ac76d4d7..50fa53b805 100644 --- a/quantum/tests/unit/nicira/test_nvplib.py +++ b/quantum/tests/unit/nicira/test_nvplib.py @@ -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,15 +83,100 @@ 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', - 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')