]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add Quantum support for NVP Layer-2 gateways
authorSalvatore Orlando <sorlando@nicira.com>
Mon, 26 Nov 2012 01:11:42 +0000 (17:11 -0800)
committerSalvatore Orlando <salv.orlando@gmail.com>
Wed, 20 Feb 2013 01:44:31 +0000 (02:44 +0100)
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

18 files changed:
etc/quantum/plugins/nicira/nvp.ini
quantum/db/migration/alembic_migrations/versions/363468ac592c_nvp_network_gw.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py
quantum/plugins/nicira/nicira_nvp_plugin/common/config.py
quantum/plugins/nicira/nicira_nvp_plugin/common/exceptions.py
quantum/plugins/nicira/nicira_nvp_plugin/extensions/nvp_networkgw.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/nicira_db.py
quantum/plugins/nicira/nicira_nvp_plugin/nicira_networkgw_db.py [new file with mode: 0644]
quantum/plugins/nicira/nicira_nvp_plugin/nvp_cluster.py
quantum/plugins/nicira/nicira_nvp_plugin/nvplib.py
quantum/tests/unit/nicira/etc/fake_get_gwservice.json [new file with mode: 0644]
quantum/tests/unit/nicira/etc/fake_get_lswitch_lport_att.json
quantum/tests/unit/nicira/etc/fake_post_gwservice.json [new file with mode: 0644]
quantum/tests/unit/nicira/etc/nvp.ini.test
quantum/tests/unit/nicira/fake_nvpapiclient.py
quantum/tests/unit/nicira/test_networkgw.py [new file with mode: 0644]
quantum/tests/unit/nicira/test_nicira_plugin.py
quantum/tests/unit/nicira/test_nvplib.py

index 54775489b95eb4467bc11f6b79ab19f442201f44..7221151edce0bbade92d422145f2bf8b3c7e5ec8 100644 (file)
@@ -21,6 +21,10 @@ reconnect_interval = 2
 # Timeout in seconds before idle sql connections are reaped
 # sql_idle_timeout = 3600
 
+[QUOTAS]
+# number of network gateways allowed per tenant, -1 means unlimited
+# quota_network_gateway = 5
+
 [NVP]
 # Maximum number of ports for each bridged logical switch
 # max_lp_per_bridged_ls = 64
@@ -56,6 +60,16 @@ reconnect_interval = 2
 # with external gateways
 # default_l3_gw_service_uuid =
 
+# UUID of the default layer 2 gateway service to use for this cluster
+# This is optional. It should be filled for providing a predefined gateway
+# tenant case use for connecting their networks.
+# default_l2_gw_service_uuid =
+
+# Name of the default interface name to be used on network-gateway.
+# This value will be used for any device associated with a network
+# gateway for which an interface name was not specified
+# default_iface_name = breth0
+
 # This parameter describes a connection to a single NVP controller. Format:
 # <ip>:<port>:<user>:<pw>:<req_timeout>:<http_timeout>:<retries>:<redirects>
 # <ip> is the ip address of the controller
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 (file)
index 0000000..2cbb5a1
--- /dev/null
@@ -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')
index 5875b1030e274d9115f93d5e5b45f78cf21dabfb..da5224444f07ac81cb5550e68031dcf97797cccd 100644 (file)
@@ -30,6 +30,7 @@ import webob.exc
 from quantum.api.v2 import attributes as attr
 from quantum.api.v2 import base
 from quantum.common import constants
+from quantum import context as q_context
 from quantum.common import exceptions as q_exc
 from quantum.common import rpc as q_rpc
 from quantum.common import topics
@@ -55,15 +56,20 @@ from quantum import policy
 from quantum.plugins.nicira.nicira_nvp_plugin.common import config
 from quantum.plugins.nicira.nicira_nvp_plugin.common import (exceptions
                                                              as nvp_exc)
+from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_networkgw
+                                                                 as networkgw)
 from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_qos
                                                                  as ext_qos)
 from quantum.plugins.nicira.nicira_nvp_plugin import nicira_db
-from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient
-from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
+from quantum.plugins.nicira.nicira_nvp_plugin import (nicira_networkgw_db
+                                                      as networkgw_db)
+from quantum.plugins.nicira.nicira_nvp_plugin import nicira_qos_db as qos_db
 from quantum.plugins.nicira.nicira_nvp_plugin import nvp_cluster
 from quantum.plugins.nicira.nicira_nvp_plugin.nvp_plugin_version import (
     PLUGIN_VERSION)
-from quantum.plugins.nicira.nicira_nvp_plugin import nicira_qos_db as qos_db
+from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient
+from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
+
 LOG = logging.getLogger("QuantumPlugin")
 NVP_FLOATINGIP_NAT_RULES_ORDER = 200
 NVP_EXTGW_NAT_RULES_ORDER = 255
@@ -108,11 +114,74 @@ def parse_config():
              'nvp_controller_connection':
              nvp_conf[cluster_name].nvp_controller_connection,
              'default_l3_gw_service_uuid':
-             nvp_conf[cluster_name].default_l3_gw_service_uuid})
+             nvp_conf[cluster_name].default_l3_gw_service_uuid,
+             'default_l2_gw_service_uuid':
+             nvp_conf[cluster_name].default_l2_gw_service_uuid,
+             'default_interface_name':
+             nvp_conf[cluster_name].default_interface_name})
     LOG.debug(_("Cluster options:%s"), clusters_options)
     return cfg.CONF.NVP, clusters_options
 
 
+def parse_clusters_opts(clusters_opts, concurrent_connections,
+                        nvp_gen_timeout, default_cluster_name):
+    # Will store the first cluster in case is needed for default
+    # cluster assignment
+    clusters = {}
+    first_cluster = None
+    for c_opts in clusters_opts:
+        # Password is guaranteed to be the same across all controllers
+        # in the same NVP cluster.
+        cluster = nvp_cluster.NVPCluster(c_opts['name'])
+        try:
+            for ctrl_conn in c_opts['nvp_controller_connection']:
+                args = ctrl_conn.split(':')
+                try:
+                    args.extend([c_opts['default_tz_uuid'],
+                                 c_opts['nvp_cluster_uuid'],
+                                 c_opts['nova_zone_id'],
+                                 c_opts['default_l3_gw_service_uuid'],
+                                 c_opts['default_l2_gw_service_uuid'],
+                                 c_opts['default_interface_name']])
+                    cluster.add_controller(*args)
+                except Exception:
+                    LOG.exception(_("Invalid connection parameters for "
+                                    "controller %(ctrl)s in "
+                                    "cluster %(cluster)s"),
+                                  {'ctrl': ctrl_conn,
+                                   'cluster': c_opts['name']})
+                    raise nvp_exc.NvpInvalidConnection(
+                        conn_params=ctrl_conn)
+        except TypeError:
+            msg = _("No controller connection specified in cluster "
+                    "configuration. Please ensure at least a value for "
+                    "'nvp_controller_connection' is specified in the "
+                    "[CLUSTER:%s] section") % c_opts['name']
+            LOG.exception(msg)
+            raise nvp_exc.NvpPluginException(err_desc=msg)
+
+        api_providers = [(x['ip'], x['port'], True)
+                         for x in cluster.controllers]
+        cluster.api_client = NvpApiClient.NVPApiHelper(
+            api_providers, cluster.user, cluster.password,
+            request_timeout=cluster.request_timeout,
+            http_timeout=cluster.http_timeout,
+            retries=cluster.retries,
+            redirects=cluster.redirects,
+            concurrent_connections=concurrent_connections,
+            nvp_gen_timeout=nvp_gen_timeout)
+
+        if not clusters:
+            first_cluster = cluster
+        clusters[c_opts['name']] = cluster
+
+    if default_cluster_name and default_cluster_name in clusters:
+        default_cluster = clusters[default_cluster_name]
+    else:
+        default_cluster = first_cluster
+    return (clusters, default_cluster)
+
+
 class NVPRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
 
     # Set RPC API version to 1.0 by default.
@@ -131,8 +200,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                   l3_db.L3_NAT_db_mixin,
                   portsecurity_db.PortSecurityDbMixin,
                   securitygroups_db.SecurityGroupDbMixin,
-                  nvp_sec.NVPSecurityGroups,
+                  networkgw_db.NetworkGatewayMixin,
                   qos_db.NVPQoSDbMixin,
+                  nvp_sec.NVPSecurityGroups,
                   nvp_meta.NvpMetadataAccess):
     """
     NvpPluginV2 is a Quantum plugin that provides L2 Virtual Network
@@ -140,10 +210,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
     """
 
     supported_extension_aliases = ["provider", "quotas", "port-security",
-                                   "router", "security-group", "nvp-qos"]
+                                   "router", "security-group", "nvp-qos",
+                                   "network-gateway"]
+
     __native_bulk_support = True
 
-    # Default controller cluster
     # Map nova zones to cluster for easy retrieval
     novazone_cluster_map = {}
     # Default controller cluster (to be used when nova zone id is unspecified)
@@ -168,6 +239,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                        self._nvp_create_port,
                        l3_db.DEVICE_OWNER_FLOATINGIP:
                        self._nvp_create_fip_port,
+                       l3_db.DEVICE_OWNER_ROUTER_INTF:
+                       self._nvp_create_router_port,
+                       networkgw_db.DEVICE_OWNER_NET_GW_INTF:
+                       self._nvp_create_l2_gw_port,
                        'default': self._nvp_create_port},
             'delete': {l3_db.DEVICE_OWNER_ROUTER_GW:
                        self._nvp_delete_ext_gw_port,
@@ -175,66 +250,61 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                        self._nvp_delete_router_port,
                        l3_db.DEVICE_OWNER_FLOATINGIP:
                        self._nvp_delete_fip_port,
+                       l3_db.DEVICE_OWNER_ROUTER_INTF:
+                       self._nvp_delete_port,
+                       networkgw_db.DEVICE_OWNER_NET_GW_INTF:
+                       self._nvp_delete_port,
                        'default': self._nvp_delete_port}
         }
 
         self.nvp_opts, self.clusters_opts = parse_config()
-        self.clusters = {}
-        for c_opts in self.clusters_opts:
-            # Password is guaranteed to be the same across all controllers
-            # in the same NVP cluster.
-            cluster = nvp_cluster.NVPCluster(c_opts['name'])
-            for controller_connection in c_opts['nvp_controller_connection']:
-                args = controller_connection.split(':')
-                try:
-                    args.extend([c_opts['default_tz_uuid'],
-                                 c_opts['nvp_cluster_uuid'],
-                                 c_opts['nova_zone_id'],
-                                 c_opts['default_l3_gw_service_uuid']])
-                    cluster.add_controller(*args)
-                except Exception:
-                    LOG.exception(_("Invalid connection parameters for "
-                                    "controller %(conn)s in cluster %(name)s"),
-                                  {'conn': controller_connection,
-                                   'name': c_opts['name']})
-                    raise nvp_exc.NvpInvalidConnection(
-                        conn_params=controller_connection)
-
-            api_providers = [(x['ip'], x['port'], True)
-                             for x in cluster.controllers]
-            cluster.api_client = NvpApiClient.NVPApiHelper(
-                api_providers, cluster.user, cluster.password,
-                request_timeout=cluster.request_timeout,
-                http_timeout=cluster.http_timeout,
-                retries=cluster.retries,
-                redirects=cluster.redirects,
-                concurrent_connections=self.nvp_opts['concurrent_connections'],
-                nvp_gen_timeout=self.nvp_opts['nvp_gen_timeout'])
-
-            if len(self.clusters) == 0:
-                first_cluster = cluster
-            self.clusters[c_opts['name']] = cluster
-
-        def_cluster_name = self.nvp_opts.default_cluster_name
-        if def_cluster_name and def_cluster_name in self.clusters:
-            self.default_cluster = self.clusters[def_cluster_name]
-        else:
-            first_cluster_name = self.clusters.keys()[0]
-            if not def_cluster_name:
-                LOG.info(_("Default cluster name not specified. "
-                           "Using first cluster:%s"), first_cluster_name)
-            elif def_cluster_name not in self.clusters:
-                LOG.warning(_("Default cluster name %(def_cluster_name)s. "
-                              "Using first cluster:%(first_cluster_name)s"),
-                            locals())
-            # otherwise set 1st cluster as default
-            self.default_cluster = self.clusters[first_cluster_name]
+        if not self.clusters_opts:
+            msg = _("No cluster specified in NVP plugin configuration. "
+                    "Unable to start. Please ensure at least a "
+                    "[CLUSTER:<cluster_name>] section is specified in "
+                    "the NVP Plugin configuration file.")
+            LOG.error(msg)
+            raise nvp_exc.NvpPluginException(err_desc=msg)
+
+        self.clusters, self.default_cluster = parse_clusters_opts(
+            self.clusters_opts, self.nvp_opts.concurrent_connections,
+            self.nvp_opts.nvp_gen_timeout, self.nvp_opts.default_cluster_name)
 
         db.configure_db()
         # Extend the fault map
         self._extend_fault_map()
         # Set up RPC interface for DHCP agent
         self.setup_rpc()
+        # TODO(salvatore-orlando): Handle default gateways in multiple clusters
+        self._ensure_default_network_gateway()
+
+    def _ensure_default_network_gateway(self):
+        # Add the gw in the db as default, and unset any previous default
+        def_l2_gw_uuid = self.default_cluster.default_l2_gw_service_uuid
+        try:
+            ctx = q_context.get_admin_context()
+            self._unset_default_network_gateways(ctx)
+            if not def_l2_gw_uuid:
+                return
+            try:
+                def_network_gw = self._get_network_gateway(ctx,
+                                                           def_l2_gw_uuid)
+            except sa_exc.NoResultFound:
+                # Create in DB only - don't go on NVP
+                def_gw_data = {'id': def_l2_gw_uuid,
+                               'name': 'default L2 gateway service',
+                               'devices': []}
+                gw_res_name = networkgw.RESOURCE_NAME.replace('-', '_')
+                def_network_gw = super(
+                    NvpPluginV2, self).create_network_gateway(
+                        ctx, {gw_res_name: def_gw_data})
+            # In any case set is as default
+            self._set_default_network_gateway(ctx, def_network_gw['id'])
+        except Exception:
+            # This is fatal - abort startup
+            LOG.exception(_("Unable to process default l2 gw service:%s"),
+                          def_l2_gw_uuid)
+            raise
 
     def _build_ip_address_list(self, context, fixed_ips, subnet_ids=None):
         """  Build ip_addresses data structure for logical router port
@@ -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
 
index b26ae26ada84e6adfd1252400afb4a62935344dc..8c8a4e49cd3df87f8159dd37ba48fd8929e5a65b 100644 (file)
@@ -61,7 +61,14 @@ cluster_opts = [
     cfg.StrOpt('default_l3_gw_service_uuid',
                help=_("Unique identifier of the NVP L3 Gateway service "
                       "which will be used for implementing routers and "
-                      "floating IPs"))
+                      "floating IPs")),
+    cfg.StrOpt('default_l2_gw_service_uuid',
+               help=_("Unique identifier of the NVP L2 Gateway service "
+                      "which will be used by default for network gateways")),
+    cfg.StrOpt('default_interface_name', default='breth0',
+               help=_("Name of the interface on a L2 Gateway transport node"
+                      "which should be used by default when setting up a "
+                      "network connection")),
 ]
 
 # Register the configuration options
index 64d365fa4c6f69fab423fd6a1adebed69a26ad98..2bacc945117b6873243e42e7224f768c950bf0e3 100644 (file)
@@ -38,6 +38,12 @@ class NvpNoMorePortsException(NvpPluginException):
                 "Maximum number of ports reached")
 
 
+class NvpPortAlreadyAttached(q_exc.Conflict):
+    message = _("Unable to plug an interface into the port %(port_id)s "
+                "for network %(net_id)s. This interface is already plugged "
+                "into port %(att_port_id)s")
+
+
 class NvpNatRuleMismatch(NvpPluginException):
     message = _("While retrieving NAT rules, %(actual_rules)s were found "
                 "whereas rules in the (%(min_rules)s,%(max_rules)s) interval "
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 (file)
index 0000000..36feb17
--- /dev/null
@@ -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
index 58b67b5567c8b9b3bad61b7d38ff13ef85052dfa..70aa16f6036324919965891858cf342128cd3be2 100644 (file)
 #    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 (file)
index 0000000..d01eeda
--- /dev/null
@@ -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)
index e664c5fa63655f8731d382c0a40f6d10f7e428f6..6210c95dc3d28a97ac4a0da02c0aae9ee7816287 100644 (file)
@@ -56,8 +56,9 @@ class NVPCluster(object):
 
     def add_controller(self, ip, port, user, password, request_timeout,
                        http_timeout, retries, redirects, default_tz_uuid,
-                       uuid=None, zone=None,
-                       default_l3_gw_service_uuid=None):
+                       uuid=None, zone=None, default_l3_gw_service_uuid=None,
+                       default_l2_gw_service_uuid=None,
+                       default_interface_name=None):
         """Add a new set of controller parameters.
 
         :param ip: IP address of controller.
@@ -70,13 +71,16 @@ class NVPCluster(object):
         :param redirects: maximum number of server redirect responses to
             follow.
         :param default_tz_uuid: default transport zone uuid.
-        :param default_next_hop: default next hop for routers in this cluster.
         :param uuid: UUID of this cluster (used in MDI configs).
         :param zone: Zone of this cluster (used in MDI configs).
+        :param default_l3_gw_service_uuid: Default l3 gateway service
+        :param default_l2_gw_service_uuid: Default l2 gateway service
+        :param default_interface_name: Default interface name for l2 gateways
         """
 
         keys = ['ip', 'user', 'password', 'default_tz_uuid',
-                'default_l3_gw_service_uuid', 'uuid', 'zone']
+                'default_l3_gw_service_uuid', 'default_l2_gw_service_uuid',
+                'default_interface_name', 'uuid', 'zone']
         controller_dict = dict([(k, locals()[k]) for k in keys])
         default_tz_uuid = controller_dict.get('default_tz_uuid')
         if not re.match(attributes.UUID_PATTERN, default_tz_uuid):
@@ -97,6 +101,17 @@ class NVPCluster(object):
                           "might not work properly in this cluster"),
                         {'l3_gw_service_uuid': l3_gw_service_uuid,
                          'cluster_name': self.name})
+        # default_l2_gw_node_uuid is an optional parameter
+        # validate only if specified
+        l2_gw_service_uuid = controller_dict.get('default_l2_gw_node_uuid')
+        if l2_gw_service_uuid and not re.match(attributes.UUID_PATTERN,
+                                               l2_gw_service_uuid):
+            LOG.warning(_("default_l2_gw_node_uuid:%(l2_gw_service_uuid)s "
+                          "is not a valid UUID in the cluster "
+                          "%(cluster_name)s."),
+                        {'l2_gw_service_uuid': l2_gw_service_uuid,
+                         'cluster_name': self.name})
+
         int_keys = [
             'port', 'request_timeout', 'http_timeout', 'retries', 'redirects']
         for k in int_keys:
@@ -155,6 +170,14 @@ class NVPCluster(object):
     def default_l3_gw_service_uuid(self):
         return self.controllers[0]['default_l3_gw_service_uuid']
 
+    @property
+    def default_l2_gw_service_uuid(self):
+        return self.controllers[0]['default_l2_gw_service_uuid']
+
+    @property
+    def default_interface_name(self):
+        return self.controllers[0]['default_interface_name']
+
     @property
     def zone(self):
         return self.controllers[0]['zone']
index 7198929d9e37ba92e683aa6c86116b8e05cb645d..d7129287ca280e05b6cdf6eddd7d48f72f8b76c1 100644 (file)
@@ -46,14 +46,14 @@ DEF_TRANSPORT_TYPE = "stt"
 URI_PREFIX = "/ws.v1"
 # Resources exposed by NVP API
 LSWITCH_RESOURCE = "lswitch"
-LSWITCHPORT_RESOURCE = "lport-%s" % LSWITCH_RESOURCE
+LSWITCHPORT_RESOURCE = "lport/%s" % LSWITCH_RESOURCE
 LROUTER_RESOURCE = "lrouter"
-LROUTERPORT_RESOURCE = "lport-%s" % LROUTER_RESOURCE
-LROUTERNAT_RESOURCE = "nat-lrouter"
-LQUEUE_RESOURCE = "lqueue"
 # Current quantum version
+LROUTERPORT_RESOURCE = "lport/%s" % LROUTER_RESOURCE
+LROUTERNAT_RESOURCE = "nat/lrouter"
+LQUEUE_RESOURCE = "lqueue"
+GWSERVICE_RESOURCE = "gateway-service"
 QUANTUM_VERSION = "2013.1"
-
 # Constants for NAT rules
 MATCH_KEYS = ["destination_ip_addresses", "destination_port_max",
               "destination_port_min", "source_ip_addresses",
@@ -114,8 +114,11 @@ def _build_uri_path(resource,
                     resource_id=None,
                     parent_resource_id=None,
                     fields=None,
-                    relations=None, filters=None, is_attachment=False):
-    resources = resource.split('-')
+                    relations=None,
+                    filters=None,
+                    types=None,
+                    is_attachment=False):
+    resources = resource.split('/')
     res_path = resources[0] + (resource_id and "/%s" % resource_id or '')
     if len(resources) > 1:
         # There is also a parent resource to account for in the uri
@@ -127,6 +130,7 @@ def _build_uri_path(resource,
     params = []
     params.append(fields and "fields=%s" % fields)
     params.append(relations and "relations=%s" % relations)
+    params.append(types and "types=%s" % types)
     if filters:
         params.extend(['%s=%s' % (k, v) for (k, v) in filters.iteritems()])
     uri_path = "%s/%s" % (URI_PREFIX, res_path)
@@ -326,6 +330,42 @@ def update_lswitch(cluster, lswitch_id, display_name,
     return obj
 
 
+def create_l2_gw_service(cluster, tenant_id, display_name, devices):
+    """ Create a NVP Layer-2 Network Gateway Service.
+
+        :param cluster: The target NVP cluster
+        :param tenant_id: Identifier of the Openstack tenant for which
+        the gateway service.
+        :param display_name: Descriptive name of this gateway service
+        :param devices: List of transport node uuids (and network
+        interfaces on them) to use for the network gateway service
+        :raise NvpApiException: if there is a problem while communicating
+        with the NVP controller
+    """
+    tags = [{"tag": tenant_id, "scope": "os_tid"}]
+    # NOTE(salvatore-orlando): This is a little confusing, but device_id in
+    # NVP is actually the identifier a physical interface on the gateway
+    # device, which in the Quantum API is referred as interface_name
+    gateways = [{"transport_node_uuid": device['id'],
+                 "device_id": device['interface_name'],
+                 "type": "L2Gateway"} for device in devices]
+    gwservice_obj = {
+        "display_name": display_name,
+        "tags": tags,
+        "gateways": gateways,
+        "type": "L2GatewayServiceConfig"
+    }
+    try:
+        return json.loads(do_single_request(
+            "POST", _build_uri_path(GWSERVICE_RESOURCE),
+            json.dumps(gwservice_obj), cluster=cluster))
+    except NvpApiClient.NvpApiException:
+        # just log and re-raise - let the caller handle it
+        LOG.exception(_("An exception occured while communicating with "
+                        "the NVP controller for cluster:%s"), cluster.name)
+        raise
+
+
 def create_lrouter(cluster, tenant_id, display_name, nexthop):
     """ Create a NVP logical router on the specified cluster.
 
@@ -375,6 +415,19 @@ def delete_lrouter(cluster, lrouter_id):
         raise
 
 
+def delete_l2_gw_service(cluster, gateway_id):
+    try:
+        do_single_request("DELETE",
+                          _build_uri_path(GWSERVICE_RESOURCE,
+                                          resource_id=gateway_id),
+                          cluster=cluster)
+    except NvpApiClient.NvpApiException:
+        # just log and re-raise - let the caller handle it
+        LOG.exception(_("An exception occured while communicating with "
+                        "the NVP controller for cluster:%s"), cluster.name)
+        raise
+
+
 def get_lrouter(cluster, lrouter_id):
     try:
         return json.loads(do_single_request(HTTP_GET,
@@ -389,6 +442,19 @@ def get_lrouter(cluster, lrouter_id):
         raise
 
 
+def get_l2_gw_service(cluster, gateway_id):
+    try:
+        return json.loads(do_single_request("GET",
+                          _build_uri_path(GWSERVICE_RESOURCE,
+                                          resource_id=gateway_id),
+                          cluster=cluster))
+    except NvpApiClient.NvpApiException:
+        # just log and re-raise - let the caller handle it
+        LOG.exception(_("An exception occured while communicating with "
+                        "the NVP controller for cluster:%s"), cluster.name)
+        raise
+
+
 def get_lrouters(cluster, tenant_id, fields=None, filters=None):
     actual_filters = {}
     if filters:
@@ -405,6 +471,38 @@ def get_lrouters(cluster, tenant_id, fields=None, filters=None):
         cluster)
 
 
+def get_l2_gw_services(cluster, tenant_id=None,
+                       fields=None, filters=None):
+    actual_filters = dict(filters or {})
+    if tenant_id:
+        actual_filters['tag'] = tenant_id
+        actual_filters['tag_scope'] = 'os_tid'
+    return get_all_query_pages(
+        _build_uri_path(GWSERVICE_RESOURCE,
+                        filters=actual_filters),
+        cluster)
+
+
+def update_l2_gw_service(cluster, gateway_id, display_name):
+    # TODO(salvatore-orlando): Allow updates for gateways too
+    gwservice_obj = get_l2_gw_service(cluster, gateway_id)
+    if not display_name:
+        # Nothing to update
+        return gwservice_obj
+    gwservice_obj["display_name"] = display_name
+    try:
+        return json.loads(do_single_request("PUT",
+                          _build_uri_path(GWSERVICE_RESOURCE,
+                                          resource_id=gateway_id),
+                          json.dumps(gwservice_obj),
+                          cluster=cluster))
+    except NvpApiClient.NvpApiException:
+        # just log and re-raise - let the caller handle it
+        LOG.exception(_("An exception occured while communicating with "
+                        "the NVP controller for cluster:%s"), cluster.name)
+        raise
+
+
 def update_lrouter(cluster, lrouter_id, display_name, nexthop):
     lrouter_obj = get_lrouter(cluster, lrouter_id)
     if not display_name and not nexthop:
@@ -829,31 +927,42 @@ def get_port_status(cluster, lswitch_id, port_id):
         return constants.PORT_STATUS_DOWN
 
 
+def _plug_interface(cluster, lswitch_id, lport_id, att_obj):
+    uri = _build_uri_path(LSWITCHPORT_RESOURCE, lport_id, lswitch_id,
+                          is_attachment=True)
+    try:
+        resp_obj = do_single_request(HTTP_PUT, uri, json.dumps(att_obj),
+                                     cluster=cluster)
+    except NvpApiClient.NvpApiException:
+        LOG.exception(_("Exception while plugging an attachment:%(att)s "
+                        "into NVP port:%(port)s for NVP logical switch "
+                        "%(net)s"), {'net': lswitch_id,
+                                     'port': lport_id,
+                                     'att': att_obj})
+        raise
+
+    result = json.dumps(resp_obj)
+    return result
+
+
+def plug_l2_gw_service(cluster, lswitch_id, lport_id,
+                       gateway_id, vlan_id=None):
+    """ Plug a Layer-2 Gateway Attachment object in a logical port """
+    att_obj = {'type': 'L2GatewayAttachment',
+               'l2_gateway_service_uuid': gateway_id}
+    if vlan_id:
+        att_obj['vlan_id'] = vlan_id
+    return _plug_interface(cluster, lswitch_id, lport_id, att_obj)
+
+
 def plug_interface(cluster, lswitch_id, port, type, attachment=None):
-    uri = "/ws.v1/lswitch/" + lswitch_id + "/lport/" + port + "/attachment"
+    """ Plug a VIF Attachment object in a logical port """
     lport_obj = {}
     if attachment:
         lport_obj["vif_uuid"] = attachment
 
     lport_obj["type"] = type
-    try:
-        resp_obj = do_single_request(HTTP_PUT, uri, json.dumps(lport_obj),
-                                     cluster=cluster)
-    except NvpApiClient.ResourceNotFound as e:
-        LOG.error(_("Port or Network not found, Error: %s"), str(e))
-        raise exception.PortNotFound(port_id=port, net_id=lswitch_id)
-    except NvpApiClient.Conflict as e:
-        LOG.error(_("Conflict while making attachment to port, "
-                    "Error: %s"), str(e))
-        raise exception.AlreadyAttached(att_id=attachment,
-                                        port_id=port,
-                                        net_id=lswitch_id,
-                                        att_port_id="UNKNOWN")
-    except NvpApiClient.NvpApiException as e:
-        raise exception.QuantumException()
-
-    result = json.dumps(resp_obj)
-    return result
+    return _plug_interface(cluster, lswitch_id, port, lport_obj)
 
 #------------------------------------------------------------------------------
 # Security Profile convenience functions.
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 (file)
index 0000000..5c8f9a3
--- /dev/null
@@ -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"
+}
index f8240a228bd077e45e82ff570beeaee551b99b17..cd1788b021d96ab4b3193d73a444b2ca3789b783 100644 (file)
@@ -1,10 +1,7 @@
 {
   "LogicalPortAttachment":
     {
-      %(peer_port_href_field)s
-      %(peer_port_uuid_field)s
-      %(vif_uuid_field)s
-      "type": "%(type)s",
-      "schema": "/ws.v1/schema/%(type)s"
+      "type": "%(att_type)s",
+      "schema": "/ws.v1/schema/%(att_type)s"
     }
 }
\ 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 (file)
index 0000000..72292fd
--- /dev/null
@@ -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"
+}
index 18b240c47e6d14c6476ee77ca6fab5c10641552a..d3b832309a2ee1daf5d53389bcf5bf65927e69f8 100644 (file)
@@ -5,4 +5,5 @@ default_tz_uuid = fake_tz_uuid
 nova_zone_id = whatever
 nvp_cluster_uuid = fake_cluster_uuid
 nvp_controller_connection=fake:443:admin:admin:30:10:2:2
-default_l3_gw_uuid = whatever
+default_l3_gw_service_uuid = whatever
+default_l2_gw_service_uuid = whatever
index 86e5b107982cae81258706b3b13fa44f107831ab..039e22930cf6113b56b099094a999eb2f9649c39 100644 (file)
@@ -42,9 +42,11 @@ class FakeClient:
     LSWITCH_LPORT_ATT = 'lswitch_lportattachment'
     LROUTER_LPORT_STATUS = 'lrouter_lportstatus'
     LROUTER_LPORT_ATT = 'lrouter_lportattachment'
+    GWSERVICE_RESOURCE = 'gatewayservice'
 
     RESOURCES = [LSWITCH_RESOURCE, LROUTER_RESOURCE, LQUEUE_RESOURCE,
-                 LPORT_RESOURCE, NAT_RESOURCE, SECPROF_RESOURCE]
+                 LPORT_RESOURCE, NAT_RESOURCE, SECPROF_RESOURCE,
+                 GWSERVICE_RESOURCE]
 
     FAKE_GET_RESPONSES = {
         LSWITCH_RESOURCE: "fake_get_lswitch.json",
@@ -56,7 +58,8 @@ class FakeClient:
         LROUTER_LPORT_STATUS: "fake_get_lrouter_lport_status.json",
         LROUTER_LPORT_ATT: "fake_get_lrouter_lport_att.json",
         LROUTER_STATUS: "fake_get_lrouter_status.json",
-        LROUTER_NAT_RESOURCE: "fake_get_lrouter_nat.json"
+        LROUTER_NAT_RESOURCE: "fake_get_lrouter_nat.json",
+        GWSERVICE_RESOURCE: "fake_get_gwservice.json"
     }
 
     FAKE_POST_RESPONSES = {
@@ -66,7 +69,8 @@ class FakeClient:
         LROUTER_LPORT_RESOURCE: "fake_post_lrouter_lport.json",
         LROUTER_NAT_RESOURCE: "fake_post_lrouter_nat.json",
         SECPROF_RESOURCE: "fake_post_security_profile.json",
-        LQUEUE_RESOURCE: "fake_post_lqueue.json"
+        LQUEUE_RESOURCE: "fake_post_lqueue.json",
+        GWSERVICE_RESOURCE: "fake_post_gwservice.json"
     }
 
     FAKE_PUT_RESPONSES = {
@@ -78,7 +82,8 @@ class FakeClient:
         LSWITCH_LPORT_ATT: "fake_put_lswitch_lport_att.json",
         LROUTER_LPORT_ATT: "fake_put_lrouter_lport_att.json",
         SECPROF_RESOURCE: "fake_post_security_profile.json",
-        LQUEUE_RESOURCE: "fake_post_lqueue.json"
+        LQUEUE_RESOURCE: "fake_post_lqueue.json",
+        GWSERVICE_RESOURCE: "fake_post_gwservice.json"
     }
 
     MANAGED_RELATIONS = {
@@ -97,6 +102,7 @@ class FakeClient:
     _fake_lrouter_lportstatus_dict = {}
     _fake_securityprofile_dict = {}
     _fake_lqueue_dict = {}
+    _fake_gatewayservice_dict = {}
 
     def __init__(self, fake_files_path):
         self.fake_files_path = fake_files_path
@@ -219,6 +225,20 @@ class FakeClient:
             fake_nat['match_json'] = match_json
         return fake_nat
 
+    def _add_gatewayservice(self, body):
+        fake_gwservice = json.loads(body)
+        fake_gwservice['uuid'] = str(uuidutils.generate_uuid())
+        fake_gwservice['tenant_id'] = self._get_tag(
+            fake_gwservice, 'os_tid')
+        # FIXME(salvatore-orlando): For simplicity we're managing only a
+        # single device. Extend the fake client for supporting multiple devices
+        first_gw = fake_gwservice['gateways'][0]
+        fake_gwservice['transport_node_uuid'] = first_gw['transport_node_uuid']
+        fake_gwservice['device_id'] = first_gw['device_id']
+        self._fake_gatewayservice_dict[fake_gwservice['uuid']] = (
+            fake_gwservice)
+        return fake_gwservice
+
     def _build_relation(self, src, dst, resource_type, relation):
         if not relation in self.MANAGED_RELATIONS[resource_type]:
             return  # Relation is not desired in output
@@ -357,20 +377,20 @@ class FakeClient:
                      if (parent_func(res_uuid) and
                          _tag_match(res_uuid) and
                          _attr_match(res_uuid))]
-
             return json.dumps({'results': items,
                                'result_count': len(items)})
 
     def _show(self, resource_type, response_file,
               uuid1, uuid2=None, relations=None):
         target_uuid = uuid2 or uuid1
+        if resource_type.endswith('attachment'):
+            resource_type = resource_type[:resource_type.index('attachment')]
         with open("%s/%s" % (self.fake_files_path, response_file)) as f:
             response_template = f.read()
             res_dict = getattr(self, '_fake_%s_dict' % resource_type)
             for item in res_dict.itervalues():
                 if 'tags' in item:
                     item['tags_json'] = json.dumps(item['tags'])
-
             items = [json.loads(response_template % res_dict[res_uuid])
                      for res_uuid in res_dict if res_uuid == target_uuid]
             if items:
@@ -392,8 +412,11 @@ class FakeClient:
             else:
                 return self._list(res_type, response_file, uuids[0],
                                   query=parsedurl.query, relations=relations)
-        elif ('lswitch' in res_type or 'lrouter' in res_type
-              or self.SECPROF_RESOURCE in res_type):
+        elif ('lswitch' in res_type or
+              'lrouter' in res_type or
+              self.SECPROF_RESOURCE in res_type or
+              'gatewayservice' in res_type):
+            LOG.debug("UUIDS:%s", uuids)
             if len(uuids) > 0:
                 return self._show(res_type, response_file, uuids[0],
                                   relations=relations)
@@ -443,6 +466,7 @@ class FakeClient:
                 relations['LogicalPortAttachment'] = json.loads(body)
                 resource['_relations'] = relations
                 body_2 = json.loads(body)
+                resource['att_type'] = body_2['type']
                 if body_2['type'] == "PatchAttachment":
                     # We need to do a trick here
                     if self.LROUTER_RESOURCE in res_type:
@@ -462,6 +486,10 @@ class FakeClient:
                 elif body_2['type'] == "L3GatewayAttachment":
                     resource['attachment_gwsvc_uuid'] = (
                         body_2['l3_gateway_service_uuid'])
+                elif body_2['type'] == "L2GatewayAttachment":
+                    resource['attachment_gwsvc_uuid'] = (
+                        body_2['l2_gateway_service_uuid'])
+
             if not is_attachment:
                 response = response_template % resource
             else:
@@ -502,3 +530,5 @@ class FakeClient:
         self._fake_lswitch_lportstatus_dict.clear()
         self._fake_lrouter_lportstatus_dict.clear()
         self._fake_lqueue_dict.clear()
+        self._fake_securityprofile_dict.clear()
+        self._fake_gatewayservice_dict.clear()
diff --git a/quantum/tests/unit/nicira/test_networkgw.py b/quantum/tests/unit/nicira/test_networkgw.py
new file mode 100644 (file)
index 0000000..de9305c
--- /dev/null
@@ -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)
index d567053f3de945ac6df648f78b3ce66eb664f6e0..5411fd1d1b872e23ad786f461636fb3ddc3dbec9 100644 (file)
@@ -28,10 +28,13 @@ from quantum import context
 from quantum.extensions import providernet as pnet
 from quantum.extensions import securitygroup as secgrp
 from quantum import manager
+import quantum.plugins.nicira.nicira_nvp_plugin as nvp_plugin
+from quantum.plugins.nicira.nicira_nvp_plugin.extensions import nvp_networkgw
 from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_qos
                                                                  as ext_qos)
 from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
 from quantum.tests.unit.nicira import fake_nvpapiclient
+import quantum.tests.unit.nicira.test_networkgw as test_l2_gw
 from quantum.tests.unit import test_extensions
 import quantum.tests.unit.test_db_plugin as test_plugin
 import quantum.tests.unit.test_extension_portsecurity as psec
@@ -39,7 +42,7 @@ import quantum.tests.unit.test_extension_security_group as ext_sg
 import quantum.tests.unit.test_l3_plugin as test_l3_plugin
 
 LOG = logging.getLogger(__name__)
-NICIRA_PKG_PATH = 'quantum.plugins.nicira.nicira_nvp_plugin'
+NICIRA_PKG_PATH = nvp_plugin.__name__
 NICIRA_EXT_PATH = "../../plugins/nicira/nicira_nvp_plugin/extensions"
 
 
@@ -705,3 +708,32 @@ class NiciraQuantumNVPOutOfSync(test_l3_plugin.L3NatTestCaseBase,
         router = self.deserialize('json', req.get_response(self.ext_api))
         self.assertEquals(router['router']['status'],
                           constants.NET_STATUS_ERROR)
+
+
+class TestNiciraNetworkGateway(test_l2_gw.NetworkGatewayDbTestCase,
+                               NiciraPluginV2TestCase):
+
+    def setUp(self):
+        ext_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
+                                NICIRA_EXT_PATH)
+        cfg.CONF.set_override('api_extensions_path', ext_path)
+        super(TestNiciraNetworkGateway, self).setUp()
+
+    def test_list_network_gateways(self):
+        with self._network_gateway(name='test-gw-1') as gw1:
+            with self._network_gateway(name='test_gw_2') as gw2:
+                req = self.new_list_request(nvp_networkgw.COLLECTION_NAME)
+                res = self.deserialize('json', req.get_response(self.ext_api))
+                # We expect the default gateway too
+                key = self.resource + 's'
+                self.assertEquals(len(res[key]), 3)
+                self.assertEquals(res[key][0]['default'],
+                                  True)
+                self.assertEquals(res[key][1]['name'],
+                                  gw1[self.resource]['name'])
+                self.assertEquals(res[key][2]['name'],
+                                  gw2[self.resource]['name'])
+
+    def test_delete_network_gateway(self):
+        # The default gateway must still be there
+        self._test_delete_network_gateway(1)
index 24ac76d4d7df2e59c181f986c52de9f67b5f25da..50fa53b80593fc9024b79f043f3ca307185bcfd1 100644 (file)
 #
 # @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')