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:
parent
09749a9e5a
commit
3005d16fe3
20
bin/quantum-l3-agent
Executable file
20
bin/quantum-l3-agent
Executable 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
19
etc/l3_agent.ini
Normal 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
414
quantum/agent/l3_agent.py
Normal 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()
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
546
quantum/db/l3_db.py
Normal 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
210
quantum/extensions/l3.py
Normal 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
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
264
quantum/tests/unit/test_l3_agent.py
Normal file
264
quantum/tests/unit/test_l3_agent.py
Normal 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()
|
642
quantum/tests/unit/test_l3_plugin.py
Normal file
642
quantum/tests/unit/test_l3_plugin.py
Normal 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)
|
1
setup.py
1
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 ='
|
||||
|
Loading…
Reference in New Issue
Block a user