diff --git a/quantum/agent/l3_agent.py b/quantum/agent/l3_agent.py index 55bfe2908c..b4e31b3ea6 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) - if ri.ex_gw_port or ex_gw_port: + # Process SNAT rules for external gateway + if ex_gw_port_id: + ri.perform_snat_action(self._handle_router_snat_rules, + internal_cidrs, interface_name) + + # 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 55f37def6d..a978376f7c 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 cea8d2f876..fc72b569dd 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. @@ -130,11 +131,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) @@ -143,10 +144,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 @@ -164,10 +165,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) @@ -180,14 +181,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): @@ -202,11 +227,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): @@ -216,30 +242,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): @@ -512,14 +515,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']: @@ -561,7 +561,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, @@ -838,6 +850,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. @@ -865,14 +886,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 0000000000..d91bd3d663 --- /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 0000000000..67fa20b8bc --- /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 99691b1bf3..29379a10e0 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 0000000000..1e53c473b5 --- /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 a2166247b2..1ac84ae61b 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 d6a08b5b5b..28df04e599 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 9755a5fb18..4732b58402 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 4750ad4749..12db44f4e4 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 70e2a5c55f..417fce61f7 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 74468cfeba..40ee96cff1 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 4b6f364553..2dac164379 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 2b7269a7c1..d2280a03ea 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 0000000000..4a9c841906 --- /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 95211f3ca7..eeefde4519 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): + 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) - agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + 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'}} + router = { + 'id': router_id, + l3_constants.INTERFACE_KEY: [internal_port], + 'enable_snat': enable_snat, + 'routes': [], + 'gw_port': ex_gw_port} + 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()}]} - - router = { - 'id': router_id, - l3_constants.FLOATINGIP_KEY: fake_floatingips1['floatingips'], - l3_constants.INTERFACE_KEY: [internal_port], - 'routes': [], - 'gw_port': ex_gw_port} - ri = l3_agent.RouterInfo(router_id, self.conf.root_helper, + 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 e710555390..8ab4c838a1 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: