From 9f1d2e0c51ba47610f30d56de356f301c22e0dcd Mon Sep 17 00:00:00 2001 From: Salvatore Orlando Date: Mon, 18 Feb 2013 10:57:39 +0100 Subject: [PATCH] Configurable external gateway modes Blueprint l3-ext-gw-modes This patch introduces an API extension for enabling or disabling source/destination NAT on the router gateway interface, and implements support for this extension in the l3 agent, which has also been refactored in order to ensure SNAT rules are applied (or removed) in a single place in the process_router routine. The patch therefore enables the extension on all plugins which leverage this agent. In this patch the validate_boolean function is re-introduced, as it used for validating a field of the external_gateway_info dict. The resource extension process is also slightly changed so that an API extension can specify which extension should be processed before its attributes can be processed. This is needed to ensure the proper validator for external_gateway_info is installed. Change-Id: Ia80cb56135229366b1706dd3c6d366e40cde1500 --- quantum/agent/l3_agent.py | 110 +++-- quantum/api/v2/attributes.py | 19 +- quantum/db/l3_db.py | 126 +++--- quantum/db/l3_gwmode_db.py | 68 +++ .../versions/128e042a2b68_ext_gw_mode.py | 62 +++ quantum/extensions/l3.py | 6 +- quantum/extensions/l3_ext_gw_mode.py | 73 ++++ .../plugins/hyperv/hyperv_quantum_plugin.py | 7 +- .../plugins/linuxbridge/lb_quantum_plugin.py | 8 +- .../plugins/metaplugin/meta_quantum_plugin.py | 3 +- quantum/plugins/nec/nec_plugin.py | 9 +- .../plugins/openvswitch/ovs_quantum_plugin.py | 4 +- quantum/plugins/ryu/ryu_quantum_plugin.py | 5 +- quantum/tests/unit/metaplugin/fake_plugin.py | 4 +- quantum/tests/unit/test_db_plugin.py | 1 - .../tests/unit/test_extension_ext_gw_mode.py | 404 ++++++++++++++++++ quantum/tests/unit/test_l3_agent.py | 112 ++++- quantum/tests/unit/test_l3_plugin.py | 16 +- 18 files changed, 895 insertions(+), 142 deletions(-) create mode 100644 quantum/db/l3_gwmode_db.py create mode 100644 quantum/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py create mode 100644 quantum/extensions/l3_ext_gw_mode.py create mode 100644 quantum/tests/unit/test_extension_ext_gw_mode.py diff --git a/quantum/agent/l3_agent.py b/quantum/agent/l3_agent.py index 55bfe2908..b4e31b3ea 100644 --- a/quantum/agent/l3_agent.py +++ b/quantum/agent/l3_agent.py @@ -92,10 +92,13 @@ class RouterInfo(object): def __init__(self, router_id, root_helper, use_namespaces, router): self.router_id = router_id self.ex_gw_port = None + self._snat_enabled = None + self._snat_action = None self.internal_ports = [] self.floating_ips = [] self.root_helper = root_helper self.use_namespaces = use_namespaces + # Invoke the setter for establishing initial SNAT action self.router = router self.iptables_manager = iptables_manager.IptablesManager( root_helper=root_helper, @@ -104,10 +107,37 @@ class RouterInfo(object): self.routes = [] + @property + def router(self): + return self._router + + @router.setter + def router(self, value): + self._router = value + if not self._router: + return + # Set a SNAT action for the router + if self._router.get('gw_port'): + if (self._router.get('enable_snat') and not self._snat_enabled): + self._snat_action = 'add_rule' + elif (self._snat_enabled and + not self._router.get('enable_snat')): + self._snat_action = 'remove_rule' + elif self.ex_gw_port: + self._snat_action = 'remove_rule' + self._snat_enabled = self._router.get('enable_snat') + def ns_name(self): if self.use_namespaces: return NS_PREFIX + self.router_id + def perform_snat_action(self, snat_callback, *args): + # Process SNAT rules for attached subnets + if self._snat_action: + snat_callback(self, self._router.get('gw_port'), + *args, action=self._snat_action) + self._snat_action = None + class L3NATAgent(manager.Manager): @@ -291,7 +321,6 @@ class L3NATAgent(manager.Manager): 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 = ri.router.get(l3_constants.INTERFACE_KEY, []) existing_port_ids = set([p['id'] for p in ri.internal_ports]) @@ -306,31 +335,53 @@ class L3NATAgent(manager.Manager): for p in new_ports: self._set_subnet_info(p) ri.internal_ports.append(p) - self.internal_network_added(ri, ex_gw_port, - p['network_id'], p['id'], + self.internal_network_added(ri, p['network_id'], p['id'], p['ip_cidr'], p['mac_address']) for p in old_ports: ri.internal_ports.remove(p) - self.internal_network_removed(ri, ex_gw_port, p['id'], - p['ip_cidr']) + self.internal_network_removed(ri, p['id'], p['ip_cidr']) internal_cidrs = [p['ip_cidr'] for p in ri.internal_ports] + # TODO(salv-orlando): RouterInfo would be a better place for + # this logic too + ex_gw_port_id = (ex_gw_port and ex_gw_port['id'] or + ri.ex_gw_port and ri.ex_gw_port['id']) + if ex_gw_port_id: + interface_name = self.get_external_device_name(ex_gw_port_id) 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) + self.external_gateway_added(ri, ex_gw_port, + interface_name, internal_cidrs) elif not ex_gw_port and ri.ex_gw_port: self.external_gateway_removed(ri, ri.ex_gw_port, - internal_cidrs) + interface_name, internal_cidrs) + + # Process SNAT rules for external gateway + if ex_gw_port_id: + ri.perform_snat_action(self._handle_router_snat_rules, + internal_cidrs, interface_name) - if ri.ex_gw_port or ex_gw_port: + # Process DNAT rules for floating IPs + if ex_gw_port or ri.ex_gw_port: self.process_router_floating_ips(ri, ex_gw_port) ri.ex_gw_port = ex_gw_port - + ri.enable_snat = ri.router.get('enable_snat') self.routes_updated(ri) + def _handle_router_snat_rules(self, ri, ex_gw_port, internal_cidrs, + interface_name, action): + ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address'] + for rule in self.external_gateway_nat_rules(ex_gw_ip, + internal_cidrs, + interface_name): + # This is an internal method so we can assume the caller + # knows which actions are valid and which not + getattr(ri.iptables_manager.ipv4['nat'], action)(*rule) + ri.iptables_manager.apply() + def process_router_floating_ips(self, ri, ex_gw_port): floating_ips = ri.router.get(l3_constants.FLOATINGIP_KEY, []) existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips]) @@ -398,10 +449,9 @@ class L3NATAgent(manager.Manager): 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): + def external_gateway_added(self, ri, ex_gw_port, + interface_name, 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.root_helper, namespace=ri.ns_name()): @@ -427,15 +477,9 @@ class L3NATAgent(manager.Manager): utils.execute(cmd, check_exit_code=False, root_helper=self.root_helper) - for (c, r) in self.external_gateway_nat_rules(ex_gw_ip, - internal_cidrs, - interface_name): - ri.iptables_manager.ipv4['nat'].add_rule(c, r) - ri.iptables_manager.apply() + def external_gateway_removed(self, ri, ex_gw_port, + interface_name, internal_cidrs): - 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.root_helper, namespace=ri.ns_name()): @@ -444,12 +488,6 @@ class L3NATAgent(manager.Manager): namespace=ri.ns_name(), prefix=EXTERNAL_DEV_PREFIX) - ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address'] - for c, r in self.external_gateway_nat_rules(ex_gw_ip, internal_cidrs, - interface_name): - ri.iptables_manager.ipv4['nat'].remove_rule(c, r) - ri.iptables_manager.apply() - def metadata_filter_rules(self): rules = [] rules.append(('INPUT', '-s 0.0.0.0/0 -d 127.0.0.1 ' @@ -474,7 +512,7 @@ class L3NATAgent(manager.Manager): rules.extend(self.internal_network_nat_rules(ex_gw_ip, cidr)) return rules - def internal_network_added(self, ri, ex_gw_port, network_id, port_id, + def internal_network_added(self, ri, network_id, port_id, internal_cidr, mac_address): interface_name = self.get_internal_device_name(port_id) if not ip_lib.device_exists(interface_name, @@ -489,14 +527,7 @@ class L3NATAgent(manager.Manager): ip_address = internal_cidr.split('/')[0] self._send_gratuitous_arp_packet(ri, interface_name, ip_address) - 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): + def internal_network_removed(self, ri, port_id, internal_cidr): interface_name = self.get_internal_device_name(port_id) if ip_lib.device_exists(interface_name, root_helper=self.root_helper, @@ -504,13 +535,6 @@ class L3NATAgent(manager.Manager): self.driver.unplug(interface_name, namespace=ri.ns_name(), prefix=INTERNAL_DEV_PREFIX) - 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): rules = [('snat', '-s %s -j SNAT --to-source %s' % (internal_cidr, ex_gw_ip))] @@ -610,11 +634,9 @@ class L3NATAgent(manager.Manager): if (not self.conf.use_namespaces and r['id'] != self.conf.router_id): continue - ex_net_id = (r['external_gateway_info'] or {}).get('network_id') if not ex_net_id and not self.conf.handle_internal_only_routers: continue - if ex_net_id and ex_net_id != target_ex_net_id: continue cur_router_ids.add(r['id']) diff --git a/quantum/api/v2/attributes.py b/quantum/api/v2/attributes.py index 55f37def6..a978376f7 100644 --- a/quantum/api/v2/attributes.py +++ b/quantum/api/v2/attributes.py @@ -84,6 +84,15 @@ def _validate_string(data, max_len=None): return msg +def _validate_boolean(data, valid_values=None): + try: + convert_to_boolean(data) + except q_exc.InvalidInput: + msg = _("'%s' is not a valid boolean value") % data + LOG.debug(msg) + return msg + + def _validate_range(data, valid_values=None): min_value = valid_values[0] max_value = valid_values[1] @@ -294,7 +303,6 @@ def _validate_dict(data, key_specs=None): msg = _("'%s' is not a dictionary") % data LOG.debug(msg) return msg - # Do not perform any further validation, if no constraints are supplied if not key_specs: return @@ -340,6 +348,11 @@ def _validate_dict_or_empty(data, key_specs=None): return _validate_dict(data, key_specs) +def _validate_dict_or_nodata(data, key_specs=None): + if data: + return _validate_dict(data, key_specs) + + def _validate_non_negative(data, valid_values=None): try: data = int(data) @@ -443,6 +456,7 @@ MAC_PATTERN = "^%s[aceACE02468](:%s{2}){5}$" % (HEX_ELEM, HEX_ELEM) validators = {'type:dict': _validate_dict, 'type:dict_or_none': _validate_dict_or_none, 'type:dict_or_empty': _validate_dict_or_empty, + 'type:dict_or_nodata': _validate_dict_or_nodata, 'type:fixed_ips': _validate_fixed_ips, 'type:hostroutes': _validate_hostroutes, 'type:ip_address': _validate_ip_address, @@ -458,7 +472,8 @@ validators = {'type:dict': _validate_dict, 'type:uuid': _validate_uuid, 'type:uuid_or_none': _validate_uuid_or_none, 'type:uuid_list': _validate_uuid_list, - 'type:values': _validate_values} + 'type:values': _validate_values, + 'type:boolean': _validate_boolean} # Define constants for base resource name NETWORK = 'network' diff --git a/quantum/db/l3_db.py b/quantum/db/l3_db.py index 19a363622..c720b4de9 100644 --- a/quantum/db/l3_db.py +++ b/quantum/db/l3_db.py @@ -41,6 +41,7 @@ LOG = logging.getLogger(__name__) DEVICE_OWNER_ROUTER_INTF = l3_constants.DEVICE_OWNER_ROUTER_INTF DEVICE_OWNER_ROUTER_GW = l3_constants.DEVICE_OWNER_ROUTER_GW DEVICE_OWNER_FLOATINGIP = l3_constants.DEVICE_OWNER_FLOATINGIP +EXTERNAL_GW_INFO = l3.EXTERNAL_GW_INFO # Maps API field to DB column # API parameter name and Database column names may differ. @@ -133,11 +134,11 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): 'tenant_id': router['tenant_id'], 'admin_state_up': router['admin_state_up'], 'status': router['status'], - 'external_gateway_info': None, + EXTERNAL_GW_INFO: None, 'gw_port_id': router['gw_port_id']} if router['gw_port_id']: nw_id = router.gw_port['network_id'] - res['external_gateway_info'] = {'network_id': nw_id} + res[EXTERNAL_GW_INFO] = {'network_id': nw_id} if process_extensions: for func in self._dict_extend_functions.get(l3.ROUTERS, []): func(self, res, router) @@ -146,10 +147,10 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): def create_router(self, context, router): r = router['router'] has_gw_info = False - if 'external_gateway_info' in r: + if EXTERNAL_GW_INFO in r: has_gw_info = True - gw_info = r['external_gateway_info'] - del r['external_gateway_info'] + gw_info = r[EXTERNAL_GW_INFO] + del r[EXTERNAL_GW_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 @@ -167,10 +168,10 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): def update_router(self, context, id, router): r = router['router'] has_gw_info = False - if 'external_gateway_info' in r: + if EXTERNAL_GW_INFO in r: has_gw_info = True - gw_info = r['external_gateway_info'] - del r['external_gateway_info'] + gw_info = r[EXTERNAL_GW_INFO] + del r[EXTERNAL_GW_INFO] with context.session.begin(subtransactions=True): if has_gw_info: self._update_router_gw_info(context, id, gw_info) @@ -183,14 +184,38 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): l3_rpc_agent_api.L3AgentNotify.routers_updated(context, routers) return self._make_router_dict(router_db) - def _update_router_gw_info(self, context, router_id, info): + def _create_router_gw_port(self, context, router, network_id): + # Port has no 'tenant-id', as it is hidden from user + gw_port = self.create_port(context.elevated(), { + 'port': {'tenant_id': '', # intentionally not set + '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 gw_port['fixed_ips']: + self.delete_port(context.elevated(), gw_port['id'], + l3_port_check=False) + 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.gw_port = self._get_port(context.elevated(), + gw_port['id']) + context.session.add(router) + + def _update_router_gw_info(self, context, router_id, info, router=None): # 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) + router = router or self._get_router(context, router_id) gw_port = router.gw_port - - network_id = info.get('network_id', None) if info else None + # network_id attribute is required by API, so it must be present + network_id = info['network_id'] if info else None if network_id: self._get_network(context, network_id) if not self._network_is_external(context, network_id): @@ -205,11 +230,12 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): if fip_count: raise l3.RouterExternalGatewayInUseByFloatingIp( router_id=router_id, net_id=gw_port['network_id']) - with context.session.begin(subtransactions=True): - router.gw_port = None - context.session.add(router) - self.delete_port(context.elevated(), gw_port['id'], - l3_port_check=False) + if gw_port and gw_port['network_id'] != network_id: + with context.session.begin(subtransactions=True): + router.gw_port = None + context.session.add(router) + self.delete_port(context.elevated(), gw_port['id'], + l3_port_check=False) if network_id is not None and (gw_port is None or gw_port['network_id'] != network_id): @@ -219,30 +245,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): self._check_for_dup_router_subnet(context, router_id, network_id, subnet['id'], subnet['cidr']) - - # Port has no 'tenant-id', as it is hidden from user - gw_port = self.create_port(context.elevated(), { - 'port': - {'tenant_id': '', # intentionally not set - '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 gw_port['fixed_ips']: - self.delete_port(context.elevated(), gw_port['id'], - l3_port_check=False) - 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.gw_port = self._get_port(context.elevated(), - gw_port['id']) - context.session.add(router) + self._create_router_gw_port(context, router, network_id) def delete_router(self, context, id): with context.session.begin(subtransactions=True): @@ -518,14 +521,11 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): external_network_id=external_network_id, port_id=internal_port['id']) - def get_assoc_data(self, context, fip, floating_network_id): - """Determine/extract data associated with the internal port. + def _internal_fip_assoc_data(self, context, fip): + """Retrieve internal port data for floating IP. - 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. + Retrieve information concerning the internal port where + the floating IP should be associated to. """ internal_port = self._get_port(context, fip['port_id']) if not internal_port['tenant_id'] == fip['tenant_id']: @@ -567,7 +567,19 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): 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'] + return internal_port, internal_subnet_id, internal_ip_address + + def get_assoc_data(self, context, fip, floating_network_id): + """Determine/extract data associated with the internal port. + 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, internal_subnet_id, + internal_ip_address) = self._internal_fip_assoc_data(context, fip) router_id = self._get_router_for_floatingip(context, internal_port, internal_subnet_id, @@ -844,6 +856,15 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): else: return [n for n in nets if n['id'] not in ext_nets] + def _build_routers_list(self, routers, gw_ports): + gw_port_id_gw_port_dict = dict((gw_port['id'], gw_port) + for gw_port in gw_ports) + for router in routers: + gw_port_id = router['gw_port_id'] + if gw_port_id: + router['gw_port'] = gw_port_id_gw_port_dict[gw_port_id] + return routers + def _get_sync_routers(self, context, router_ids=None, active=None): """Query routers and their gw ports for l3 agent. @@ -871,14 +892,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): gw_ports = [] if gw_port_ids: gw_ports = self.get_sync_gw_ports(context, gw_port_ids) - gw_port_id_gw_port_dict = {} - for gw_port in gw_ports: - gw_port_id_gw_port_dict[gw_port['id']] = gw_port - for router_dict in router_dicts: - gw_port_id = router_dict['gw_port_id'] - if gw_port_id: - router_dict['gw_port'] = gw_port_id_gw_port_dict[gw_port_id] - return router_dicts + return self._build_routers_list(router_dicts, gw_ports) def _get_sync_floating_ips(self, context, router_ids): """Query floating_ips that relate to list of router_ids.""" diff --git a/quantum/db/l3_gwmode_db.py b/quantum/db/l3_gwmode_db.py new file mode 100644 index 000000000..d91bd3d66 --- /dev/null +++ b/quantum/db/l3_gwmode_db.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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: Salvatore Orlando, Nicira, Inc +# + +import sqlalchemy as sa + +from quantum.db import l3_db +from quantum.extensions import l3 +from quantum.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) +EXTERNAL_GW_INFO = l3.EXTERNAL_GW_INFO + +# Modify the Router Data Model adding the enable_snat attribute +setattr(l3_db.Router, 'enable_snat', + sa.Column(sa.Boolean, default=True, nullable=False)) + + +class L3_NAT_db_mixin(l3_db.L3_NAT_db_mixin): + """Mixin class to add configurable gateway modes.""" + + def _make_router_dict(self, router, fields=None): + res = super(L3_NAT_db_mixin, self)._make_router_dict(router) + if router['gw_port_id']: + nw_id = router.gw_port['network_id'] + res[EXTERNAL_GW_INFO] = {'network_id': nw_id, + 'enable_snat': router.enable_snat} + return self._fields(res, fields) + + def _update_router_gw_info(self, context, router_id, info): + router = self._get_router(context, router_id) + # if enable_snat is not specified use the value + # stored in the database (default:True) + enable_snat = not info or info.get('enable_snat', router.enable_snat) + with context.session.begin(subtransactions=True): + router.enable_snat = enable_snat + + # Calls superclass, pass router db object for avoiding re-loading + super(L3_NAT_db_mixin, self)._update_router_gw_info( + context, router_id, info, router=router) + + def _build_routers_list(self, routers, gw_ports): + gw_port_id_gw_port_dict = {} + for gw_port in gw_ports: + gw_port_id_gw_port_dict[gw_port['id']] = gw_port + for rtr in routers: + gw_port_id = rtr['gw_port_id'] + if gw_port_id: + rtr['gw_port'] = gw_port_id_gw_port_dict[gw_port_id] + # Add enable_snat key + rtr['enable_snat'] = rtr[EXTERNAL_GW_INFO]['enable_snat'] + return routers diff --git a/quantum/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py b/quantum/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py new file mode 100644 index 000000000..67fa20b8b --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 OpenStack Foundation +# +# 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. +# + +"""ext_gw_mode + +Revision ID: 128e042a2b68 +Revises: 176a85fc7d79 +Create Date: 2013-03-27 00:35:17.323280 + +""" + +# revision identifiers, used by Alembic. +revision = '128e042a2b68' +down_revision = '176a85fc7d79' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'quantum.plugins.hyperv.hyperv_quantum_plugin.HyperVQuantumPlugin', + 'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2', + 'quantum.plugins.metaplugin.meta_quantum_plugin.MetaPluginV2', + 'quantum.plugins.nec.nec_plugin.NECPluginV2', + 'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2', + 'quantum.plugins.ryu.ryu_quantum_plugin.RyuQuantumPluginV2' +] + +from alembic import op +import sqlalchemy as sa + + +from quantum.db import migration + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.add_column('routers', sa.Column('enable_snat', sa.Boolean(), + nullable=False, default=True)) + # Set enable_snat to True for existing routers + op.execute("UPDATE routers SET enable_snat=True") + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.drop_column('routers', 'enable_snat') diff --git a/quantum/extensions/l3.py b/quantum/extensions/l3.py index ba5285318..393abeac7 100644 --- a/quantum/extensions/l3.py +++ b/quantum/extensions/l3.py @@ -88,8 +88,8 @@ class RouterExternalGatewayInUseByFloatingIp(qexception.InUse): "more floating IPs.") ROUTERS = 'routers' +EXTERNAL_GW_INFO = 'external_gateway_info' -# Attribute Map RESOURCE_ATTRIBUTE_MAP = { ROUTERS: { 'id': {'allow_post': False, 'allow_put': False, @@ -109,8 +109,8 @@ RESOURCE_ATTRIBUTE_MAP = { 'required_by_policy': True, 'validate': {'type:string': None}, 'is_visible': True}, - 'external_gateway_info': {'allow_post': True, 'allow_put': True, - 'is_visible': True, 'default': None} + EXTERNAL_GW_INFO: {'allow_post': True, 'allow_put': True, + 'is_visible': True, 'default': None} }, 'floatingips': { 'id': {'allow_post': False, 'allow_put': False, diff --git a/quantum/extensions/l3_ext_gw_mode.py b/quantum/extensions/l3_ext_gw_mode.py new file mode 100644 index 000000000..1e53c473b --- /dev/null +++ b/quantum/extensions/l3_ext_gw_mode.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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: Salvatore Orlando, Nicira, Inc +# + +from quantum.api import extensions +from quantum.common import exceptions as qexception +from quantum.extensions import l3 + + +class RouterDNatDisabled(qexception.BadRequest): + message = _("DNat is disabled for the router %(router_id)s. Floating IPs " + "cannot be associated.") + +EXTENDED_ATTRIBUTES_2_0 = { + 'routers': {l3.EXTERNAL_GW_INFO: + {'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'default': None, + 'validate': + {'type:dict_or_nodata': + {'network_id': {'type:uuid': None, 'required': True}, + 'enable_snat': {'type:boolean': None, 'required': False}} + }}}} + + +class L3_ext_gw_mode(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "Quantum L3 Configurable external gateway mode" + + @classmethod + def get_alias(cls): + return "ext-gw-mode" + + @classmethod + def get_description(cls): + return ("Extension of the router abstraction for specifying whether " + "SNAT, DNAT or both should occur on the external gateway") + + @classmethod + def get_namespace(cls): + return "http://docs.openstack.org/ext/quantum/ext-gw-mode/api/v1.0" + + @classmethod + def get_updated(cls): + return "2013-03-28T10:00:00-00:00" + + def get_required_extensions(self): + return ["router"] + + def get_extended_resources(self, version): + if version == "2.0": + return dict(EXTENDED_ATTRIBUTES_2_0.items()) + else: + return {} diff --git a/quantum/plugins/hyperv/hyperv_quantum_plugin.py b/quantum/plugins/hyperv/hyperv_quantum_plugin.py index a2166247b..1ac84ae61 100644 --- a/quantum/plugins/hyperv/hyperv_quantum_plugin.py +++ b/quantum/plugins/hyperv/hyperv_quantum_plugin.py @@ -22,7 +22,7 @@ from quantum.api.v2 import attributes from quantum.common import exceptions as q_exc from quantum.common import topics from quantum.db import db_base_plugin_v2 -from quantum.db import l3_db +from quantum.db import l3_gwmode_db from quantum.db import quota_db # noqa from quantum.extensions import portbindings from quantum.extensions import providernet as provider @@ -141,13 +141,14 @@ class VlanNetworkProvider(BaseNetworkProvider): class HyperVQuantumPlugin(db_base_plugin_v2.QuantumDbPluginV2, - l3_db.L3_NAT_db_mixin): + l3_gwmode_db.L3_NAT_db_mixin): # This attribute specifies whether the plugin supports or not # bulk operations. Name mangling is used in order to ensure it # is qualified by class __native_bulk_support = True - supported_extension_aliases = ["provider", "router", "binding", "quotas"] + supported_extension_aliases = ["provider", "router", "ext-gw-mode", + "binding", "quotas"] def __init__(self, configfile=None): self._db = hyperv_db.HyperVPluginDB() diff --git a/quantum/plugins/linuxbridge/lb_quantum_plugin.py b/quantum/plugins/linuxbridge/lb_quantum_plugin.py index d6a08b5b5..28df04e59 100644 --- a/quantum/plugins/linuxbridge/lb_quantum_plugin.py +++ b/quantum/plugins/linuxbridge/lb_quantum_plugin.py @@ -32,6 +32,7 @@ from quantum.db import api as db_api from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base from quantum.db import extraroute_db +from quantum.db import l3_gwmode_db from quantum.db import l3_rpc_base from quantum.db import portbindings_db from quantum.db import quota_db # noqa @@ -185,6 +186,7 @@ class AgentNotifierApi(proxy.RpcProxy, class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2, extraroute_db.ExtraRoute_db_mixin, + l3_gwmode_db.L3_NAT_db_mixin, sg_db_rpc.SecurityGroupServerRpcMixin, agentschedulers_db.AgentSchedulerDbMixin, portbindings_db.PortBindingMixin): @@ -211,9 +213,9 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2, __native_pagination_support = True __native_sorting_support = True - _supported_extension_aliases = ["provider", "router", "binding", "quotas", - "security-group", "agent", "extraroute", - "agent_scheduler"] + _supported_extension_aliases = ["provider", "router", "ext-gw-mode", + "binding", "quotas", "security-group", + "agent", "extraroute", "agent_scheduler"] @property def supported_extension_aliases(self): diff --git a/quantum/plugins/metaplugin/meta_quantum_plugin.py b/quantum/plugins/metaplugin/meta_quantum_plugin.py index 9755a5fb1..4732b5840 100644 --- a/quantum/plugins/metaplugin/meta_quantum_plugin.py +++ b/quantum/plugins/metaplugin/meta_quantum_plugin.py @@ -51,7 +51,8 @@ class MetaPluginV2(db_base_plugin_v2.QuantumDbPluginV2, LOG.debug(_("Start initializing metaplugin")) self.supported_extension_aliases = \ cfg.CONF.META.supported_extension_aliases.split(',') - self.supported_extension_aliases += ['flavor', 'router', 'extraroute'] + self.supported_extension_aliases += ['flavor', 'router', + 'ext-gw-mode', 'extraroute'] # Ignore config option overapping def _is_opt_registered(opts, opt): diff --git a/quantum/plugins/nec/nec_plugin.py b/quantum/plugins/nec/nec_plugin.py index 4750ad474..12db44f4e 100644 --- a/quantum/plugins/nec/nec_plugin.py +++ b/quantum/plugins/nec/nec_plugin.py @@ -25,6 +25,7 @@ from quantum.db import agents_db from quantum.db import agentschedulers_db from quantum.db import dhcp_rpc_base from quantum.db import extraroute_db +from quantum.db import l3_gwmode_db from quantum.db import l3_rpc_base from quantum.db import quota_db # noqa from quantum.db import securitygroups_rpc_base as sg_db_rpc @@ -59,6 +60,7 @@ class OperationalStatus: class NECPluginV2(nec_plugin_base.NECPluginV2Base, extraroute_db.ExtraRoute_db_mixin, + l3_gwmode_db.L3_NAT_db_mixin, sg_db_rpc.SecurityGroupServerRpcMixin, agentschedulers_db.AgentSchedulerDbMixin): """NECPluginV2 controls an OpenFlow Controller. @@ -73,10 +75,9 @@ class NECPluginV2(nec_plugin_base.NECPluginV2Base, The port binding extension enables an external application relay information to and from the plugin. """ - _supported_extension_aliases = ["router", "quotas", "binding", - "security-group", "extraroute", - "agent", "agent_scheduler", - ] + _supported_extension_aliases = ["router", "ext-gw-mode", "quotas", + "binding", "security-group", + "extraroute", "agent", "agent_scheduler"] @property def supported_extension_aliases(self): diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index d3d14af12..5e415affc 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -38,6 +38,7 @@ from quantum.db import agentschedulers_db from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base from quantum.db import extraroute_db +from quantum.db import l3_gwmode_db from quantum.db import l3_rpc_base from quantum.db import portbindings_db from quantum.db import quota_db # noqa @@ -214,6 +215,7 @@ class AgentNotifierApi(proxy.RpcProxy, class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2, extraroute_db.ExtraRoute_db_mixin, + l3_gwmode_db.L3_NAT_db_mixin, sg_db_rpc.SecurityGroupServerRpcMixin, agentschedulers_db.AgentSchedulerDbMixin, portbindings_db.PortBindingMixin): @@ -242,7 +244,7 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2, __native_pagination_support = True __native_sorting_support = True - _supported_extension_aliases = ["provider", "router", + _supported_extension_aliases = ["provider", "router", "ext-gw-mode", "binding", "quotas", "security-group", "agent", "extraroute", "agent_scheduler"] diff --git a/quantum/plugins/ryu/ryu_quantum_plugin.py b/quantum/plugins/ryu/ryu_quantum_plugin.py index 3c2ac4968..e96c90256 100644 --- a/quantum/plugins/ryu/ryu_quantum_plugin.py +++ b/quantum/plugins/ryu/ryu_quantum_plugin.py @@ -29,6 +29,7 @@ from quantum.db import api as db from quantum.db import db_base_plugin_v2 from quantum.db import dhcp_rpc_base from quantum.db import extraroute_db +from quantum.db import l3_gwmode_db from quantum.db import l3_rpc_base from quantum.db import models_v2 from quantum.db import securitygroups_rpc_base as sg_db_rpc @@ -86,9 +87,11 @@ class AgentNotifierApi(proxy.RpcProxy, class RyuQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2, extraroute_db.ExtraRoute_db_mixin, + l3_gwmode_db.L3_NAT_db_mixin, sg_db_rpc.SecurityGroupServerRpcMixin): - _supported_extension_aliases = ["router", "extraroute", "security-group"] + _supported_extension_aliases = ["router", "ext-gw-mode", + "extraroute", "security-group"] @property def supported_extension_aliases(self): diff --git a/quantum/tests/unit/metaplugin/fake_plugin.py b/quantum/tests/unit/metaplugin/fake_plugin.py index 4b6f36455..2dac16437 100644 --- a/quantum/tests/unit/metaplugin/fake_plugin.py +++ b/quantum/tests/unit/metaplugin/fake_plugin.py @@ -15,11 +15,11 @@ # under the License. from quantum.db import db_base_plugin_v2 -from quantum.db import l3_db +from quantum.db import l3_gwmode_db class Fake1(db_base_plugin_v2.QuantumDbPluginV2, - l3_db.L3_NAT_db_mixin): + l3_gwmode_db.L3_NAT_db_mixin): supported_extension_aliases = ['router'] def fake_func(self): diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index 90d226072..f16c0c80e 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -45,7 +45,6 @@ from quantum.tests import base from quantum.tests.unit import test_extensions from quantum.tests.unit import testlib_api - DB_PLUGIN_KLASS = 'quantum.db.db_base_plugin_v2.QuantumDbPluginV2' ROOTDIR = os.path.dirname(os.path.dirname(__file__)) ETCDIR = os.path.join(ROOTDIR, 'etc') diff --git a/quantum/tests/unit/test_extension_ext_gw_mode.py b/quantum/tests/unit/test_extension_ext_gw_mode.py new file mode 100644 index 000000000..4a9c84190 --- /dev/null +++ b/quantum/tests/unit/test_extension_ext_gw_mode.py @@ -0,0 +1,404 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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: Salvatore Orlando, Nicira, Inc +# + +import stubout + +import fixtures +import mock +from oslo.config import cfg +from webob import exc + +from quantum.common import constants +from quantum.common.test_lib import test_config +from quantum.db import api as db_api +from quantum.db import l3_db +from quantum.db import l3_gwmode_db +from quantum.db import models_v2 +from quantum.extensions import l3 +from quantum.extensions import l3_ext_gw_mode +from quantum.openstack.common import uuidutils +from quantum.tests import base +from quantum.tests.unit import test_db_plugin +from quantum.tests.unit import test_l3_plugin + +_uuid = uuidutils.generate_uuid +FAKE_GW_PORT_ID = _uuid() +FAKE_GW_PORT_MAC = 'aa:bb:cc:dd:ee:ff' +FAKE_FIP_EXT_PORT_ID = _uuid() +FAKE_FIP_EXT_PORT_MAC = '11:22:33:44:55:66' +FAKE_FIP_INT_PORT_ID = _uuid() +FAKE_FIP_INT_PORT_MAC = 'aa:aa:aa:aa:aa:aa' +FAKE_ROUTER_PORT_ID = _uuid() +FAKE_ROUTER_PORT_MAC = 'bb:bb:bb:bb:bb:bb' + + +class StuboutFixture(fixtures.Fixture): + """Setup stubout and add unsetAll to cleanup.""" + + def setUp(self): + super(StuboutFixture, self).setUp() + self.stubs = stubout.StubOutForTesting() + self.addCleanup(self.stubs.UnsetAll) + self.addCleanup(self.stubs.SmartUnsetAll) + + +def stubout_floating_ip_calls(stubs, fake_count=0): + + def get_floatingips_count(_1, _2, filters): + return fake_count + + stubs.Set(l3_db.L3_NAT_db_mixin, 'get_floatingips_count', + get_floatingips_count) + + +class TestExtensionManager(object): + + def get_resources(self): + # Simulate extension of L3 attribute map + for key in l3.RESOURCE_ATTRIBUTE_MAP.keys(): + l3.RESOURCE_ATTRIBUTE_MAP[key].update( + l3_ext_gw_mode.EXTENDED_ATTRIBUTES_2_0.get(key, {})) + return l3.L3.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +# A simple class for making a concrete class out of the mixin +class TestDbPlugin(test_l3_plugin.TestL3NatPlugin, + l3_gwmode_db.L3_NAT_db_mixin): + + supported_extension_aliases = ["router", "ext-gw-mode"] + + +class TestL3GwModeMixin(base.BaseTestCase): + + def setUp(self): + super(TestL3GwModeMixin, self).setUp() + stubout_fixture = self.useFixture(StuboutFixture()) + self.stubs = stubout_fixture.stubs + self.target_object = TestDbPlugin() + # Patch the context + ctx_patcher = mock.patch('quantum.context', autospec=True) + mock_context = ctx_patcher.start() + self.addCleanup(db_api.clear_db) + self.addCleanup(ctx_patcher.stop) + self.context = mock_context.get_admin_context() + # This ensure also calls to elevated work in unit tests + self.context.elevated.return_value = self.context + self.context.session = db_api.get_session() + # Create sample data for tests + self.ext_net_id = _uuid() + self.int_net_id = _uuid() + self.int_sub_id = _uuid() + self.tenant_id = 'the_tenant' + self.network = models_v2.Network( + id=self.ext_net_id, + tenant_id=self.tenant_id, + admin_state_up=True, + status=constants.NET_STATUS_ACTIVE) + self.net_ext = l3_db.ExternalNetwork(network_id=self.ext_net_id) + self.context.session.add(self.network) + # The following is to avoid complains from sqlite on + # foreign key violations + self.context.session.flush() + self.context.session.add(self.net_ext) + self.router = l3_db.Router( + id=_uuid(), + name=None, + tenant_id=self.tenant_id, + admin_state_up=True, + status=constants.NET_STATUS_ACTIVE, + enable_snat=True, + gw_port_id=None) + self.context.session.add(self.router) + self.context.session.flush() + self.router_gw_port = models_v2.Port( + id=FAKE_GW_PORT_ID, + tenant_id=self.tenant_id, + device_id=self.router.id, + device_owner=l3_db.DEVICE_OWNER_ROUTER_GW, + admin_state_up=True, + status=constants.PORT_STATUS_ACTIVE, + mac_address=FAKE_GW_PORT_MAC, + network_id=self.ext_net_id) + self.router.gw_port_id = self.router_gw_port.id + self.context.session.add(self.router) + self.context.session.add(self.router_gw_port) + self.context.session.flush() + self.fip_ext_port = models_v2.Port( + id=FAKE_FIP_EXT_PORT_ID, + tenant_id=self.tenant_id, + admin_state_up=True, + device_id=self.router.id, + device_owner=l3_db.DEVICE_OWNER_FLOATINGIP, + status=constants.PORT_STATUS_ACTIVE, + mac_address=FAKE_FIP_EXT_PORT_MAC, + network_id=self.ext_net_id) + self.context.session.add(self.fip_ext_port) + self.context.session.flush() + self.int_net = models_v2.Network( + id=self.int_net_id, + tenant_id=self.tenant_id, + admin_state_up=True, + status=constants.NET_STATUS_ACTIVE) + self.int_sub = models_v2.Subnet( + id=self.int_sub_id, + tenant_id=self.tenant_id, + ip_version=4, + cidr='3.3.3.0/24', + gateway_ip='3.3.3.1', + network_id=self.int_net_id) + self.router_port = models_v2.Port( + id=FAKE_ROUTER_PORT_ID, + tenant_id=self.tenant_id, + admin_state_up=True, + device_id=self.router.id, + device_owner=l3_db.DEVICE_OWNER_ROUTER_INTF, + status=constants.PORT_STATUS_ACTIVE, + mac_address=FAKE_ROUTER_PORT_MAC, + network_id=self.int_net_id) + self.router_port_ip_info = models_v2.IPAllocation( + port_id=self.router_port.id, + network_id=self.int_net.id, + subnet_id=self.int_sub_id, + ip_address='3.3.3.1') + self.context.session.add(self.int_net) + self.context.session.add(self.int_sub) + self.context.session.add(self.router_port) + self.context.session.add(self.router_port_ip_info) + self.context.session.flush() + self.fip_int_port = models_v2.Port( + id=FAKE_FIP_INT_PORT_ID, + tenant_id=self.tenant_id, + admin_state_up=True, + device_id='something', + device_owner='compute:nova', + status=constants.PORT_STATUS_ACTIVE, + mac_address=FAKE_FIP_INT_PORT_MAC, + network_id=self.int_net_id) + self.fip_int_ip_info = models_v2.IPAllocation( + port_id=self.fip_int_port.id, + network_id=self.int_net.id, + subnet_id=self.int_sub_id, + ip_address='3.3.3.3') + self.fip = l3_db.FloatingIP( + id=_uuid(), + floating_ip_address='1.1.1.2', + floating_network_id=self.ext_net_id, + floating_port_id=FAKE_FIP_EXT_PORT_ID, + fixed_port_id=None, + fixed_ip_address=None, + router_id=None) + self.context.session.add(self.fip_int_port) + self.context.session.add(self.fip_int_ip_info) + self.context.session.add(self.fip) + self.context.session.flush() + self.fip_request = {'port_id': FAKE_FIP_INT_PORT_ID, + 'tenant_id': self.tenant_id} + + def _reset_ext_gw(self): + # Reset external gateway + self.router.gw_port_id = None + self.context.session.add(self.router) + self.context.session.flush() + + def _test_update_router_gw(self, gw_info, expected_enable_snat): + self.target_object._update_router_gw_info( + self.context, self.router.id, gw_info) + router = self.target_object._get_router( + self.context, self.router.id) + try: + self.assertEqual(FAKE_GW_PORT_ID, + router.gw_port.id) + self.assertEqual(FAKE_GW_PORT_MAC, + router.gw_port.mac_address) + except AttributeError: + self.assertIsNone(router.gw_port) + self.assertEqual(expected_enable_snat, router.enable_snat) + + def test_update_router_gw_with_gw_info_none(self): + self._test_update_router_gw(None, True) + + def test_update_router_gw_with_network_only(self): + info = {'network_id': self.ext_net_id} + self._test_update_router_gw(info, True) + + def test_update_router_gw_with_snat_disabled(self): + info = {'network_id': self.ext_net_id, + 'enable_snat': False} + self._test_update_router_gw(info, False) + + def test_make_router_dict_no_ext_gw(self): + self._reset_ext_gw() + router_dict = self.target_object._make_router_dict(self.router) + self.assertEqual(None, router_dict[l3.EXTERNAL_GW_INFO]) + + def test_make_router_dict_with_ext_gw(self): + router_dict = self.target_object._make_router_dict(self.router) + self.assertEqual({'network_id': self.ext_net_id, + 'enable_snat': True}, + router_dict[l3.EXTERNAL_GW_INFO]) + + def test_make_router_dict_with_ext_gw_snat_disabled(self): + self.router.enable_snat = False + router_dict = self.target_object._make_router_dict(self.router) + self.assertEqual({'network_id': self.ext_net_id, + 'enable_snat': False}, + router_dict[l3.EXTERNAL_GW_INFO]) + + def test_build_routers_list_no_ext_gw(self): + self._reset_ext_gw() + router_dict = self.target_object._make_router_dict(self.router) + routers = self.target_object._build_routers_list([router_dict], []) + self.assertEqual(1, len(routers)) + router = routers[0] + self.assertIsNone(router.get('gw_port')) + self.assertIsNone(router.get('enable_snat')) + + def test_build_routers_list_with_ext_gw(self): + router_dict = self.target_object._make_router_dict(self.router) + routers = self.target_object._build_routers_list( + [router_dict], [self.router.gw_port]) + self.assertEqual(1, len(routers)) + router = routers[0] + self.assertIsNotNone(router.get('gw_port')) + self.assertEqual(FAKE_GW_PORT_ID, router['gw_port']['id']) + self.assertTrue(router.get('enable_snat')) + + def test_build_routers_list_with_ext_gw_snat_disabled(self): + self.router.enable_snat = False + router_dict = self.target_object._make_router_dict(self.router) + routers = self.target_object._build_routers_list( + [router_dict], [self.router.gw_port]) + self.assertEqual(1, len(routers)) + router = routers[0] + self.assertIsNotNone(router.get('gw_port')) + self.assertEqual(FAKE_GW_PORT_ID, router['gw_port']['id']) + self.assertFalse(router.get('enable_snat')) + + +class ExtGwModeTestCase(test_db_plugin.QuantumDbPluginV2TestCase, + test_l3_plugin.L3NatTestCaseMixin): + + def setUp(self): + # Store l3 resource attribute map as it's will be updated + self._l3_attribute_map_bk = {} + for item in l3.RESOURCE_ATTRIBUTE_MAP: + self._l3_attribute_map_bk[item] = ( + l3.RESOURCE_ATTRIBUTE_MAP[item].copy()) + test_config['plugin_name_v2'] = ( + 'quantum.tests.unit.test_extension_ext_gw_mode.TestDbPlugin') + test_config['extension_manager'] = TestExtensionManager() + # for these tests we need to enable overlapping ips + cfg.CONF.set_default('allow_overlapping_ips', True) + super(ExtGwModeTestCase, self).setUp() + self.addCleanup(self.restore_l3_attribute_map) + + def restore_l3_attribute_map(self): + l3.RESOURCE_ATTRIBUTE_MAP = self._l3_attribute_map_bk + + def tearDown(self): + super(ExtGwModeTestCase, self).tearDown() + + def _set_router_external_gateway(self, router_id, network_id, + snat_enabled=None, + expected_code=exc.HTTPOk.code, + quantum_context=None): + ext_gw_info = {'network_id': network_id} + if snat_enabled in (True, False): + ext_gw_info['enable_snat'] = snat_enabled + return self._update('routers', router_id, + {'router': {'external_gateway_info': + ext_gw_info}}, + expected_code=expected_code, + quantum_context=quantum_context) + + def test_router_create_show_no_ext_gwinfo(self): + name = 'router1' + tenant_id = _uuid() + expected_value = [('name', name), ('tenant_id', tenant_id), + ('admin_state_up', True), ('status', 'ACTIVE'), + ('external_gateway_info', None)] + with self.router(name=name, admin_state_up=True, + tenant_id=tenant_id) as router: + res = self._show('routers', router['router']['id']) + for k, v in expected_value: + self.assertEqual(res['router'][k], v) + + def _test_router_create_show_ext_gwinfo(self, snat_input_value, + snat_expected_value): + name = 'router1' + tenant_id = _uuid() + with self.subnet() as s: + ext_net_id = s['subnet']['network_id'] + self._set_net_external(ext_net_id) + input_value = {'network_id': ext_net_id} + if snat_input_value in (True, False): + input_value['enable_snat'] = snat_input_value + expected_value = [('name', name), ('tenant_id', tenant_id), + ('admin_state_up', True), ('status', 'ACTIVE'), + ('external_gateway_info', + {'network_id': ext_net_id, + 'enable_snat': snat_expected_value})] + with self.router( + name=name, admin_state_up=True, tenant_id=tenant_id, + external_gateway_info=input_value) as router: + res = self._show('routers', router['router']['id']) + for k, v in expected_value: + self.assertEqual(res['router'][k], v) + + def test_router_create_show_ext_gwinfo_default(self): + self._test_router_create_show_ext_gwinfo(None, True) + + def test_router_create_show_ext_gwinfo_with_snat_enabled(self): + self._test_router_create_show_ext_gwinfo(True, True) + + def test_router_create_show_ext_gwinfo_with_snat_disabled(self): + self._test_router_create_show_ext_gwinfo(False, False) + + def _test_router_update_ext_gwinfo(self, snat_input_value, + snat_expected_value): + with self.router() as r: + with self.subnet() as s: + ext_net_id = s['subnet']['network_id'] + self._set_net_external(ext_net_id) + self._set_router_external_gateway( + r['router']['id'], ext_net_id, + snat_enabled=snat_input_value) + body = self._show('routers', r['router']['id']) + res_gw_info = body['router']['external_gateway_info'] + self.assertEqual(res_gw_info['network_id'], ext_net_id) + self.assertEqual(res_gw_info['enable_snat'], + snat_expected_value) + self._remove_external_gateway_from_router( + r['router']['id'], ext_net_id) + + def test_router_update_ext_gwinfo_default(self): + self._test_router_update_ext_gwinfo(None, True) + + def test_router_update_ext_gwinfo_with_snat_enabled(self): + self._test_router_update_ext_gwinfo(True, True) + + def test_router_update_ext_gwinfo_with_snat_disabled(self): + self._test_router_update_ext_gwinfo(False, False) diff --git a/quantum/tests/unit/test_l3_agent.py b/quantum/tests/unit/test_l3_agent.py index 95211f3ca..eeefde451 100644 --- a/quantum/tests/unit/test_l3_agent.py +++ b/quantum/tests/unit/test_l3_agent.py @@ -92,6 +92,24 @@ class TestBasicRouterOperations(base.BaseTestCase): self.assertTrue(ri.ns_name().endswith(id)) + def test_router_info_create_with_router(self): + id = _uuid() + ex_gw_port = {'id': _uuid(), + 'network_id': _uuid(), + 'fixed_ips': [{'ip_address': '19.4.4.4', + 'subnet_id': _uuid()}], + 'subnet': {'cidr': '19.4.4.0/24', + 'gateway_ip': '19.4.4.1'}} + router = { + 'id': _uuid(), + 'enable_snat': True, + 'routes': [], + 'gw_port': ex_gw_port} + ri = l3_agent.RouterInfo(id, self.conf.root_helper, + self.conf.use_namespaces, router) + self.assertTrue(ri.ns_name().endswith(id)) + self.assertEqual(ri.router, router) + def testAgentCreate(self): l3_agent.L3NATAgent(HOSTNAME, self.conf) @@ -104,17 +122,16 @@ class TestBasicRouterOperations(base.BaseTestCase): agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) 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, network_id, + agent.internal_network_added(ri, network_id, port_id, cidr, mac) self.assertEqual(self.mock_driver.plug.call_count, 1) self.assertEqual(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) + agent.internal_network_removed(ri, port_id, cidr) self.assertEqual(self.mock_driver.unplug.call_count, 1) else: raise Exception("Invalid action %s" % action) @@ -142,7 +159,8 @@ class TestBasicRouterOperations(base.BaseTestCase): if action == 'add': self.device_exists.return_value = False - agent.external_gateway_added(ri, ex_gw_port, internal_cidrs) + agent.external_gateway_added(ri, ex_gw_port, + interface_name, internal_cidrs) self.assertEqual(self.mock_driver.plug.call_count, 1) self.assertEqual(self.mock_driver.init_l3.call_count, 1) arping_cmd = ['arping', '-A', '-U', @@ -158,7 +176,8 @@ class TestBasicRouterOperations(base.BaseTestCase): elif action == 'remove': self.device_exists.return_value = True - agent.external_gateway_removed(ri, ex_gw_port, internal_cidrs) + agent.external_gateway_removed(ri, ex_gw_port, + interface_name, internal_cidrs) self.assertEqual(self.mock_driver.unplug.call_count, 1) else: raise Exception("Invalid action %s" % action) @@ -311,9 +330,28 @@ class TestBasicRouterOperations(base.BaseTestCase): 'via', '10.100.10.30']] self._check_agent_method_called(agent, expected, namespace) - def testProcessRouter(self): - - agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + def _verify_snat_rules(self, rules, router): + interfaces = router[l3_constants.INTERFACE_KEY] + source_cidrs = [] + for interface in interfaces: + prefix = interface['subnet']['cidr'].split('/')[1] + source_cidr = "%s/%s" % (interface['fixed_ips'][0]['ip_address'], + prefix) + source_cidrs.append(source_cidr) + source_nat_ip = router['gw_port']['fixed_ips'][0]['ip_address'] + interface_name = ('qg-%s' % router['gw_port']['id'])[:14] + expected_rules = [ + '! -i %s ! -o %s -m conntrack ! --ctstate DNAT -j ACCEPT' % + (interface_name, interface_name)] + for source_cidr in source_cidrs: + value_dict = {'source_cidr': source_cidr, + 'source_nat_ip': source_nat_ip} + expected_rules.append('-s %(source_cidr)s -j SNAT --to-source ' + '%(source_nat_ip)s' % value_dict) + for r in rules: + self.assertIn(r.rule, expected_rules) + + def _prepare_router_data(self, enable_snat=True): router_id = _uuid() ex_gw_port = {'id': _uuid(), 'network_id': _uuid(), @@ -330,19 +368,23 @@ class TestBasicRouterOperations(base.BaseTestCase): 'subnet': {'cidr': '35.4.4.0/24', 'gateway_ip': '35.4.4.1'}} - fake_floatingips1 = {'floatingips': [ - {'id': _uuid(), - 'floating_ip_address': '8.8.8.8', - 'fixed_ip_address': '7.7.7.7', - 'port_id': _uuid()}]} - router = { 'id': router_id, - l3_constants.FLOATINGIP_KEY: fake_floatingips1['floatingips'], l3_constants.INTERFACE_KEY: [internal_port], + 'enable_snat': enable_snat, 'routes': [], 'gw_port': ex_gw_port} - ri = l3_agent.RouterInfo(router_id, self.conf.root_helper, + return router + + def testProcessRouter(self): + agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + router = self._prepare_router_data() + fake_floatingips1 = {'floatingips': [ + {'id': _uuid(), + 'floating_ip_address': '8.8.8.8', + 'fixed_ip_address': '7.7.7.7', + 'port_id': _uuid()}]} + ri = l3_agent.RouterInfo(router['id'], self.conf.root_helper, self.conf.use_namespaces, router=router) agent.process_router(ri) @@ -362,6 +404,44 @@ class TestBasicRouterOperations(base.BaseTestCase): del router['gw_port'] agent.process_router(ri) + def test_process_router_snat_disabled(self): + + agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + router = self._prepare_router_data() + ri = l3_agent.RouterInfo(router['id'], self.conf.root_helper, + self.conf.use_namespaces, router=router) + # Process with NAT + agent.process_router(ri) + orig_nat_rules = ri.iptables_manager.ipv4['nat'].rules[:] + # Reprocess without NAT + router['enable_snat'] = False + # Reassign the router object to RouterInfo + ri.router = router + agent.process_router(ri) + nat_rules_delta = (set(orig_nat_rules) - + set(ri.iptables_manager.ipv4['nat'].rules)) + self.assertEqual(len(nat_rules_delta), 2) + self._verify_snat_rules(nat_rules_delta, router) + + def test_process_router_snat_enabled(self): + + agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + router = self._prepare_router_data(enable_snat=False) + ri = l3_agent.RouterInfo(router['id'], self.conf.root_helper, + self.conf.use_namespaces, router=router) + # Process with NAT + agent.process_router(ri) + orig_nat_rules = ri.iptables_manager.ipv4['nat'].rules[:] + # Reprocess without NAT + router['enable_snat'] = True + # Reassign the router object to RouterInfo + ri.router = router + agent.process_router(ri) + nat_rules_delta = (set(ri.iptables_manager.ipv4['nat'].rules) - + set(orig_nat_rules)) + self.assertEqual(len(nat_rules_delta), 2) + self._verify_snat_rules(nat_rules_delta, router) + def testRoutersWithAdminStateDown(self): agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) self.plugin_api.get_external_network_id.return_value = None diff --git a/quantum/tests/unit/test_l3_plugin.py b/quantum/tests/unit/test_l3_plugin.py index e71055539..8ab4c838a 100644 --- a/quantum/tests/unit/test_l3_plugin.py +++ b/quantum/tests/unit/test_l3_plugin.py @@ -342,10 +342,14 @@ class L3NatTestCaseMixin(object): return router_req.get_response(self.ext_api) - def _make_router(self, fmt, tenant_id, name=None, - admin_state_up=None, set_context=False): + def _make_router(self, fmt, tenant_id, name=None, admin_state_up=None, + external_gateway_info=None, set_context=False): + arg_list = (external_gateway_info and + ('external_gateway_info', ) or None) res = self._create_router(fmt, tenant_id, name, - admin_state_up, set_context) + admin_state_up, set_context, + arg_list=arg_list, + external_gateway_info=external_gateway_info) return self.deserialize(fmt, res) def _add_external_gateway_to_router(self, router_id, network_id, @@ -384,9 +388,11 @@ class L3NatTestCaseMixin(object): @contextlib.contextmanager def router(self, name='router1', admin_state_up=True, - fmt=None, tenant_id=_uuid(), set_context=False): + fmt=None, tenant_id=_uuid(), + external_gateway_info=None, set_context=False): router = self._make_router(fmt or self.fmt, tenant_id, name, - admin_state_up, set_context) + admin_state_up, external_gateway_info, + set_context) try: yield router finally: -- 2.45.2