From 3005d16fe3588bdf4b928e79f640b991df9fae3b Mon Sep 17 00:00:00 2001 From: Dan Wendlandt Date: Wed, 15 Aug 2012 12:56:52 -0700 Subject: [PATCH] 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 --- bin/quantum-l3-agent | 20 + etc/l3_agent.ini | 19 + quantum/agent/l3_agent.py | 414 +++++++++++ quantum/api/v2/base.py | 24 +- quantum/common/exceptions.py | 10 +- quantum/db/db_base_plugin_v2.py | 3 +- quantum/db/l3_db.py | 546 +++++++++++++++ quantum/extensions/l3.py | 210 ++++++ quantum/plugins/cisco/db/api.py | 6 +- .../plugins/openvswitch/ovs_quantum_plugin.py | 10 +- quantum/tests/unit/test_api_v2.py | 5 +- quantum/tests/unit/test_l3_agent.py | 264 +++++++ quantum/tests/unit/test_l3_plugin.py | 642 ++++++++++++++++++ setup.py | 1 + 14 files changed, 2160 insertions(+), 14 deletions(-) create mode 100755 bin/quantum-l3-agent create mode 100644 etc/l3_agent.ini create mode 100644 quantum/agent/l3_agent.py create mode 100644 quantum/db/l3_db.py create mode 100644 quantum/extensions/l3.py create mode 100644 quantum/tests/unit/test_l3_agent.py create mode 100644 quantum/tests/unit/test_l3_plugin.py diff --git a/bin/quantum-l3-agent b/bin/quantum-l3-agent new file mode 100755 index 0000000000..39413e2758 --- /dev/null +++ b/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() diff --git a/etc/l3_agent.ini b/etc/l3_agent.ini new file mode 100644 index 0000000000..b119c8fdec --- /dev/null +++ b/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% + diff --git a/quantum/agent/l3_agent.py b/quantum/agent/l3_agent.py new file mode 100644 index 0000000000..098db3b3e0 --- /dev/null +++ b/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() diff --git a/quantum/api/v2/base.py b/quantum/api/v2/base.py index f16df2e936..9800714dc2 100644 --- a/quantum/api/v2/base.py +++ b/quantum/api/v2/base.py @@ -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 diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index 241727c1b1..4e0f0456d4 100644 --- a/quantum/common/exceptions.py +++ b/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): diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index eed7225423..0fee6ff6f6 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -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 index 0000000000..0a1417887d --- /dev/null +++ b/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) diff --git a/quantum/extensions/l3.py b/quantum/extensions/l3.py new file mode 100644 index 0000000000..1ee5e82461 --- /dev/null +++ b/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 diff --git a/quantum/plugins/cisco/db/api.py b/quantum/plugins/cisco/db/api.py index 2841372194..5aab5015e9 100644 --- a/quantum/plugins/cisco/db/api.py +++ b/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( diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index 722d7d8b04..2e966b21d7 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -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) diff --git a/quantum/tests/unit/test_api_v2.py b/quantum/tests/unit/test_api_v2.py index 055e7eec2f..2eff401e28 100644 --- a/quantum/tests/unit/test_api_v2.py +++ b/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 diff --git a/quantum/tests/unit/test_l3_agent.py b/quantum/tests/unit/test_l3_agent.py new file mode 100644 index 0000000000..57efc2faf3 --- /dev/null +++ b/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() diff --git a/quantum/tests/unit/test_l3_plugin.py b/quantum/tests/unit/test_l3_plugin.py new file mode 100644 index 0000000000..8cd9fe30c7 --- /dev/null +++ b/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'): + 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) diff --git a/setup.py b/setup.py index 143c2c6c91..04664f2f1c 100644 --- 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 ='