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
This commit is contained in:
Dan Wendlandt 2012-08-15 12:56:52 -07:00 committed by Salvatore Orlando
parent 09749a9e5a
commit 3005d16fe3
14 changed files with 2160 additions and 14 deletions

20
bin/quantum-l3-agent Executable file
View File

@ -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()

19
etc/l3_agent.ini Normal file
View File

@ -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%

414
quantum/agent/l3_agent.py Normal file
View File

@ -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 File

@ -32,6 +32,7 @@ XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
exceptions.InUse: webob.exc.HTTPConflict,
exceptions.BadRequest: webob.exc.HTTPBadRequest,
exceptions.ResourceExhausted: webob.exc.HTTPServiceUnavailable,
exceptions.MacAddressGenerationFailure:
webob.exc.HTTPServiceUnavailable,
@ -132,8 +133,11 @@ def _verbose(request):
class Controller(object):
def __init__(self, plugin, collection, resource,
attr_info, allow_bulk=False):
def __init__(self, plugin, collection, resource, attr_info,
allow_bulk=False, member_actions=None):
if member_actions is None:
member_actions = []
self._plugin = plugin
self._collection = collection
self._resource = resource
@ -143,6 +147,7 @@ class Controller(object):
self._policy_attrs = [name for (name, info) in self._attr_info.items()
if info.get('required_by_policy')]
self._publisher_id = notifier_api.publisher_id('network')
self._member_actions = member_actions
def _is_native_bulk_supported(self):
native_bulk_attr_name = ("_%s__native_bulk_support"
@ -157,6 +162,7 @@ class Controller(object):
# make sure fields_to_strip is iterable
if not fields_to_strip:
fields_to_strip = []
return dict(item for item in data.iteritems()
if self._is_visible(item[0])
and not item[0] in fields_to_strip)
@ -170,6 +176,14 @@ class Controller(object):
original_fields.extend(self._policy_attrs)
return original_fields, fields_to_add
def __getattr__(self, name):
if name in self._member_actions:
def _handle_action(request, id, body=None):
return getattr(self._plugin, name)(request.context, id, body)
return _handle_action
else:
raise AttributeError
def _items(self, request, do_authz=False):
"""Retrieves and formats a list of elements of the requested entity"""
# NOTE(salvatore-orlando): The following ensures that fields which
@ -545,8 +559,10 @@ class Controller(object):
})
def create_resource(collection, resource, plugin, params, allow_bulk=False):
controller = Controller(plugin, collection, resource, params, allow_bulk)
def create_resource(collection, resource, plugin, params, allow_bulk=False,
member_actions=None):
controller = Controller(plugin, collection, resource, params, allow_bulk,
member_actions=member_actions)
# NOTE(jkoelker) To anyone wishing to add "proper" xml support
# this is where you do it

View File

@ -34,6 +34,10 @@ class QuantumException(OpenstackException):
message = _("An unknown exception occurred.")
class BadRequest(QuantumException):
message = _('Bad %(resource)s request: %(msg)s')
class NotFound(QuantumException):
pass
@ -86,13 +90,13 @@ class NetworkInUse(InUse):
class SubnetInUse(InUse):
message = _("Unable to complete operation on subnet %(subnet_id)s. "
"There is used by one or more ports.")
"One or more ports have an IP allocation from this subnet.")
class PortInUse(InUse):
message = _("Unable to complete operation on port %(port_id)s "
"for network %(net_id)s. The attachment '%(att_id)s"
"is plugged into the logical port.")
"for network %(net_id)s. Port already has an attached"
"device %(device_id)s.")
class MacAddressInUse(InUse):

View File

@ -1067,11 +1067,12 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
return self._make_port_dict(port, fields)
def get_ports(self, context, filters=None, fields=None, verbose=None):
fixed_ips = filters.pop('fixed_ips', [])
fixed_ips = filters.pop('fixed_ips', []) if filters else []
ports = self._get_collection(context, models_v2.Port,
self._make_port_dict,
filters=filters, fields=fields,
verbose=verbose)
if ports and fixed_ips:
filtered_ports = []
for port in ports:

546
quantum/db/l3_db.py Normal file
View File

@ -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)

210
quantum/extensions/l3.py Normal file
View File

@ -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 File

@ -213,7 +213,7 @@ def port_set_attachment(net_id, port_id, new_interface_id):
# We are setting, not clearing, the attachment-id
if port['interface_id']:
raise q_exc.PortInUse(net_id=net_id, port_id=port_id,
att_id=port['interface_id'])
device_id=port['interface_id'])
try:
port = (session.query(models.Port).
@ -256,7 +256,7 @@ def port_destroy(net_id, port_id):
one())
if port['interface_id']:
raise q_exc.PortInUse(net_id=net_id, port_id=port_id,
att_id=port['interface_id'])
device_id=port['interface_id'])
session.delete(port)
session.flush()
return port
@ -281,7 +281,7 @@ def port_set_attachment_by_id(port_id, new_interface_id):
if new_interface_id != "":
if port['interface_id']:
raise q_exc.PortInUse(port_id=port_id,
att_id=port['interface_id'])
device_id=port['interface_id'])
try:
port = session.query(models.Port).filter_by(

View File

@ -30,6 +30,7 @@ from quantum.common import topics
from quantum.db import api as db
from quantum.db import db_base_plugin_v2
from quantum.db import dhcp_rpc_base
from quantum.db import l3_db
from quantum.db import models_v2
from quantum.openstack.common import context
from quantum.openstack.common import cfg
@ -162,7 +163,8 @@ class AgentNotifierApi(proxy.RpcProxy):
topic=self.topic_tunnel_update)
class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
l3_db.L3_NAT_db_mixin):
"""Implement the Quantum abstractions using Open vSwitch.
Depending on whether tunneling is enabled, either a GRE tunnel or
@ -181,7 +183,7 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
# bulk operations. Name mangling is used in order to ensure it
# is qualified by class
__native_bulk_support = True
supported_extension_aliases = ["provider"]
supported_extension_aliases = ["provider", "os-quantum-router"]
def __init__(self, configfile=None):
self.enable_tunneling = cfg.CONF.OVS.enable_tunneling
@ -361,3 +363,7 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2):
vlan_id = ovs_db_v2.get_vlan(port['network_id'])
self.notifier.port_update(self.context, port, vlan_id)
return port
def delete_port(self, context, id):
self.disassociate_floatingips(context, id)
return super(OVSQuantumPluginV2, self).delete_port(context, id)

View File

@ -50,12 +50,15 @@ def etcdir(*p):
return os.path.join(ETCDIR, *p)
def _get_path(resource, id=None, fmt=None):
def _get_path(resource, id=None, action=None, fmt=None):
path = '/%s' % resource
if id is not None:
path = path + '/%s' % id
if action is not None:
path = path + '/%s' % action
if fmt is not None:
path = path + '.%s' % fmt

View File

@ -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 File

@ -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)

View File

@ -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 ='