Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

quantum l3 + floating IP support

bp quantum-l3-fw-nat

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

Change-Id: I6ee61396d22e2fd7840aa2ff7d1f6f4a2c6e54d4
  • Loading branch information...
commit 3005d16fe3588bdf4b928e79f640b991df9fae3b 1 parent 09749a9
@danwent danwent authored salv-orlando committed
View
20 bin/quantum-l3-agent
@@ -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()
View
19 etc/l3_agent.ini
@@ -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%
+
View
414 quantum/agent/l3_agent.py
@@ -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()
View
24 quantum/api/v2/base.py
@@ -32,6 +32,7 @@
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 @@ def __init__(self, plugin, collection, resource,
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 @@ def _view(self, data, fields_to_strip=None):
# 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 @@ def _do_field_list(self, original_fields):
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 @@ def _validate_network_tenant_ownership(self, request, resource_item):
})
-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
View
10 quantum/common/exceptions.py
@@ -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):
View
3  quantum/db/db_base_plugin_v2.py
@@ -1067,11 +1067,12 @@ def get_port(self, context, id, fields=None, verbose=None):
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:
View
546 quantum/db/l3_db.py
@@ -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)
View
210 quantum/extensions/l3.py
@@ -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
View
6 quantum/plugins/cisco/db/api.py
@@ -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(
View
10 quantum/plugins/openvswitch/ovs_quantum_plugin.py
@@ -30,6 +30,7 @@
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 @@ def tunnel_update(self, context, tunnel_ip, tunnel_id):
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 @@ def update_port(self, context, id, port):
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)
View
5 quantum/tests/unit/test_api_v2.py
@@ -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
View
264 quantum/tests/unit/test_l3_agent.py
@@ -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()
View
642 quantum/tests/unit/test_l3_plugin.py
@@ -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'):