]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Configurable external gateway modes
authorSalvatore Orlando <salv.orlando@gmail.com>
Mon, 18 Feb 2013 09:57:39 +0000 (10:57 +0100)
committerSalvatore Orlando <salv.orlando@gmail.com>
Mon, 27 May 2013 18:59:09 +0000 (20:59 +0200)
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

18 files changed:
quantum/agent/l3_agent.py
quantum/api/v2/attributes.py
quantum/db/l3_db.py
quantum/db/l3_gwmode_db.py [new file with mode: 0644]
quantum/db/migration/alembic_migrations/versions/128e042a2b68_ext_gw_mode.py [new file with mode: 0644]
quantum/extensions/l3.py
quantum/extensions/l3_ext_gw_mode.py [new file with mode: 0644]
quantum/plugins/hyperv/hyperv_quantum_plugin.py
quantum/plugins/linuxbridge/lb_quantum_plugin.py
quantum/plugins/metaplugin/meta_quantum_plugin.py
quantum/plugins/nec/nec_plugin.py
quantum/plugins/openvswitch/ovs_quantum_plugin.py
quantum/plugins/ryu/ryu_quantum_plugin.py
quantum/tests/unit/metaplugin/fake_plugin.py
quantum/tests/unit/test_db_plugin.py
quantum/tests/unit/test_extension_ext_gw_mode.py [new file with mode: 0644]
quantum/tests/unit/test_l3_agent.py
quantum/tests/unit/test_l3_plugin.py

index 55bfe2908c80bc6a2141d3f622c86f77641ce090..b4e31b3ea6ed672a82cfef2e872b3157ca728659 100644 (file)
@@ -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'])
index 55f37def6df48cb0bc3e16faa8a14bbe4c0cc709..a978376f7c3298dd86730f264f713b51885db5c2 100644 (file)
@@ -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'
index 19a363622e344e2c05d4d9153b2f1a0aa7cbe776..c720b4de93dec6123777d31e035c9198298dbcb8 100644 (file)
@@ -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 (file)
index 0000000..d91bd3d
--- /dev/null
@@ -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 (file)
index 0000000..67fa20b
--- /dev/null
@@ -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')
index ba5285318eb516c37c6654bff8173bae16ffdb94..393abeac72d13f17d6751bf0d5f15a76fdeacdf5 100644 (file)
@@ -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 (file)
index 0000000..1e53c47
--- /dev/null
@@ -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 {}
index a2166247b2dabea759e117b371e70f336e935257..1ac84ae61bc6d48fd7a34f59b3df3cc5dd2cee43 100644 (file)
@@ -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()
index d6a08b5b5b1581cc65b20f01df78dbfb2eb43594..28df04e5995a47e82d4b61fcd14711ff6fc1b4e8 100644 (file)
@@ -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):
index 9755a5fb181deeb77f9db4992014e66024d39984..4732b58402dd4e227ec6e7efc5ebd78b994e557e 100644 (file)
@@ -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):
index 4750ad47497f9c8fedd6f8e336baddc4aad75654..12db44f4e4076e9418cbf0e8fa35ab58b3d75a8a 100644 (file)
@@ -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):
index d3d14af12ed7ce51edb7672ef180ca537c65058e..5e415affc8bc2e9924d049f1083bcb0aa0b46c7c 100644 (file)
@@ -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"]
 
index 3c2ac49689c430f2aaff65c9d9ba229b6b4b8a34..e96c9025669935480e6841f593d4e2d4fa9a5d89 100644 (file)
@@ -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):
index 4b6f3645534f8132260df24d9e0a5a70bacc6242..2dac164379b865d50e3cca7e7837ee88987356d7 100644 (file)
 #    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):
index 90d2260720371461e68dc5fc4487ab306494fcf3..f16c0c80e60639f296f0d0b7868845b44b73014f 100644 (file)
@@ -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 (file)
index 0000000..4a9c841
--- /dev/null
@@ -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)
index 95211f3ca7aec839b1cab94bbd4b00e7cce6f16f..eeefde451961f102eab2f02cbc299e1d90dbaa67 100644 (file)
@@ -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
index e71055539011ab625b985fc676cb844da06adbf3..8ab4c838a1c63d3b48539f4d94457b963ee45551 100644 (file)
@@ -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: