]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
quantum l3 + floating IP support
authorDan Wendlandt <dan@nicira.com>
Wed, 15 Aug 2012 19:56:52 +0000 (12:56 -0700)
committerSalvatore Orlando <salv.orlando@gmail.com>
Fri, 17 Aug 2012 15:15:34 +0000 (08:15 -0700)
bp quantum-l3-fw-nat

router & floating IP API calls, plugin db, and agent implemented
and unit tested

Change-Id: I6ee61396d22e2fd7840aa2ff7d1f6f4a2c6e54d4

14 files changed:
bin/quantum-l3-agent [new file with mode: 0755]
etc/l3_agent.ini [new file with mode: 0644]
quantum/agent/l3_agent.py [new file with mode: 0644]
quantum/api/v2/base.py
quantum/common/exceptions.py
quantum/db/db_base_plugin_v2.py
quantum/db/l3_db.py [new file with mode: 0644]
quantum/extensions/l3.py [new file with mode: 0644]
quantum/plugins/cisco/db/api.py
quantum/plugins/openvswitch/ovs_quantum_plugin.py
quantum/tests/unit/test_api_v2.py
quantum/tests/unit/test_l3_agent.py [new file with mode: 0644]
quantum/tests/unit/test_l3_plugin.py [new file with mode: 0644]
setup.py

diff --git a/bin/quantum-l3-agent b/bin/quantum-l3-agent
new file mode 100755 (executable)
index 0000000..39413e2
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 Openstack, LLC.
+# 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.
+
+from quantum.agent.l3_agent import main
+main()
diff --git a/etc/l3_agent.ini b/etc/l3_agent.ini
new file mode 100644 (file)
index 0000000..b119c8f
--- /dev/null
@@ -0,0 +1,19 @@
+[DEFAULT]
+# Show debugging output in log (sets DEBUG log level output)
+# debug = True
+
+# L3 requires that an inteface driver be set.  Choose the one that best
+# matches your plugin.
+
+# OVS
+interface_driver = quantum.agent.linux.interface.OVSInterfaceDriver
+# LinuxBridge
+#interface_driver = quantum.agent.linux.interface.BridgeInterfaceDriver
+
+# The Quantum user information for accessing the Quantum API.
+auth_url = http://localhost:35357/v2.0
+auth_region = RegionOne
+admin_tenant_name = %SERVICE_TENANT_NAME%
+admin_user = %SERVICE_USER%
+admin_password = %SERVICE_PASSWORD%
+
diff --git a/quantum/agent/l3_agent.py b/quantum/agent/l3_agent.py
new file mode 100644 (file)
index 0000000..098db3b
--- /dev/null
@@ -0,0 +1,414 @@
+"""
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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.
+#
+# @author: Dan Wendlandt, Nicira, Inc
+#
+"""
+
+import logging
+import sys
+import time
+
+import netaddr
+
+from quantum.agent.common import config
+from quantum.agent.linux import interface
+from quantum.agent.linux import ip_lib
+from quantum.agent.linux import iptables_manager
+from quantum.agent.linux import utils as linux_utils
+from quantum.db import l3_db
+from quantum.openstack.common import cfg
+from quantum.openstack.common import importutils
+from quantumclient.v2_0 import client
+
+LOG = logging.getLogger(__name__)
+NS_PREFIX = 'qrouter-'
+INTERNAL_DEV_PREFIX = 'qr-'
+EXTERNAL_DEV_PREFIX = 'qgw-'
+
+
+class RouterInfo(object):
+
+    def __init__(self, router_id, root_helper):
+        self.router_id = router_id
+        self.ex_gw_port = None
+        self.internal_ports = []
+        self.floating_ips = []
+        self.root_helper = root_helper
+
+        self.iptables_manager = iptables_manager.IptablesManager(
+            root_helper=root_helper,
+            #FIXME(danwent): use_ipv6=True,
+            namespace=self.ns_name())
+
+    def ns_name(self):
+        return NS_PREFIX + self.router_id
+
+
+class L3NATAgent(object):
+
+    OPTS = [
+        cfg.StrOpt('admin_user'),
+        cfg.StrOpt('admin_password'),
+        cfg.StrOpt('admin_tenant_name'),
+        cfg.StrOpt('auth_url'),
+        cfg.StrOpt('auth_strategy', default='keystone'),
+        cfg.StrOpt('auth_region'),
+        cfg.StrOpt('root_helper', default='sudo'),
+        cfg.StrOpt('external_network_bridge', default='br-ex',
+                   help="Name of bridge used for external network traffic."),
+        cfg.StrOpt('interface_driver',
+                   help="The driver used to manage the virtual interface."),
+        cfg.IntOpt('polling_interval',
+                   default=3,
+                   help="The time in seconds between state poll requests."),
+        cfg.StrOpt('metadata_ip', default='127.0.0.1',
+                   help="IP address used by Nova metadata server."),
+        cfg.IntOpt('metadata_port',
+                   default=8775,
+                   help="TCP Port used by Nova metadata server."),
+        #FIXME(danwent): not currently used
+        cfg.BoolOpt('send_arp_for_ha',
+                    default=True,
+                    help="Send gratuitious ARP when router IP is configured")
+    ]
+
+    def __init__(self, conf):
+        self.conf = conf
+
+        if not conf.interface_driver:
+            LOG.error(_('You must specify an interface driver'))
+            sys.exit(1)
+        try:
+            self.driver = importutils.import_object(conf.interface_driver,
+                                                    conf)
+        except:
+            LOG.exception(_("Error importing interface driver '%s'"
+                            % conf.interface_driver))
+            sys.exit(1)
+
+        self.polling_interval = conf.polling_interval
+
+        if not ip_lib.device_exists(self.conf.external_network_bridge):
+            raise Exception("external network bridge '%s' does not exist"
+                            % self.conf.external_network_bridge)
+
+        self.qclient = client.Client(
+            username=self.conf.admin_user,
+            password=self.conf.admin_password,
+            tenant_name=self.conf.admin_tenant_name,
+            auth_url=self.conf.auth_url,
+            auth_strategy=self.conf.auth_strategy,
+            auth_region=self.conf.auth_region
+        )
+
+        # disable forwarding
+        linux_utils.execute(['sysctl', '-w', 'net.ipv4.ip_forward=0'],
+                            self.conf.root_helper, check_exit_code=False)
+
+        self._destroy_router_namespaces()
+
+        # enable forwarding
+        linux_utils.execute(['sysctl', '-w', 'net.ipv4.ip_forward=1'],
+                            self.conf.root_helper, check_exit_code=False)
+
+    def _destroy_router_namespaces(self):
+        """Destroy all router namespaces on the host to eliminate
+        all stale linux devices, iptables rules, and namespaces.
+        """
+        root_ip = ip_lib.IPWrapper(self.conf.root_helper)
+        for ns in root_ip.get_namespaces(self.conf.root_helper):
+            if ns.startswith(NS_PREFIX):
+                ns_ip = ip_lib.IPWrapper(self.conf.root_helper,
+                                         namespace=ns)
+                for d in ns_ip.get_devices():
+                    if d.name.startswith(INTERNAL_DEV_PREFIX):
+                        # device is on default bridge
+                        self.driver.unplug(d.name)
+                    elif d.name.startswith(EXTERNAL_DEV_PREFIX):
+                        self.driver.unplug(d.name,
+                                           bridge=
+                                           self.conf.external_network_bridge)
+
+                # FIXME(danwent): disabling actual deletion of namespace
+                # until we figure out why it fails.  Having deleted all
+                # devices, the only harm here should be the clutter of
+                # the namespace lying around.
+
+                # ns_ip.netns.delete(ns)
+
+    def daemon_loop(self):
+
+        #TODO(danwent): this simple diff logic does not handle if a
+        # resource is modified (i.e., ip change on port, or floating ip
+        # mapped from one IP to another).  Will fix this properly with
+        # update notifications.
+        # Likewise, it does not handle removing routers
+
+        self.router_info = {}
+        while True:
+            try:
+                #TODO(danwent): provide way to limit this to a single
+                # router, for model where agent runs in dedicated VM
+                for r in self.qclient.list_routers()['routers']:
+                    if r['id'] not in self.router_info:
+                        self.router_info[r['id']] = (RouterInfo(r['id'],
+                                                     self.conf.root_helper))
+                    ri = self.router_info[r['id']]
+                    self.process_router(ri)
+            except:
+                LOG.exception("Error running l3_nat daemon_loop")
+
+            time.sleep(self.polling_interval)
+
+    def _set_subnet_info(self, port):
+        ips = port['fixed_ips']
+        if not ips:
+            raise Exception("Router port %s has no IP address" % port['id'])
+        if len(ips) > 1:
+            LOG.error("Ignoring multiple IPs on router port %s" % port['id'])
+        port['subnet'] = self.qclient.show_subnet(
+            ips[0]['subnet_id'])['subnet']
+        prefixlen = netaddr.IPNetwork(port['subnet']['cidr']).prefixlen
+        port['ip_cidr'] = "%s/%s" % (ips[0]['ip_address'], prefixlen)
+
+    def process_router(self, ri):
+
+        ex_gw_port = self._get_ex_gw_port(ri)
+
+        internal_ports = self.qclient.list_ports(
+            device_id=ri.router_id,
+            device_owner=l3_db.DEVICE_OWNER_ROUTER_INTF)['ports']
+
+        existing_port_ids = set([p['id'] for p in ri.internal_ports])
+        current_port_ids = set([p['id'] for p in internal_ports])
+
+        for p in internal_ports:
+            if p['id'] not in existing_port_ids:
+                self._set_subnet_info(p)
+                ri.internal_ports.append(p)
+                self.internal_network_added(ri, ex_gw_port, p['id'],
+                                            p['ip_cidr'], p['mac_address'])
+
+        port_ids_to_remove = existing_port_ids - current_port_ids
+        for p in ri.internal_ports:
+            if p['id'] in port_ids_to_remove:
+                ri.internal_ports.remove(p)
+                self.internal_network_removed(ri, ex_gw_port, p['id'],
+                                              p['ip_cidr'])
+
+        internal_cidrs = [p['ip_cidr'] for p in ri.internal_ports]
+
+        if ex_gw_port and not ri.ex_gw_port:
+            self._set_subnet_info(ex_gw_port)
+            self.external_gateway_added(ri, ex_gw_port, internal_cidrs)
+        elif not ex_gw_port and ri.ex_gw_port:
+            self.external_gateway_removed(ri, ri.ex_gw_port,
+                                          internal_cidrs)
+
+        if ri.ex_gw_port or ex_gw_port:
+            self.process_router_floating_ips(ri, ex_gw_port)
+
+        ri.ex_gw_port = ex_gw_port
+
+    def process_router_floating_ips(self, ri, ex_gw_port):
+        floating_ips = self.qclient.list_floatingips(
+            router_id=ri.router_id)['floatingips']
+        existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips])
+        cur_floating_ip_ids = set([fip['id'] for fip in floating_ips])
+
+        for fip in floating_ips:
+            if fip['port_id']:
+                if fip['id'] not in existing_floating_ip_ids:
+                    ri.floating_ips.append(fip)
+                    self.floating_ip_added(ri, ex_gw_port,
+                                           fip['floating_ip_address'],
+                                           fip['fixed_ip_address'])
+
+        floating_ip_ids_to_remove = (existing_floating_ip_ids -
+                                     cur_floating_ip_ids)
+        for fip in ri.floating_ips:
+            if fip['id'] in floating_ip_ids_to_remove:
+                ri.floating_ips.remove(fip)
+                self.floating_ip_removed(ri, ri.ex_gw_port,
+                                         fip['floating_ip_address'],
+                                         fip['fixed_ip_address'])
+
+    def _get_ex_gw_port(self, ri):
+        ports = self.qclient.list_ports(
+            device_id=ri.router_id,
+            device_owner=l3_db.DEVICE_OWNER_ROUTER_GW)['ports']
+        if not ports:
+            return None
+        elif len(ports) == 1:
+            return ports[0]
+        else:
+            LOG.error("Ignoring multiple gateway ports for router %s"
+                      % ri.router_id)
+
+    def get_internal_device_name(self, port_id):
+        return (INTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
+
+    def get_external_device_name(self, port_id):
+        return (EXTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
+
+    def external_gateway_added(self, ri, ex_gw_port, internal_cidrs):
+
+        interface_name = self.get_external_device_name(ex_gw_port['id'])
+        ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
+        if not ip_lib.device_exists(interface_name,
+                                    root_helper=self.conf.root_helper,
+                                    namespace=ri.ns_name()):
+            self.driver.plug(None, ex_gw_port['id'], interface_name,
+                             ex_gw_port['mac_address'],
+                             bridge=self.conf.external_network_bridge,
+                             namespace=ri.ns_name())
+        self.driver.init_l3(interface_name, [ex_gw_port['ip_cidr']],
+                            namespace=ri.ns_name())
+
+        gw_ip = ex_gw_port['subnet']['gateway_ip']
+        if ex_gw_port['subnet']['gateway_ip']:
+            cmd = ['route', 'add', 'default', 'gw', gw_ip]
+            ip_wrapper = ip_lib.IPWrapper(self.conf.root_helper,
+                                          namespace=ri.ns_name())
+            ip_wrapper.netns.execute(cmd)
+
+        for (c, r) in self.external_gateway_filter_rules():
+            ri.iptables_manager.ipv4['filter'].add_rule(c, r)
+        for (c, r) in self.external_gateway_nat_rules(ex_gw_ip,
+                                                      internal_cidrs):
+            ri.iptables_manager.ipv4['nat'].add_rule(c, r)
+        ri.iptables_manager.apply()
+
+    def external_gateway_removed(self, ri, ex_gw_port, internal_cidrs):
+
+        interface_name = self.get_external_device_name(ex_gw_port['id'])
+        if ip_lib.device_exists(interface_name,
+                                root_helper=self.conf.root_helper,
+                                namespace=ri.ns_name()):
+            self.driver.unplug(interface_name)
+
+        ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
+        for c, r in self.external_gateway_filter_rules():
+            ri.iptables_manager.ipv4['filter'].remove_rule(c, r)
+        for c, r in self.external_gateway_nat_rules(ex_gw_ip, internal_cidrs):
+            ri.iptables_manager.ipv4['nat'].remove_rule(c, r)
+        ri.iptables_manager.apply()
+
+    def external_gateway_filter_rules(self):
+        return [('INPUT', '-s 0.0.0.0/0 -d %s '
+                          '-p tcp -m tcp --dport %s '
+                          '-j ACCEPT' %
+                          (self.conf.metadata_ip, self.conf.metadata_port))]
+
+    def external_gateway_nat_rules(self, ex_gw_ip, internal_cidrs):
+        rules = [('PREROUTING', '-s 0.0.0.0/0 -d 169.254.169.254/32 '
+                  '-p tcp -m tcp --dport 80 -j DNAT '
+                  '--to-destination %s:%s' %
+                  (self.conf.metadata_ip, self.conf.metadata_port))]
+        for cidr in internal_cidrs:
+            rules.extend(self.internal_network_nat_rules(ex_gw_ip, cidr))
+        return rules
+
+    def internal_network_added(self, ri, ex_gw_port, port_id,
+                               internal_cidr, mac_address):
+        interface_name = self.get_internal_device_name(port_id)
+        if not ip_lib.device_exists(interface_name,
+                                    root_helper=self.conf.root_helper,
+                                    namespace=ri.ns_name()):
+            self.driver.plug(None, port_id, interface_name, mac_address,
+                             namespace=ri.ns_name())
+
+        self.driver.init_l3(interface_name, [internal_cidr],
+                            namespace=ri.ns_name())
+
+        if ex_gw_port:
+            ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
+            for c, r in self.internal_network_nat_rules(ex_gw_ip,
+                                                        internal_cidr):
+                ri.iptables_manager.ipv4['nat'].add_rule(c, r)
+            ri.iptables_manager.apply()
+
+    def internal_network_removed(self, ri, ex_gw_port, port_id, internal_cidr):
+        interface_name = self.get_internal_device_name(port_id)
+        if ip_lib.device_exists(interface_name,
+                                root_helper=self.conf.root_helper,
+                                namespace=ri.ns_name()):
+            self.driver.unplug(interface_name)
+
+        if ex_gw_port:
+            ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
+            for c, r in self.internal_network_nat_rules(ex_gw_ip,
+                                                        internal_cidr):
+                ri.iptables_manager.ipv4['nat'].remove_rule(c, r)
+            ri.iptables_manager.apply()
+
+    def internal_network_nat_rules(self, ex_gw_ip, internal_cidr):
+        return [('snat', '-s %s -j SNAT --to-source %s' %
+                 (internal_cidr, ex_gw_ip)),
+                ('POSTROUTING', '-s %s -d %s/32 -j ACCEPT' %
+                 (internal_cidr, self.conf.metadata_ip))]
+
+    def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        ip_cidr = str(floating_ip) + '/32'
+        interface_name = self.get_external_device_name(ex_gw_port['id'])
+        device = ip_lib.IPDevice(interface_name, self.conf.root_helper,
+                                 namespace=ri.ns_name())
+
+        if not ip_cidr in [addr['cidr'] for addr in device.addr.list()]:
+            net = netaddr.IPNetwork(ip_cidr)
+            device.addr.add(net.version, ip_cidr, str(net.broadcast))
+
+        for chain, rule in self.floating_forward_rules(floating_ip, fixed_ip):
+            ri.iptables_manager.ipv4['nat'].add_rule(chain, rule)
+        ri.iptables_manager.apply()
+
+    def floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
+        ip_cidr = str(floating_ip) + '/32'
+        net = netaddr.IPNetwork(ip_cidr)
+        interface_name = self.get_external_device_name(ex_gw_port['id'])
+
+        device = ip_lib.IPDevice(interface_name, self.conf.root_helper,
+                                 namespace=ri.ns_name())
+        device.addr.delete(net.version, ip_cidr)
+
+        for chain, rule in self.floating_forward_rules(floating_ip, fixed_ip):
+            ri.iptables_manager.ipv4['nat'].remove_rule(chain, rule)
+        ri.iptables_manager.apply()
+
+    def floating_forward_rules(self, floating_ip, fixed_ip):
+        return [('PREROUTING', '-d %s -j DNAT --to %s' %
+                 (floating_ip, fixed_ip)),
+                ('OUTPUT', '-d %s -j DNAT --to %s' %
+                 (floating_ip, fixed_ip)),
+                ('float-snat', '-s %s -j SNAT --to %s' %
+                 (fixed_ip, floating_ip))]
+
+
+def main():
+    conf = config.setup_conf()
+    conf.register_opts(L3NATAgent.OPTS)
+    conf.register_opts(interface.OPTS)
+    conf(sys.argv)
+    config.setup_logging(conf)
+
+    mgr = L3NATAgent(conf)
+    mgr.daemon_loop()
+
+
+if __name__ == '__main__':
+    main()
index f16df2e936aeca372e63c07decf8532816fad17f..9800714dc26e74a06336c84035cc0002806a99ba 100644 (file)
@@ -32,6 +32,7 @@ XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
 
 FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
              exceptions.InUse: webob.exc.HTTPConflict,
+             exceptions.BadRequest: webob.exc.HTTPBadRequest,
              exceptions.ResourceExhausted: webob.exc.HTTPServiceUnavailable,
              exceptions.MacAddressGenerationFailure:
              webob.exc.HTTPServiceUnavailable,
@@ -132,8 +133,11 @@ def _verbose(request):
 
 
 class Controller(object):
-    def __init__(self, plugin, collection, resource,
-                 attr_info, allow_bulk=False):
+
+    def __init__(self, plugin, collection, resource, attr_info,
+                 allow_bulk=False, member_actions=None):
+        if member_actions is None:
+            member_actions = []
         self._plugin = plugin
         self._collection = collection
         self._resource = resource
@@ -143,6 +147,7 @@ class Controller(object):
         self._policy_attrs = [name for (name, info) in self._attr_info.items()
                               if info.get('required_by_policy')]
         self._publisher_id = notifier_api.publisher_id('network')
+        self._member_actions = member_actions
 
     def _is_native_bulk_supported(self):
         native_bulk_attr_name = ("_%s__native_bulk_support"
@@ -157,6 +162,7 @@ class Controller(object):
         # make sure fields_to_strip is iterable
         if not fields_to_strip:
             fields_to_strip = []
+
         return dict(item for item in data.iteritems()
                     if self._is_visible(item[0])
                     and not item[0] in fields_to_strip)
@@ -170,6 +176,14 @@ class Controller(object):
             original_fields.extend(self._policy_attrs)
         return original_fields, fields_to_add
 
+    def __getattr__(self, name):
+        if name in self._member_actions:
+            def _handle_action(request, id, body=None):
+                return getattr(self._plugin, name)(request.context, id, body)
+            return _handle_action
+        else:
+            raise AttributeError
+
     def _items(self, request, do_authz=False):
         """Retrieves and formats a list of elements of the requested entity"""
         # NOTE(salvatore-orlando): The following ensures that fields which
@@ -545,8 +559,10 @@ class Controller(object):
             })
 
 
-def create_resource(collection, resource, plugin, params, allow_bulk=False):
-    controller = Controller(plugin, collection, resource, params, allow_bulk)
+def create_resource(collection, resource, plugin, params, allow_bulk=False,
+                    member_actions=None):
+    controller = Controller(plugin, collection, resource, params, allow_bulk,
+                            member_actions=member_actions)
 
     # NOTE(jkoelker) To anyone wishing to add "proper" xml support
     #                this is where you do it
index 241727c1b1813b85193adc4931c13025717fc28c..4e0f0456d46cc4bdfcc0e1e0a0ed0bac8fd73fa8 100644 (file)
@@ -34,6 +34,10 @@ class QuantumException(OpenstackException):
     message = _("An unknown exception occurred.")
 
 
+class BadRequest(QuantumException):
+    message = _('Bad %(resource)s request: %(msg)s')
+
+
 class NotFound(QuantumException):
     pass
 
@@ -86,13 +90,13 @@ class NetworkInUse(InUse):
 
 class SubnetInUse(InUse):
     message = _("Unable to complete operation on subnet %(subnet_id)s. "
-                "There is used by one or more ports.")
+                "One or more ports have an IP allocation from this subnet.")
 
 
 class PortInUse(InUse):
     message = _("Unable to complete operation on port %(port_id)s "
-                "for network %(net_id)s. The attachment '%(att_id)s"
-                "is plugged into the logical port.")
+                "for network %(net_id)s. Port already has an attached"
+                "device %(device_id)s.")
 
 
 class MacAddressInUse(InUse):
index eed7225423cd801698c66b30ac5985ea6abf266b..0fee6ff6f67fb13750d43c9e01681d8cfb348e29 100644 (file)
@@ -1067,11 +1067,12 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         return self._make_port_dict(port, fields)
 
     def get_ports(self, context, filters=None, fields=None, verbose=None):
-        fixed_ips = filters.pop('fixed_ips', [])
+        fixed_ips = filters.pop('fixed_ips', []) if filters else []
         ports = self._get_collection(context, models_v2.Port,
                                      self._make_port_dict,
                                      filters=filters, fields=fields,
                                      verbose=verbose)
+
         if ports and fixed_ips:
             filtered_ports = []
             for port in ports:
diff --git a/quantum/db/l3_db.py b/quantum/db/l3_db.py
new file mode 100644 (file)
index 0000000..0a14178
--- /dev/null
@@ -0,0 +1,546 @@
+"""
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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.
+#
+# @author: Dan Wendlandt, Nicira, Inc
+#
+"""
+
+import logging
+
+import sqlalchemy as sa
+from sqlalchemy import orm
+from sqlalchemy.orm import exc
+import webob.exc as w_exc
+
+from quantum.api.v2 import attributes
+from quantum.common import exceptions as q_exc
+from quantum.common import utils
+from quantum.db import model_base
+from quantum.db import models_v2
+from quantum.extensions import l3
+from quantum.openstack.common import cfg
+
+
+LOG = logging.getLogger(__name__)
+
+l3_opts = [
+    cfg.StrOpt('metadata_ip_address', default='127.0.0.1'),
+    cfg.IntOpt('metadata_port', default=8775)
+]
+
+# Register the configuration options
+cfg.CONF.register_opts(l3_opts)
+
+DEVICE_OWNER_ROUTER_INTF = "network:router_interface"
+DEVICE_OWNER_ROUTER_GW = "network:router_gateway"
+DEVICE_OWNER_FLOATINGIP = "network:floatingip"
+
+
+class Router(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
+    """Represents a v2 quantum router."""
+    name = sa.Column(sa.String(255))
+    status = sa.Column(sa.String(16))
+    admin_state_up = sa.Column(sa.Boolean)
+    gw_port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id',
+                                                        ondelete="CASCADE"))
+    gw_port = orm.relationship(models_v2.Port)
+
+
+class FloatingIP(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
+    """Represents a floating IP, which may or many not be
+       allocated to a tenant, and may or may not be associated with
+       an internal port/ip address/router.
+    """
+    floating_ip_address = sa.Column(sa.String(64), nullable=False)
+    floating_network_id = sa.Column(sa.String(36), nullable=False)
+    floating_port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id'),
+                                 nullable=False)
+    fixed_port_id = sa.Column(sa.String(36), sa.ForeignKey('ports.id'))
+    fixed_ip_address = sa.Column(sa.String(64))
+    router_id = sa.Column(sa.String(36), sa.ForeignKey('routers.id'))
+
+
+class L3_NAT_db_mixin(l3.RouterPluginBase):
+    """Mixin class to add L3/NAT router methods to db_plugin_base_v2"""
+
+    def _get_router(self, context, id, verbose=None):
+        try:
+            router = self._get_by_id(context, Router, id, verbose=verbose)
+        except exc.NoResultFound:
+            raise l3.RouterNotFound(router_id=id)
+        except exc.MultipleResultsFound:
+            LOG.error('Multiple routers match for %s' % id)
+            raise l3.RouterNotFound(router_id=id)
+        return router
+
+    def _make_router_dict(self, router, fields=None):
+        res = {'id': router['id'],
+               'name': router['name'],
+               'tenant_id': router['tenant_id'],
+               'admin_state_up': router['admin_state_up'],
+               'status': router['status'],
+               'external_gateway_info': None}
+        if router['gw_port_id']:
+            nw_id = router.gw_port['network_id']
+            res['external_gateway_info'] = {'network_id': nw_id}
+        return self._fields(res, fields)
+
+    def create_router(self, context, router):
+        r = router['router']
+        has_gw_info = False
+        if 'external_gateway_info' in r:
+            has_gw_info = True
+            gw_info = r['external_gateway_info']
+            del r['external_gateway_info']
+        tenant_id = self._get_tenant_id_for_create(context, r)
+        with context.session.begin(subtransactions=True):
+            # pre-generate id so it will be available when
+            # configuring external gw port
+            router_db = Router(id=utils.str_uuid(),
+                               tenant_id=tenant_id,
+                               name=r['name'],
+                               admin_state_up=r['admin_state_up'],
+                               status="ACTIVE")
+            context.session.add(router_db)
+            if has_gw_info:
+                self._update_router_gw_info(context, router_db['id'], gw_info)
+        return self._make_router_dict(router_db)
+
+    def update_router(self, context, id, router):
+        r = router['router']
+        has_gw_info = False
+        if 'external_gateway_info' in r:
+            has_gw_info = True
+            gw_info = r['external_gateway_info']
+            del r['external_gateway_info']
+        with context.session.begin(subtransactions=True):
+            if has_gw_info:
+                self._update_router_gw_info(context, id, gw_info)
+            router_db = self._get_router(context, id)
+            # Ensure we actually have something to update
+            if r.keys():
+                router_db.update(r)
+        return self._make_router_dict(router_db)
+
+    def _update_router_gw_info(self, context, router_id, info):
+        # TODO(salvatore-orlando): guarantee atomic behavior also across
+        # operations that span beyond the model classes handled by this
+        # class (e.g.: delete_port)
+        router = self._get_router(context, router_id)
+        gw_port = router.gw_port
+
+        network_id = info.get('network_id', None) if info else None
+        if network_id:
+            #FIXME(danwent): confirm net-id is valid external network
+            self._get_network(context, network_id)
+
+        # figure out if we need to delete existing port
+        if gw_port and gw_port['network_id'] != network_id:
+            with context.session.begin(subtransactions=True):
+                router.update({'gw_port_id': None})
+                context.session.add(router)
+            self.delete_port(context, gw_port['id'])
+
+        if network_id is not None and (gw_port is None or
+                                       gw_port['network_id'] != network_id):
+            # Port has no 'tenant-id', as it is hidden from user
+            gw_port = self.create_port(context, {
+                'port':
+                {'network_id': network_id,
+                 'mac_address': attributes.ATTR_NOT_SPECIFIED,
+                 'fixed_ips': attributes.ATTR_NOT_SPECIFIED,
+                 'device_id': router_id,
+                 'device_owner': DEVICE_OWNER_ROUTER_GW,
+                 'admin_state_up': True,
+                 'name': ''}})
+
+            if not len(gw_port['fixed_ips']):
+                self.delete_port(context, gw_port['id'])
+                msg = ('No IPs available for external network %s' %
+                       network_id)
+                raise q_exc.BadRequest(resource='router', msg=msg)
+
+            with context.session.begin(subtransactions=True):
+                router.update({'gw_port_id': gw_port['id']})
+                context.session.add(router)
+
+    def delete_router(self, context, id):
+        with context.session.begin(subtransactions=True):
+            router = self._get_router(context, id)
+
+            device_filter = {'device_id': [id],
+                             'device_owner': [DEVICE_OWNER_ROUTER_INTF]}
+            ports = self.get_ports(context, filters=device_filter)
+            if ports:
+                raise l3.RouterInUse(router_id=id)
+            # NOTE(salvatore-orlando): gw port will be automatically deleted
+            # thanks to cascading on the ORM relationship
+            context.session.delete(router)
+
+    def get_router(self, context, id, fields=None, verbose=None):
+        router = self._get_router(context, id, verbose=verbose)
+        return self._make_router_dict(router, fields)
+
+    def get_routers(self, context, filters=None, fields=None, verbose=None):
+        return self._get_collection(context, Router,
+                                    self._make_router_dict,
+                                    filters=filters, fields=fields,
+                                    verbose=verbose)
+
+    def _check_for_dup_router_subnet(self, context, router_id,
+                                     network_id, subnet_id):
+        try:
+            rport_qry = context.session.query(models_v2.Port)
+            rports = rport_qry.filter_by(
+                device_id=router_id,
+                device_owner=DEVICE_OWNER_ROUTER_INTF,
+                network_id=network_id).all()
+            # its possible these ports on on the same network, but
+            # different subnet
+            for p in rports:
+                for ip in p['fixed_ips']:
+                    if ip['subnet_id'] == subnet_id:
+                        msg = ("Router already has a port on subnet %s"
+                               % subnet_id)
+                        raise q_exc.BadRequest(resource='router', msg=msg)
+
+        except exc.NoResultFound:
+            pass
+
+    def add_router_interface(self, context, router_id, interface_info):
+        # make sure router exists - will raise if not
+        self._get_router(context, router_id)
+        if not interface_info:
+            msg = "Either subnet_id or port_id must be specified"
+            raise q_exc.BadRequest(resource='router', msg=msg)
+
+        if 'port_id' in interface_info:
+            if 'subnet_id' in interface_info:
+                msg = "cannot specify both subnet-id and port-id"
+                raise q_exc.BadRequest(resource='router', msg=msg)
+
+            port = self._get_port(context, interface_info['port_id'])
+            if port['device_id']:
+                raise q_exc.PortInUse(net_id=port['network_id'],
+                                      port_id=port['id'],
+                                      device_id=port['device_id'])
+            fixed_ips = [ip for ip in port['fixed_ips']]
+            if len(fixed_ips) != 1:
+                msg = 'Router port must have exactly one fixed IP'
+                raise q_exc.BadRequest(resource='router', msg=msg)
+            self._check_for_dup_router_subnet(context, router_id,
+                                              port['network_id'],
+                                              fixed_ips[0]['subnet_id'])
+            port.update({'device_id': router_id,
+                         'device_owner': DEVICE_OWNER_ROUTER_INTF})
+        elif 'subnet_id' in interface_info:
+            subnet_id = interface_info['subnet_id']
+            subnet = self._get_subnet(context, subnet_id)
+            # Ensure the subnet has a gateway
+            if not subnet['gateway_ip']:
+                msg = 'Subnet for router interface must have a gateway IP'
+                raise q_exc.BadRequest(resource='router', msg=msg)
+            self._check_for_dup_router_subnet(context, router_id,
+                                              subnet['network_id'], subnet_id)
+            fixed_ip = {'ip_address': subnet['gateway_ip'],
+                        'subnet_id': subnet['id']}
+            port = self.create_port(context, {
+                'port':
+                {'network_id': subnet['network_id'],
+                 'fixed_ips': [fixed_ip],
+                 'mac_address': attributes.ATTR_NOT_SPECIFIED,
+                 'admin_state_up': True,
+                 'device_id': router_id,
+                 'device_owner': DEVICE_OWNER_ROUTER_INTF,
+                 'name': ''}})
+        return {'port_id': port['id'],
+                'subnet_id': port['fixed_ips'][0]['subnet_id']}
+
+    def remove_router_interface(self, context, router_id, interface_info):
+        # make sure router exists
+        router = self._get_router(context, router_id)
+
+        if not interface_info:
+            msg = "Either subnet_id or port_id must be specified"
+            raise q_exc.BadRequest(resource='router', msg=msg)
+        if 'port_id' in interface_info:
+            port_db = self._get_port(context, interface_info['port_id'])
+            if 'subnet_id' in interface_info:
+                port_subnet_id = port_db['fixed_ips'][0]['subnet_id']
+                if port_subnet_id != interface_info['subnet_id']:
+                    raise w_exc.HTTPConflict("subnet_id %s on port does not "
+                                             "match requested one (%s)"
+                                             % (port_subnet_id,
+                                                interface_info['subnet_id']))
+            if port_db['device_id'] != router_id:
+                raise w_exc.HTTPConflict("port_id %s not used by router" %
+                                         port_db['id'])
+            self.delete_port(context, port_db['id'])
+        elif 'subnet_id' in interface_info:
+            subnet_id = interface_info['subnet_id']
+            subnet = self._get_subnet(context, subnet_id)
+            found = False
+
+            try:
+                rport_qry = context.session.query(models_v2.Port)
+                ports = rport_qry.filter_by(
+                    device_id=router_id,
+                    device_owner=DEVICE_OWNER_ROUTER_INTF,
+                    network_id=subnet['network_id']).all()
+
+                for p in ports:
+                    if p['fixed_ips'][0]['subnet_id'] == subnet_id:
+                        self.delete_port(context, p['id'])
+                        found = True
+                        break
+            except exc.NoResultFound:
+                pass
+
+            if not found:
+                raise w_exc.HTTPNotFound("Router %(router_id)s has no "
+                                         "interface on subnet %(subnet_id)s"
+                                         % locals())
+
+    def _get_floatingip(self, context, id, verbose=None):
+        try:
+            floatingip = self._get_by_id(context, FloatingIP, id,
+                                         verbose=verbose)
+        except exc.NoResultFound:
+            raise l3.FloatingIPNotFound(floatingip_id=id)
+        except exc.MultipleResultsFound:
+            LOG.error('Multiple floating ips match for %s' % id)
+            raise l3.FloatingIPNotFound(floatingip_id=id)
+        return floatingip
+
+    def _make_floatingip_dict(self, floatingip, fields=None):
+        res = {'id': floatingip['id'],
+               'tenant_id': floatingip['tenant_id'],
+               'floating_ip_address': floatingip['floating_ip_address'],
+               'floating_network_id': floatingip['floating_network_id'],
+               'router_id': floatingip['router_id'],
+               'port_id': floatingip['fixed_port_id'],
+               'fixed_ip_address': floatingip['fixed_ip_address']}
+        return self._fields(res, fields)
+
+    def _get_router_for_internal_subnet(self, context, internal_port,
+                                        internal_subnet_id):
+        subnet_db = self._get_subnet(context, internal_subnet_id)
+        if not subnet_db['gateway_ip']:
+            msg = ('Cannot add floating IP to port on subnet %s '
+                   'which has no gateway_ip' % internal_subnet_id)
+            raise q_exc.BadRequest(resource='floatingip', msg=msg)
+
+        #FIXME(danwent): can do join, but cannot use standard F-K syntax?
+        # just do it inefficiently for now
+        port_qry = context.session.query(models_v2.Port)
+        ports = port_qry.filter_by(network_id=internal_port['network_id'])
+        for p in ports:
+            ips = [ip['ip_address'] for ip in p['fixed_ips']]
+            if len(ips) != 1:
+                continue
+            fixed = p['fixed_ips'][0]
+            if (fixed['ip_address'] == subnet_db['gateway_ip'] and
+                    fixed['subnet_id'] == internal_subnet_id):
+                router_qry = context.session.query(Router)
+                try:
+                    router = router_qry.filter_by(id=p['device_id']).one()
+                    #TODO(danwent): confirm that this router has a floating
+                    # ip enabled gateway with support for this floating IP
+                    # network
+                    return router['id']
+                except exc.NoResultFound:
+                    pass
+
+        raise l3.ExternalGatewayForFloatingIPNotFound(
+            subnet_id=internal_subnet_id,
+            port_id=internal_port['id'])
+
+    def get_assoc_data(self, context, fip):
+        """When a floating IP is associated with an internal port,
+        we need to extract/determine some data associated with the
+        internal port, including the internal_ip_address, and router_id.
+        We also need to confirm that this internal port is owned by the
+        tenant who owns the floating IP.
+        """
+        internal_port = self._get_port(context, fip['port_id'])
+        if not internal_port['tenant_id'] == fip['tenant_id']:
+            msg = ('Port %s is associated with a different tenant'
+                   'and therefore cannot be found to floating IP %s'
+                   % (fip['port_id'], fip['id']))
+            raise q_exc.BadRequest(resource='floating', msg=msg)
+
+        internal_subnet_id = None
+        if 'fixed_ip_address' in fip and fip['fixed_ip_address']:
+            internal_ip_address = fip['fixed_ip_address']
+            for ip in internal_port['fixed_ips']:
+                if ip['ip_address'] == internal_ip_address:
+                    internal_subnet_id = ip['subnet_id']
+            if not internal_subnet_id:
+                msg = ('Port %s does not have fixed ip %s' %
+                       (internal_port['id'], internal_ip_address))
+                raise q_exc.BadRequest(resource='floatingip', msg=msg)
+        else:
+            ips = [ip['ip_address'] for ip in internal_port['fixed_ips']]
+            if len(ips) == 0:
+                msg = ('Cannot add floating IP to port %s that has'
+                       'no fixed IP addresses' % internal_port['id'])
+                raise q_exc.BadRequest(resource='floatingip', msg=msg)
+            if len(ips) > 1:
+                msg = ('Port %s has multiple fixed IPs.  Must provide'
+                       ' a specific IP when assigning a floating IP' %
+                       internal_port['id'])
+                raise q_exc.BadRequest(resource='floatingip', msg=msg)
+            internal_ip_address = internal_port['fixed_ips'][0]['ip_address']
+            internal_subnet_id = internal_port['fixed_ips'][0]['subnet_id']
+
+        router_id = self._get_router_for_internal_subnet(context,
+                                                         internal_port,
+                                                         internal_subnet_id)
+        return (fip['port_id'], internal_ip_address, router_id)
+
+    def _update_fip_assoc(self, context, fip, floatingip_db, external_port):
+        port_id = internal_ip_address = router_id = None
+        if 'port_id' in fip and fip['port_id']:
+            port_qry = context.session.query(FloatingIP)
+            try:
+                port_qry.filter_by(fixed_port_id=fip['port_id']).one()
+                raise l3.FloatingIPPortAlreadyAssociated(
+                    port_id=fip['port_id'])
+            except exc.NoResultFound:
+                pass
+            port_id, internal_ip_address, router_id = self.get_assoc_data(
+                context,
+                fip)
+            # Assign external address for floating IP
+            # fetch external gateway port
+            ports = self.get_ports(context, filters={'device_id': [router_id]})
+            if not ports:
+                msg = ("The router %s needed for association a floating ip "
+                       "to port %s does not have an external gateway"
+                       % (router_id, port_id))
+                raise q_exc.BadRequest(resource='floatingip', msg=msg)
+            # retrieve external subnet identifier
+            # NOTE: by design we cannot have more than 1 IP on ext gw port
+            ext_subnet_id = ports[0]['fixed_ips'][0]['subnet_id']
+            # ensure floating ip address is taken from this subnet
+            for fixed_ip in external_port['fixed_ips']:
+                if fixed_ip['subnet_id'] == ext_subnet_id:
+                    floatingip_db.update(
+                        {'floating_ip_address': fixed_ip['ip_address'],
+                         'floating_port_id': external_port['id']})
+        else:
+            # fallback choice (first IP address on external port)
+            floatingip_db.update(
+                {'floating_ip_address':
+                    external_port['fixed_ips'][0]['ip_address'],
+                 'floating_port_id':
+                    external_port['id']})
+
+        floatingip_db.update({'fixed_ip_address': internal_ip_address,
+                              'fixed_port_id': port_id,
+                              'router_id': router_id})
+
+    def create_floatingip(self, context, floatingip):
+        fip = floatingip['floatingip']
+        tenant_id = self._get_tenant_id_for_create(context, fip)
+        fip_id = utils.str_uuid()
+
+        #TODO(danwent): validate that network_id is valid floatingip-network
+
+        # This external port is never exposed to the tenant.
+        # it is used purely for internal system and admin use when
+        # managing floating IPs.
+        external_port = self.create_port(context, {
+            'port':
+            {'network_id': fip['floating_network_id'],
+             'mac_address': attributes.ATTR_NOT_SPECIFIED,
+             'fixed_ips': attributes.ATTR_NOT_SPECIFIED,
+             'admin_state_up': True,
+             'device_id': fip_id,
+             'device_owner': DEVICE_OWNER_FLOATINGIP,
+             'name': ''}})
+        # Ensure IP addresses are allocated on external port
+        if not external_port['fixed_ips']:
+            msg = "Unable to find any IP address on external network"
+            # remove the external port
+            self.delete_port(context, external_port['id'])
+            raise q_exc.BadRequest(resource='floatingip', msg=msg)
+
+        try:
+            with context.session.begin(subtransactions=True):
+                floatingip_db = FloatingIP(
+                    id=fip_id,
+                    tenant_id=tenant_id,
+                    floating_network_id=fip['floating_network_id'])
+                fip['tenant_id'] = tenant_id
+                # Update association with internal port
+                # and define external IP address
+                self._update_fip_assoc(context, fip,
+                                       floatingip_db, external_port)
+                context.session.add(floatingip_db)
+        # TODO(salvatore-orlando): Avoid broad catch
+        # Maybe by introducing base class for L3 exceptions
+        except Exception:
+            LOG.exception("Floating IP association failed")
+            # Remove the port created for internal purposes
+            self.delete_port(context, external_port['id'])
+            raise
+
+        return self._make_floatingip_dict(floatingip_db)
+
+    def update_floatingip(self, context, id, floatingip):
+        fip = floatingip['floatingip']
+        with context.session.begin(subtransactions=True):
+            floatingip_db = self._get_floatingip(context, id)
+            fip['tenant_id'] = floatingip_db['tenant_id']
+            fip['id'] = id
+            fip_port_id = floatingip_db['floating_port_id']
+            self._update_fip_assoc(context, fip, floatingip_db,
+                                   self.get_port(context, fip_port_id))
+        return self._make_floatingip_dict(floatingip_db)
+
+    def delete_floatingip(self, context, id):
+        floatingip = self._get_floatingip(context, id)
+        with context.session.begin(subtransactions=True):
+            context.session.delete(floatingip)
+        self.delete_port(context, floatingip['floating_port_id'])
+
+    def get_floatingip(self, context, id, fields=None, verbose=None):
+        floatingip = self._get_floatingip(context, id, verbose=verbose)
+        return self._make_floatingip_dict(floatingip, fields)
+
+    def get_floatingips(self, context, filters=None, fields=None,
+                        verbose=None):
+        return self._get_collection(context, FloatingIP,
+                                    self._make_floatingip_dict,
+                                    filters=filters, fields=fields,
+                                    verbose=verbose)
+
+    def disassociate_floatingips(self, context, port_id):
+        with context.session.begin(subtransactions=True):
+            try:
+                fip_qry = context.session.query(FloatingIP)
+                floating_ip = fip_qry.filter_by(fixed_port_id=port_id).one()
+                floating_ip.update({'fixed_port_id': None,
+                                    'fixed_ip_address': None,
+                                    'router_id': None})
+            except exc.NoResultFound:
+                return
+            except exc.MultipleResultsFound:
+                # should never happen
+                raise Exception('Multiple floating IPs found for port %s'
+                                % port_id)
diff --git a/quantum/extensions/l3.py b/quantum/extensions/l3.py
new file mode 100644 (file)
index 0000000..1ee5e82
--- /dev/null
@@ -0,0 +1,210 @@
+"""
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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.
+#
+# @author: Dan Wendlandt, Nicira, Inc
+#
+"""
+
+from abc import abstractmethod
+
+from quantum.api.v2 import attributes as attr
+from quantum.api.v2 import base
+from quantum.common import exceptions as qexception
+from quantum.extensions import extensions
+from quantum import manager
+from quantum.openstack.common import cfg
+from quantum import quota
+
+
+# L3 Exceptions
+class RouterNotFound(qexception.NotFound):
+    message = _("Router %(router_id)s could not be found")
+
+
+class RouterInUse(qexception.InUse):
+    message = _("Router %(router_id)s still has active ports")
+
+
+class FloatingIPNotFound(qexception.NotFound):
+    message = _("Floating IP %(floatingip_id)s could not be found")
+
+
+class ExternalGatewayForFloatingIPNotFound(qexception.NotFound):
+    message = _("Could not find an external network gateway reachable "
+                "from subnet %(subnet_id)s.  Therefore, cannot associate "
+                "Port %(port_id)s with a Floating IP.")
+
+
+class FloatingIPPortAlreadyAssociated(qexception.InUse):
+    message = _("Port %(port_id) already has a floating IP associated with it")
+
+
+# Attribute Map
+RESOURCE_ATTRIBUTE_MAP = {
+    'routers': {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:regex': attr.UUID_PATTERN},
+               'is_visible': True},
+        'name': {'allow_post': True, 'allow_put': True,
+                 'is_visible': True, 'default': ''},
+        'admin_state_up': {'allow_post': True, 'allow_put': True,
+                           'default': True,
+                           'convert_to': attr.convert_to_boolean,
+                           'validate': {'type:boolean': None},
+                           'is_visible': True},
+        'status': {'allow_post': False, 'allow_put': False,
+                   'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'required_by_policy': True,
+                      'is_visible': True},
+        'external_gateway_info': {'allow_post': True, 'allow_put': True,
+                                  'is_visible': True, 'default': None}
+    },
+    'floatingips': {
+        'id': {'allow_post': False, 'allow_put': False,
+               'is_visible': True},
+        'floating_ip_address': {'allow_post': False, 'allow_put': False,
+                                'is_visible': True},
+        'floating_network_id': {'allow_post': True, 'allow_put': False,
+                                'is_visible': True},
+        'router_id': {'allow_post': False, 'allow_put': False,
+                      'is_visible': True, 'default': None},
+        'port_id': {'allow_post': True, 'allow_put': True,
+                    'is_visible': True, 'default': None},
+        'fixed_ip_address': {'allow_post': True, 'allow_put': True,
+                             'is_visible': True, 'default': None},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'required_by_policy': True,
+                      'is_visible': True}
+    },
+}
+
+l3_quota_opts = [
+    cfg.IntOpt('quota_router',
+               default=10,
+               help='number of routers allowed per tenant, -1 for unlimited'),
+    cfg.IntOpt('quota_floatingip',
+               default=50,
+               help='number of floating IPs allowed per tenant, '
+                    '-1 for unlimited'),
+]
+cfg.CONF.register_opts(l3_quota_opts, 'QUOTAS')
+
+
+class L3(object):
+
+    @classmethod
+    def get_name(cls):
+        return "Quantum Router"
+
+    @classmethod
+    def get_alias(cls):
+        return "os-quantum-router"
+
+    @classmethod
+    def get_description(cls):
+        return ("Router abstraction for basic L3 forwarding"
+                " between L2 Quantum networks and access to external"
+                " networks via a NAT gateway.")
+
+    @classmethod
+    def get_namespace(cls):
+        return "http://docs.openstack.org/ext/os-quantum-router/api/v1.0"
+
+    @classmethod
+    def get_updated(cls):
+        return "2012-07-20T10:00:00-00:00"
+
+    @classmethod
+    def get_resources(cls):
+        """ Returns Ext Resources """
+        exts = []
+        plugin = manager.QuantumManager.get_plugin()
+        for resource_name in ['router', 'floatingip']:
+            collection_name = resource_name + "s"
+            params = RESOURCE_ATTRIBUTE_MAP.get(collection_name, dict())
+
+            member_actions = {}
+            if resource_name == 'router':
+                member_actions = {'add_router_interface': 'PUT',
+                                  'remove_router_interface': 'PUT'}
+
+            quota.QUOTAS.register_resource_by_name(resource_name)
+
+            controller = base.create_resource(collection_name,
+                                              resource_name,
+                                              plugin, params,
+                                              member_actions=member_actions)
+
+            ex = extensions.ResourceExtension(collection_name,
+                                              controller,
+                                              member_actions=member_actions)
+            exts.append(ex)
+
+        return exts
+
+
+class RouterPluginBase(object):
+
+    @abstractmethod
+    def create_router(self, context, router):
+        pass
+
+    @abstractmethod
+    def update_router(self, context, id, router):
+        pass
+
+    @abstractmethod
+    def get_router(self, context, id, fields=None, verbose=None):
+        pass
+
+    @abstractmethod
+    def delete_router(self, context, id):
+        pass
+
+    @abstractmethod
+    def get_routers(self, context, filters=None, fields=None, verbose=None):
+        pass
+
+    @abstractmethod
+    def add_router_interface(self, context, router_id, interface_info):
+        pass
+
+    @abstractmethod
+    def remove_router_interface(self, context, router_id, interface_info):
+        pass
+
+    @abstractmethod
+    def create_floatingip(self, context, floatingip):
+        pass
+
+    @abstractmethod
+    def update_floatingip(self, context, id, floatingip):
+        pass
+
+    @abstractmethod
+    def get_floatingip(self, context, id, fields=None, verbose=None):
+        pass
+
+    @abstractmethod
+    def delete_floatingip(self, context, id):
+        pass
+
+    @abstractmethod
+    def get_floatingips(self, context, filters=None, fields=None,
+                        verbose=None):
+        pass
index 28413721949dcf66b964a738b560cec43768ae3b..5aab5015e9688ecd362ae9b1badf06938973ca0c 100644 (file)
@@ -213,7 +213,7 @@ def port_set_attachment(net_id, port_id, new_interface_id):
         # We are setting, not clearing, the attachment-id
         if port['interface_id']:
             raise q_exc.PortInUse(net_id=net_id, port_id=port_id,
-                                  att_id=port['interface_id'])
+                                  device_id=port['interface_id'])
 
         try:
             port = (session.query(models.Port).
@@ -256,7 +256,7 @@ def port_destroy(net_id, port_id):
                 one())
         if port['interface_id']:
             raise q_exc.PortInUse(net_id=net_id, port_id=port_id,
-                                  att_id=port['interface_id'])
+                                  device_id=port['interface_id'])
         session.delete(port)
         session.flush()
         return port
@@ -281,7 +281,7 @@ def port_set_attachment_by_id(port_id, new_interface_id):
     if new_interface_id != "":
         if port['interface_id']:
             raise q_exc.PortInUse(port_id=port_id,
-                                  att_id=port['interface_id'])
+                                  device_id=port['interface_id'])
 
         try:
             port = session.query(models.Port).filter_by(
index 722d7d8b04848c0927051c2236236c18eb4313a9..2e966b21d7c6008cc65f0fc591d913cad35c0f3e 100644 (file)
@@ -30,6 +30,7 @@ from quantum.common import topics
 from quantum.db import api as db
 from quantum.db import db_base_plugin_v2
 from quantum.db import dhcp_rpc_base
+from quantum.db import l3_db
 from quantum.db import models_v2
 from quantum.openstack.common import context
 from quantum.openstack.common import cfg
@@ -162,7 +163,8 @@ class AgentNotifierApi(proxy.RpcProxy):
                          topic=self.topic_tunnel_update)
 
 
-class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
+class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
+                         l3_db.L3_NAT_db_mixin):
     """Implement the Quantum abstractions using Open vSwitch.
 
     Depending on whether tunneling is enabled, either a GRE tunnel or
@@ -181,7 +183,7 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
     # bulk operations. Name mangling is used in order to ensure it
     # is qualified by class
     __native_bulk_support = True
-    supported_extension_aliases = ["provider"]
+    supported_extension_aliases = ["provider", "os-quantum-router"]
 
     def __init__(self, configfile=None):
         self.enable_tunneling = cfg.CONF.OVS.enable_tunneling
@@ -361,3 +363,7 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
                 vlan_id = ovs_db_v2.get_vlan(port['network_id'])
                 self.notifier.port_update(self.context, port, vlan_id)
         return port
+
+    def delete_port(self, context, id):
+        self.disassociate_floatingips(context, id)
+        return super(OVSQuantumPluginV2, self).delete_port(context, id)
index 055e7eec2f11d7e5e296b05136666560e83c950a..2eff401e28ea1e2adf95c97e571b79d9e2ffbee3 100644 (file)
@@ -50,12 +50,15 @@ def etcdir(*p):
     return os.path.join(ETCDIR, *p)
 
 
-def _get_path(resource, id=None, fmt=None):
+def _get_path(resource, id=None, action=None, fmt=None):
     path = '/%s' % resource
 
     if id is not None:
         path = path + '/%s' % id
 
+    if action is not None:
+        path = path + '/%s' % action
+
     if fmt is not None:
         path = path + '.%s' % fmt
 
diff --git a/quantum/tests/unit/test_l3_agent.py b/quantum/tests/unit/test_l3_agent.py
new file mode 100644 (file)
index 0000000..57efc2f
--- /dev/null
@@ -0,0 +1,264 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Nicira, 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 time
+import unittest
+
+import mock
+
+from quantum.agent.common import config
+from quantum.agent import l3_agent
+from quantum.agent.linux import interface
+from quantum.db import l3_db
+from quantum.tests.unit import test_api_v2
+
+_uuid = test_api_v2._uuid
+
+
+class TestBasicRouterOperations(unittest.TestCase):
+
+    def setUp(self):
+        self.conf = config.setup_conf()
+        self.conf.register_opts(l3_agent.L3NATAgent.OPTS)
+        self.conf.register_opts(interface.OPTS)
+        self.conf.set_override('interface_driver',
+                               'quantum.agent.linux.interface.NullDriver')
+        self.conf.root_helper = 'sudo'
+
+        self.device_exists_p = mock.patch(
+            'quantum.agent.linux.ip_lib.device_exists')
+        self.device_exists = self.device_exists_p.start()
+
+        self.utils_exec_p = mock.patch(
+            'quantum.agent.linux.utils.execute')
+        self.utils_exec = self.utils_exec_p.start()
+
+        self.dvr_cls_p = mock.patch('quantum.agent.linux.interface.NullDriver')
+        driver_cls = self.dvr_cls_p.start()
+        self.mock_driver = mock.MagicMock()
+        self.mock_driver.DEV_NAME_LEN = (
+            interface.LinuxInterfaceDriver.DEV_NAME_LEN)
+        driver_cls.return_value = self.mock_driver
+
+        self.ip_cls_p = mock.patch('quantum.agent.linux.ip_lib.IPWrapper')
+        ip_cls = self.ip_cls_p.start()
+        self.mock_ip = mock.MagicMock()
+        ip_cls.return_value = self.mock_ip
+
+        self.client_cls_p = mock.patch('quantumclient.v2_0.client.Client')
+        client_cls = self.client_cls_p.start()
+        self.client_inst = mock.Mock()
+        client_cls.return_value = self.client_inst
+
+    def tearDown(self):
+        self.device_exists_p.stop()
+        self.client_cls_p.stop()
+        self.ip_cls_p.stop()
+        self.dvr_cls_p.stop()
+        self.utils_exec_p.stop()
+
+    def testRouterInfoCreate(self):
+        id = _uuid()
+        ri = l3_agent.RouterInfo(id, self.conf.root_helper)
+
+        self.assertTrue(ri.ns_name().endswith(id))
+
+    def testAgentCreate(self):
+        agent = l3_agent.L3NATAgent(self.conf)
+
+        # calls to disable/enable routing
+        self.utils_exec.assert_has_calls([
+            mock.call(mock.ANY, self.conf.root_helper,
+                      check_exit_code=mock.ANY),
+            mock.call(mock.ANY, self.conf.root_helper,
+                      check_exit_code=mock.ANY)])
+
+        self.device_exists.assert_has_calls(
+            [mock.call(self.conf.external_network_bridge)])
+
+    def _test_internal_network_action(self, action):
+        port_id = _uuid()
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper)
+        agent = l3_agent.L3NATAgent(self.conf)
+        interface_name = agent.get_internal_device_name(port_id)
+        cidr = '99.0.1.9/24'
+        mac = 'ca:fe:de:ad:be:ef'
+        ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30'}]}
+
+        if action == 'add':
+            self.device_exists.return_value = False
+            agent.internal_network_added(ri, ex_gw_port, port_id, cidr, mac)
+            self.assertEquals(self.mock_driver.plug.call_count, 1)
+            self.assertEquals(self.mock_driver.init_l3.call_count, 1)
+        elif action == 'remove':
+            self.device_exists.return_value = True
+            agent.internal_network_removed(ri, ex_gw_port, port_id, cidr)
+            self.assertEquals(self.mock_driver.unplug.call_count, 1)
+        else:
+            raise Exception("Invalid action %s" % action)
+
+    def testAgentAddInternalNetwork(self):
+        self._test_internal_network_action('add')
+
+    def testAgentRemoveInternalNetwork(self):
+        self._test_internal_network_action('remove')
+
+    def _test_external_gateway_action(self, action):
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper)
+        agent = l3_agent.L3NATAgent(self.conf)
+        internal_cidrs = ['100.0.1.0/24', '200.74.0.0/16']
+        ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30',
+                                     'subnet_id': _uuid()}],
+                      'subnet': {'gateway_ip': '20.0.0.1'},
+                      'id': _uuid(),
+                      'mac_address': 'ca:fe:de:ad:be:ef',
+                      'ip_cidr': '20.0.0.30/24'}
+
+        if action == 'add':
+            self.device_exists.return_value = False
+            agent.external_gateway_added(ri, ex_gw_port, internal_cidrs)
+            self.assertEquals(self.mock_driver.plug.call_count, 1)
+            self.assertEquals(self.mock_driver.init_l3.call_count, 1)
+            self.assertEquals(self.mock_ip.netns.execute.call_count, 1)
+
+        elif action == 'remove':
+            self.device_exists.return_value = True
+            agent.external_gateway_removed(ri, ex_gw_port, internal_cidrs)
+            self.assertEquals(self.mock_driver.unplug.call_count, 1)
+        else:
+            raise Exception("Invalid action %s" % action)
+
+    def testAgentAddExternalGateway(self):
+        self._test_external_gateway_action('add')
+
+    def testAgentRemoveExternalGateway(self):
+        self._test_external_gateway_action('remove')
+
+    def _test_floating_ip_action(self, action):
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper)
+        agent = l3_agent.L3NATAgent(self.conf)
+        floating_ip = '20.0.0.100'
+        fixed_ip = '10.0.0.23'
+        ex_gw_port = {'fixed_ips': [{'ip_address': '20.0.0.30',
+                                     'subnet_id': _uuid()}],
+                      'subnet': {'gateway_ip': '20.0.0.1'},
+                      'id': _uuid(),
+                      'mac_address': 'ca:fe:de:ad:be:ef',
+                      'ip_cidr': '20.0.0.30/24'}
+
+        if action == 'add':
+            self.device_exists.return_value = False
+            agent.floating_ip_added(ri, ex_gw_port, floating_ip, fixed_ip)
+
+        elif action == 'remove':
+            self.device_exists.return_value = True
+            agent.floating_ip_removed(ri, ex_gw_port, floating_ip, fixed_ip)
+        else:
+            raise Exception("Invalid action %s" % action)
+
+    def testAgentAddFloatingIP(self):
+        self._test_floating_ip_action('add')
+
+    def testAgentRemoveFloatingIP(self):
+        self._test_floating_ip_action('remove')
+
+    def testProcessRouter(self):
+
+        agent = l3_agent.L3NATAgent(self.conf)
+        router_id = _uuid()
+        ri = l3_agent.RouterInfo(router_id, self.conf.root_helper)
+
+        # return data so that state is built up
+        ex_gw_port = {'id': _uuid(),
+                      'fixed_ips': [{'ip_address': '19.4.4.4',
+                                     'subnet_id': _uuid()}]}
+        internal_port = {'id': _uuid(),
+                         'fixed_ips': [{'ip_address': '35.4.4.4',
+                                        'subnet_id': _uuid()}],
+                         'mac_address': 'ca:fe:de:ad:be:ef'}
+
+        def fake_list_ports1(**kwargs):
+            if kwargs['device_owner'] == l3_db.DEVICE_OWNER_ROUTER_GW:
+                return {'ports': [ex_gw_port]}
+            elif kwargs['device_owner'] == l3_db.DEVICE_OWNER_ROUTER_INTF:
+                return {'ports': [internal_port]}
+
+        fake_subnet = {'subnet': {'cidr': '19.4.4.0/24',
+                                  'gateway_ip': '19.4.4.1'}}
+
+        fake_floatingips = {'floatingips': [
+            {'id': _uuid(),
+             'floating_ip_address': '8.8.8.8',
+             'fixed_ip_address': '7.7.7.7',
+             'port_id': _uuid()}]}
+
+        self.client_inst.list_ports.side_effect = fake_list_ports1
+        self.client_inst.show_subnet.return_value = fake_subnet
+        self.client_inst.list_floatingips.return_value = fake_floatingips
+        agent.process_router(ri)
+
+        # remove just the floating ips
+        self.client_inst.list_floatingips.return_value = {'floatingips': []}
+        agent.process_router(ri)
+
+        # now return no ports so state is torn down
+        self.client_inst.list_ports.return_value = {'ports': []}
+        agent.process_router(ri)
+
+    def testDaemonLoop(self):
+
+        # just take a pass through the loop, then raise on time.sleep()
+        time_sleep_p = mock.patch('time.sleep')
+        time_sleep = time_sleep_p.start()
+
+        class ExpectedException(Exception):
+            pass
+
+        time_sleep.side_effect = ExpectedException()
+        self.client_inst.list_routers.return_value = {'routers':
+                                                      [{'id': _uuid()}]}
+
+        agent = l3_agent.L3NATAgent(self.conf)
+        self.assertRaises(ExpectedException, agent.daemon_loop)
+
+        time_sleep_p.stop()
+
+    def testDestroyNamespace(self):
+
+        class FakeDev(object):
+            def __init__(self, name):
+                self.name = name
+
+        self.mock_ip.get_namespaces.return_value = ['qrouter-foo']
+        self.mock_ip.get_devices.return_value = [FakeDev('qr-aaaa'),
+                                                 FakeDev('qgw-aaaa')]
+
+        agent = l3_agent.L3NATAgent(self.conf)
+        agent._destroy_router_namespaces()
+
+    def testMain(self):
+        agent_mock_p = mock.patch('quantum.agent.l3_agent.L3NATAgent')
+        agent_mock = agent_mock_p.start()
+        agent_mock.daemon_loop.return_value = None
+
+        with mock.patch('quantum.agent.l3_agent.sys') as mock_sys:
+            mock_sys.argv = []
+            l3_agent.main()
+
+        agent_mock_p.stop()
diff --git a/quantum/tests/unit/test_l3_plugin.py b/quantum/tests/unit/test_l3_plugin.py
new file mode 100644 (file)
index 0000000..8cd9fe3
--- /dev/null
@@ -0,0 +1,642 @@
+"""
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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.
+#
+# @author: Dan Wendlandt, Nicira, Inc
+#
+"""
+
+import contextlib
+import copy
+import logging
+import unittest
+
+import mock
+import webtest
+from webob import exc
+
+from quantum.api.v2 import attributes
+from quantum.common import config
+from quantum.common.test_lib import test_config
+from quantum.db import db_base_plugin_v2
+from quantum.db import l3_db
+from quantum.extensions import extensions
+from quantum.extensions import l3
+from quantum import manager
+from quantum.openstack.common import cfg
+from quantum.tests.unit import test_api_v2
+from quantum.tests.unit import test_extensions
+from quantum.tests.unit import test_db_plugin
+
+LOG = logging.getLogger(__name__)
+
+_uuid = test_api_v2._uuid
+_get_path = test_api_v2._get_path
+
+
+class L3TestExtensionManager(object):
+
+    def get_resources(self):
+        return l3.L3.get_resources()
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
+
+
+class L3NatExtensionTestCase(unittest.TestCase):
+
+    def setUp(self):
+
+        plugin = 'quantum.extensions.l3.RouterPluginBase'
+
+        # Ensure 'stale' patched copies of the plugin are never returned
+        manager.QuantumManager._instance = None
+
+        # Ensure existing ExtensionManager is not used
+        extensions.PluginAwareExtensionManager._instance = None
+
+        # Save the global RESOURCE_ATTRIBUTE_MAP
+        self.saved_attr_map = {}
+        for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems():
+            self.saved_attr_map[resource] = attrs.copy()
+
+        # 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 the os-quantum-router  extension
+        manager.QuantumManager.get_plugin().supported_extension_aliases = (
+            ["os-quantum-router"])
+
+        ext_mgr = L3TestExtensionManager()
+        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()
+
+        # Restore the global RESOURCE_ATTRIBUTE_MAP
+        attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map
+
+    def test_router_create(self):
+        router_id = _uuid()
+        data = {'router': {'name': 'router1', 'admin_state_up': True,
+                           'tenant_id': _uuid(),
+                           'external_gateway_info': None}}
+        return_value = copy.deepcopy(data['router'])
+        return_value.update({'status': "ACTIVE", 'id': router_id})
+
+        instance = self.plugin.return_value
+        instance.create_router.return_value = return_value
+
+        res = self.api.post_json(_get_path('routers'), data)
+
+        instance.create_router.assert_called_with(mock.ANY,
+                                                  router=data)
+        self.assertEqual(res.status_int, exc.HTTPCreated.code)
+        self.assertTrue('router' in res.json)
+        router = res.json['router']
+        self.assertEqual(router['id'], router_id)
+        self.assertEqual(router['status'], "ACTIVE")
+        self.assertEqual(router['admin_state_up'], True)
+
+    def test_router_list(self):
+        router_id = _uuid()
+        return_value = [{'router': {'name': 'router1', 'admin_state_up': True,
+                                    'tenant_id': _uuid(), 'id': router_id}}]
+
+        instance = self.plugin.return_value
+        instance.get_routers.return_value = return_value
+
+        res = self.api.get(_get_path('routers'))
+
+        instance.get_routers.assert_called_with(mock.ANY, fields=mock.ANY,
+                                                verbose=mock.ANY,
+                                                filters=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+
+    def test_router_update(self):
+        router_id = _uuid()
+        update_data = {'router': {'admin_state_up': False}}
+        return_value = {'name': 'router1', 'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE", 'id': router_id}
+
+        instance = self.plugin.return_value
+        instance.update_router.return_value = return_value
+
+        res = self.api.put_json(_get_path('routers', id=router_id),
+                                update_data)
+
+        instance.update_router.assert_called_with(mock.ANY, router_id,
+                                                  router=update_data)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('router' in res.json)
+        router = res.json['router']
+        self.assertEqual(router['id'], router_id)
+        self.assertEqual(router['status'], "ACTIVE")
+        self.assertEqual(router['admin_state_up'], False)
+
+    def test_router_get(self):
+        router_id = _uuid()
+        return_value = {'name': 'router1', 'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE", 'id': router_id}
+
+        instance = self.plugin.return_value
+        instance.get_router.return_value = return_value
+
+        res = self.api.get(_get_path('routers', id=router_id))
+
+        instance.get_router.assert_called_with(mock.ANY, router_id,
+                                               fields=mock.ANY,
+                                               verbose=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('router' in res.json)
+        router = res.json['router']
+        self.assertEqual(router['id'], router_id)
+        self.assertEqual(router['status'], "ACTIVE")
+        self.assertEqual(router['admin_state_up'], False)
+
+    def test_router_delete(self):
+        router_id = _uuid()
+
+        res = self.api.delete(_get_path('routers', id=router_id))
+
+        instance = self.plugin.return_value
+        instance.delete_router.assert_called_with(mock.ANY, router_id)
+        self.assertEqual(res.status_int, exc.HTTPNoContent.code)
+
+    def test_router_add_interface(self):
+        router_id = _uuid()
+        subnet_id = _uuid()
+        port_id = _uuid()
+
+        interface_data = {'subnet_id': subnet_id}
+        return_value = copy.deepcopy(interface_data)
+        return_value['port_id'] = port_id
+
+        instance = self.plugin.return_value
+        instance.add_router_interface.return_value = return_value
+
+        path = _get_path('routers', id=router_id,
+                         action="add_router_interface")
+        res = self.api.put_json(path, interface_data)
+
+        instance.add_router_interface.assert_called_with(mock.ANY, router_id,
+                                                         interface_data)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('port_id' in res.json)
+        self.assertEqual(res.json['port_id'], port_id)
+        self.assertEqual(res.json['subnet_id'], subnet_id)
+
+
+# This plugin class is just for testing
+class TestL3NatPlugin(db_base_plugin_v2.QuantumDbPluginV2,
+                      l3_db.L3_NAT_db_mixin):
+    supported_extension_aliases = ["os-quantum-router"]
+
+    def delete_port(self, context, id):
+        self.disassociate_floatingips(context, id)
+        return super(TestL3NatPlugin, self).delete_port(context, id)
+
+
+class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
+
+    def setUp(self):
+        test_config['plugin_name_v2'] = (
+            'quantum.tests.unit.test_l3_plugin.TestL3NatPlugin')
+        ext_mgr = L3TestExtensionManager()
+        test_config['extension_manager'] = ext_mgr
+        super(L3NatDBTestCase, self).setUp()
+
+    def _create_router(self, fmt, tenant_id, name=None, admin_state_up=None):
+        data = {'router': {'tenant_id': tenant_id}}
+        if name:
+            data['router']['name'] = name
+        if admin_state_up:
+            data['router']['admin_state_up'] = admin_state_up
+        router_req = self.new_create_request('routers', data, fmt)
+        return router_req.get_response(self.ext_api)
+
+    def _add_external_gateway_to_router(self, router_id, network_id,
+                                        expected_code=exc.HTTPOk.code):
+        return self._update('routers', router_id,
+                            {'router': {'external_gateway_info':
+                                        {'network_id': network_id}}},
+                            expected_code=expected_code)
+
+    def _remove_external_gateway_from_router(self, router_id, network_id,
+                                             expected_code=exc.HTTPOk.code):
+        return self._update('routers', router_id,
+                            {'router': {'external_gateway_info':
+                                       {}}},
+                            expected_code=expected_code)
+
+    def _router_interface_action(self, action, router_id, subnet_id, port_id,
+                                 expected_code=exc.HTTPOk.code):
+        interface_data = {}
+        if subnet_id:
+            interface_data.update({'subnet_id': subnet_id})
+        if port_id and (action != 'add' or not subnet_id):
+            interface_data.update({'port_id': port_id})
+
+        req = self.new_action_request('routers', interface_data, router_id,
+                                      "%s_router_interface" % action)
+        res = req.get_response(self.ext_api)
+        self.assertEqual(res.status_int, expected_code)
+        return self.deserialize('json', res)
+
+    @contextlib.contextmanager
+    def router(self, name='router1', admin_status_up=True, fmt='json'):
+        res = self._create_router(fmt, _uuid(), name=name,
+                                  admin_state_up=admin_status_up)
+        router = self.deserialize(fmt, res)
+        yield router
+        self._delete('routers', router['router']['id'])
+
+    def test_router_crd_ops(self):
+        with self.router() as r:
+            body = self._list('routers')
+            self.assertEquals(len(body['routers']), 1)
+            self.assertEquals(body['routers'][0]['id'], r['router']['id'])
+
+            body = self._show('routers', r['router']['id'])
+            self.assertEquals(body['router']['id'], r['router']['id'])
+            self.assertEquals(body['router']['external_gateway_info'], None)
+
+        # post-delete, check that it is really gone
+        body = self._list('routers')
+        self.assertEquals(len(body['routers']), 0)
+
+        body = self._show('routers', r['router']['id'],
+                          expected_code=exc.HTTPNotFound.code)
+
+    def test_router_update(self):
+        rname1 = "yourrouter"
+        rname2 = "nachorouter"
+        with self.router(name=rname1) as r:
+            body = self._show('routers', r['router']['id'])
+            self.assertEquals(body['router']['name'], rname1)
+
+            body = self._update('routers', r['router']['id'],
+                                {'router': {'name': rname2}})
+
+            body = self._show('routers', r['router']['id'])
+            self.assertEquals(body['router']['name'], rname2)
+
+    def test_router_add_interface_subnet(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                body = self._router_interface_action('add',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None)
+                self.assertTrue('port_id' in body)
+
+                # fetch port and confirm device_id
+                r_port_id = body['port_id']
+                body = self._show('ports', r_port_id)
+                self.assertEquals(body['port']['device_id'], r['router']['id'])
+
+                body = self._router_interface_action('remove',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None)
+                body = self._show('ports', r_port_id,
+                                  expected_code=exc.HTTPNotFound.code)
+
+    def test_router_add_interface_port(self):
+        with self.router() as r:
+            with self.port() as p:
+                body = self._router_interface_action('add',
+                                                     r['router']['id'],
+                                                     None,
+                                                     p['port']['id'])
+                self.assertTrue('port_id' in body)
+                self.assertEquals(body['port_id'], p['port']['id'])
+
+                # fetch port and confirm device_id
+                body = self._show('ports', p['port']['id'])
+                self.assertEquals(body['port']['device_id'], r['router']['id'])
+
+    def test_router_add_interface_dup_subnet1(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                body = self._router_interface_action('add',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None)
+                body = self._router_interface_action('add',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None,
+                                                     expected_code=
+                                                     exc.HTTPBadRequest.code)
+                body = self._router_interface_action('remove',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None)
+
+    def test_router_add_interface_dup_subnet2(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                with self.port(subnet=s) as p1:
+                    with self.port(subnet=s) as p2:
+                        self._router_interface_action('add',
+                                                      r['router']['id'],
+                                                      None,
+                                                      p1['port']['id'])
+                        self._router_interface_action('add',
+                                                      r['router']['id'],
+                                                      None,
+                                                      p2['port']['id'],
+                                                      expected_code=
+                                                      exc.HTTPBadRequest.code)
+
+    def test_router_add_interface_no_data(self):
+        with self.router() as r:
+            body = self._router_interface_action('add',
+                                                 r['router']['id'],
+                                                 None,
+                                                 None,
+                                                 expected_code=
+                                                 exc.HTTPBadRequest.code)
+
+    def test_router_add_gateway(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                self._add_external_gateway_to_router(
+                    r['router']['id'],
+                    s['subnet']['network_id'])
+                body = self._show('routers', r['router']['id'])
+                net_id = body['router']['external_gateway_info']['network_id']
+                self.assertEquals(net_id, s['subnet']['network_id'])
+                self._remove_external_gateway_from_router(
+                    r['router']['id'],
+                    s['subnet']['network_id'])
+                body = self._show('routers', r['router']['id'])
+                gw_info = body['router']['external_gateway_info']
+                self.assertEquals(gw_info, None)
+
+    def test_router_add_gateway_invalid_network(self):
+        with self.router() as r:
+            self._add_external_gateway_to_router(
+                r['router']['id'],
+                "foobar", expected_code=exc.HTTPNotFound.code)
+
+    def test_router_add_gateway_no_subnet(self):
+        with self.router() as r:
+            with self.network() as n:
+                self._add_external_gateway_to_router(
+                    r['router']['id'],
+                    n['network']['id'], expected_code=exc.HTTPBadRequest.code)
+
+    def test_router_delete_inuse_interface(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                self._router_interface_action('add',
+                                              r['router']['id'],
+                                              s['subnet']['id'],
+                                              None)
+                self._delete('routers', r['router']['id'],
+                             expected_code=exc.HTTPConflict.code)
+
+                # remove interface so test can exit without errors
+                self._router_interface_action('remove',
+                                              r['router']['id'],
+                                              s['subnet']['id'],
+                                              None)
+
+    def test_router_remove_router_interface_wrong_subnet_returns_409(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                with self.port() as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  s['subnet']['id'],
+                                                  p['port']['id'],
+                                                  exc.HTTPConflict.code)
+
+    def test_router_remove_router_interface_wrong_port_returns_409(self):
+        with self.router() as r:
+            with self.subnet() as s:
+                with self.port() as p:
+                    self._router_interface_action('add',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p['port']['id'])
+                    # create another port for testing failure case
+                    res = self._create_port('json', p['port']['network_id'])
+                    p2 = self.deserialize('json', res)
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  None,
+                                                  p2['port']['id'],
+                                                  exc.HTTPConflict.code)
+                    # remove extra port created
+                    self._delete('ports', p2['port']['id'])
+
+    def _create_floatingip(self, fmt, network_id, port_id=None,
+                           fixed_ip=None):
+        data = {'floatingip': {'floating_network_id': network_id,
+                               'tenant_id': self._tenant_id}}
+        if port_id:
+            data['floatingip']['port_id'] = port_id
+            if fixed_ip:
+                data['floatingip']['fixed_ip'] = fixed_ip
+        floatingip_req = self.new_create_request('floatingips', data, fmt)
+        return floatingip_req.get_response(self.ext_api)
+
+    def _validate_floating_ip(self, fip):
+        body = self._list('floatingips')
+        self.assertEquals(len(body['floatingips']), 1)
+        self.assertEquals(body['floatingips'][0]['id'],
+                          fip['floatingip']['id'])
+
+        body = self._show('floatingips', fip['floatingip']['id'])
+        self.assertEquals(body['floatingip']['id'],
+                          fip['floatingip']['id'])
+
+    @contextlib.contextmanager
+    def floatingip_with_assoc(self, port_id=None, fmt='json'):
+        with self.subnet() as public_sub:
+            with self.port() as private_port:
+                with self.router() as r:
+                    sid = private_port['port']['fixed_ips'][0]['subnet_id']
+                    private_sub = {'subnet': {'id': sid}}
+                    self._add_external_gateway_to_router(
+                        r['router']['id'],
+                        public_sub['subnet']['network_id'])
+                    self._router_interface_action('add', r['router']['id'],
+                                                  private_sub['subnet']['id'],
+                                                  None)
+
+                    res = self._create_floatingip(
+                        fmt,
+                        public_sub['subnet']['network_id'],
+                        port_id=private_port['port']['id'])
+                    self.assertEqual(res.status_int, exc.HTTPCreated.code)
+                    floatingip = self.deserialize(fmt, res)
+                    yield floatingip
+                    self._delete('floatingips', floatingip['floatingip']['id'])
+                    self._remove_external_gateway_from_router(
+                        r['router']['id'],
+                        public_sub['subnet']['network_id'])
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  private_sub['subnet']['id'],
+                                                  None)
+
+    @contextlib.contextmanager
+    def floatingip_no_assoc(self, private_sub, fmt='json'):
+        with self.subnet() as public_sub:
+            with self.router() as r:
+                self._add_external_gateway_to_router(
+                    r['router']['id'],
+                    public_sub['subnet']['network_id'])
+                self._router_interface_action('add', r['router']['id'],
+                                              private_sub['subnet']['id'],
+                                              None)
+
+                res = self._create_floatingip(
+                    fmt,
+                    public_sub['subnet']['network_id'])
+                self.assertEqual(res.status_int, exc.HTTPCreated.code)
+                floatingip = self.deserialize(fmt, res)
+                yield floatingip
+                self._delete('floatingips', floatingip['floatingip']['id'])
+                self._remove_external_gateway_from_router(
+                    r['router']['id'],
+                    public_sub['subnet']['network_id'])
+                self._router_interface_action('remove', r['router']['id'],
+                                              private_sub['subnet']['id'],
+                                              None)
+
+    def test_floatingip_crd_ops(self):
+        with self.floatingip_with_assoc() as fip:
+            self._validate_floating_ip(fip)
+
+        # post-delete, check that it is really gone
+        body = self._list('floatingips')
+        self.assertEquals(len(body['floatingips']), 0)
+
+        self._show('floatingips', fip['floatingip']['id'],
+                   expected_code=exc.HTTPNotFound.code)
+
+    def test_floatingip_update(self):
+        with self.port() as p:
+            private_sub = {'subnet': {'id':
+                                      p['port']['fixed_ips'][0]['subnet_id']}}
+            with self.floatingip_no_assoc(private_sub) as fip:
+                body = self._show('floatingips', fip['floatingip']['id'])
+                self.assertEquals(body['floatingip']['port_id'], None)
+                self.assertEquals(body['floatingip']['fixed_ip_address'], None)
+
+                port_id = p['port']['id']
+                ip_address = p['port']['fixed_ips'][0]['ip_address']
+                fixed_ip = p['port']['fixed_ips'][0]['ip_address']
+                body = self._update('floatingips', fip['floatingip']['id'],
+                                    {'floatingip': {'port_id': port_id}})
+                self.assertEquals(body['floatingip']['port_id'], port_id)
+                self.assertEquals(body['floatingip']['fixed_ip_address'],
+                                  ip_address)
+
+    def test_floatingip_with_assoc(self):
+        with self.floatingip_with_assoc() as fip:
+            body = self._show('floatingips', fip['floatingip']['id'])
+            self.assertEquals(body['floatingip']['id'],
+                              fip['floatingip']['id'])
+            self.assertEquals(body['floatingip']['port_id'],
+                              fip['floatingip']['port_id'])
+            self.assertTrue(body['floatingip']['fixed_ip_address'] is not None)
+            self.assertTrue(body['floatingip']['router_id'] is not None)
+
+    def test_floatingip_port_delete(self):
+        with self.subnet() as private_sub:
+            with self.floatingip_no_assoc(private_sub) as fip:
+                with self.port(subnet=private_sub) as p:
+                    body = self._update('floatingips', fip['floatingip']['id'],
+                                        {'floatingip':
+                                         {'port_id': p['port']['id']}})
+                # note: once this port goes out of scope, the port will be
+                # deleted, which is what we want to test. We want to confirm
+                # that the fields are set back to None
+                body = self._show('floatingips', fip['floatingip']['id'])
+                self.assertEquals(body['floatingip']['id'],
+                                  fip['floatingip']['id'])
+                self.assertEquals(body['floatingip']['port_id'], None)
+                self.assertEquals(body['floatingip']['fixed_ip_address'], None)
+                self.assertEquals(body['floatingip']['router_id'], None)
+
+    def test_double_floating_assoc(self):
+        with self.floatingip_with_assoc() as fip1:
+            with self.subnet() as s:
+                with self.floatingip_no_assoc(s) as fip2:
+                    port_id = fip1['floatingip']['port_id']
+                    body = self._update('floatingips',
+                                        fip2['floatingip']['id'],
+                                        {'floatingip':
+                                         {'port_id': port_id}},
+                                        expected_code=exc.HTTPConflict.code)
+
+    def test_create_floatingip_no_ext_gateway_return_404(self):
+        with self.subnet() as public_sub:
+            with self.port() as private_port:
+                with self.router() as r:
+                    res = self._create_floatingip(
+                        'json',
+                        public_sub['subnet']['network_id'],
+                        port_id=private_port['port']['id'])
+                    # this should be some kind of error
+                    self.assertEqual(res.status_int, exc.HTTPNotFound.code)
+
+    def test_create_floatingip_no_public_subnet_returns_400(self):
+        with self.network() as public_network:
+            with self.port() as private_port:
+                with self.router() as r:
+                    sid = private_port['port']['fixed_ips'][0]['subnet_id']
+                    private_sub = {'subnet': {'id': sid}}
+                    self._router_interface_action('add', r['router']['id'],
+                                                  private_sub['subnet']['id'],
+                                                  None)
+
+                    res = self._create_floatingip(
+                        'json',
+                        public_network['network']['id'],
+                        port_id=private_port['port']['id'])
+                    self.assertEqual(res.status_int, exc.HTTPBadRequest.code)
+                    # cleanup
+                    self._router_interface_action('remove',
+                                                  r['router']['id'],
+                                                  private_sub['subnet']['id'],
+                                                  None)
index 143c2c6c9113528ec390cb2b099b7c12037da296..04664f2f1c35e4b5114705e975dac98bc0e83ecc 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -98,6 +98,7 @@ setuptools.setup(
     entry_points={
         'console_scripts': [
             'quantum-dhcp-agent = quantum.agent.dhcp_agent:main',
+            'quantum-l3-agent = quantum.agent.l3_nat_agent:main',
             'quantum-linuxbridge-agent ='
             'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
             'quantum-openvswitch-agent ='