]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Metadata support for NVP plugin
authorSalvatore Orlando <salv.orlando@gmail.com>
Thu, 14 Feb 2013 00:46:43 +0000 (16:46 -0800)
committerSalvatore Orlando <salv.orlando@gmail.com>
Mon, 18 Feb 2013 18:24:58 +0000 (19:24 +0100)
Bug #1121119

Allows the NVP plugin to leverage the metadata proxy, by creating an
ad-hoc topology for allowing access to a metadata proxy from a NVP
router leveraging existing agents.

This patch also removes previous code for metadata support in the
NVP plugin, which was based on DHCP Option 121. This is now provided
by the dhcp agent as well.

Change-Id: If37ef388e063f40bb06908ee2f72c431f29dac31

etc/dhcp_agent.ini
etc/quantum/plugins/nicira/nvp.ini
quantum/agent/dhcp_agent.py
quantum/plugins/nicira/nicira_nvp_plugin/QuantumPlugin.py
quantum/plugins/nicira/nicira_nvp_plugin/common/config.py
quantum/plugins/nicira/nicira_nvp_plugin/common/metadata_access.py [new file with mode: 0644]
quantum/tests/unit/nicira/test_nicira_plugin.py
quantum/tests/unit/test_dhcp_agent.py
quantum/tests/unit/test_l3_plugin.py

index 0422baf84794d3a6e07103be86702ccaface419a..a3c798f3859351974af3c6a267f335ea33eeb553 100644 (file)
@@ -30,9 +30,16 @@ dhcp_driver = quantum.agent.linux.dhcp.Dnsmasq
 # iproute2 package that supports namespaces).
 # use_namespaces = True
 
-#  The DHCP server can assist with providing metadata support on isolated
-#  networks. Setting this value to True will cause the DHCP server to append
-#  specific host routes to the DHCP request.  The metadata service will only
-#  be activated when the subnet gateway_ip is None.  The guest instance must
-#  be configured to request host routes via DHCP (Option 121).
+# The DHCP server can assist with providing metadata support on isolated
+# networks. Setting this value to True will cause the DHCP server to append
+# specific host routes to the DHCP request.  The metadata service will only
+# be activated when the subnet gateway_ip is None.  The guest instance must
+# be configured to request host routes via DHCP (Option 121).
 # enable_isolated_metadata = False
+# Allows for serving metadata requests coming from a dedicated metadata
+# access network whose cidr is 169.254.169.254/16 (or larger prefix), and
+# is connected to a Quantum router from which the VMs send metadata
+# request. In this case DHCP Option 121 will not be injected in VMs, as
+# they will be able to reach 169.254.169.254 through a router.
+# This option requires enable_isolated_metadata = True
+# enable_metadata_network = False
index 7a6b6a065522764a2707b11d3c61e4cc6e09d357..54775489b95eb4467bc11f6b79ab19f442201f44 100644 (file)
@@ -1,10 +1,3 @@
-[DEFAULT]
-
-# The following flag will cause a host route to the metadata server
-# to be injected into instances. The metadata server will be reached
-# via the dhcp server.
-metadata_dhcp_host_route = False
-
 [DATABASE]
 # This line MUST be changed to actually run the plugin.
 # Example:
@@ -39,6 +32,9 @@ reconnect_interval = 2
 # is not specified. If it is empty or reference a non-existent cluster
 # the first cluster specified in this configuration file will be used
 # default_cluster_name =
+# The following flag enables the creation of a dedicated connection
+# to the metadata proxy for metadata server access via Quantum router
+# enable_metadata_access_network = True
 
 #[CLUSTER:example]
 # This is uuid of the default NVP Transport zone that will be used for
index 97c384f0b4cec87d46002b622ddf10c98682832d..3ea81a87d39dafcf3c88b57c681c708073f5dda9 100644 (file)
@@ -29,6 +29,7 @@ from quantum.agent.linux import external_process
 from quantum.agent.linux import interface
 from quantum.agent.linux import ip_lib
 from quantum.agent import rpc as agent_rpc
+from quantum.common import constants
 from quantum.common import exceptions
 from quantum.common import topics
 from quantum import context
@@ -40,7 +41,8 @@ from quantum.openstack.common import uuidutils
 
 LOG = logging.getLogger(__name__)
 NS_PREFIX = 'qdhcp-'
-METADATA_DEFAULT_IP = '169.254.169.254/16'
+METADATA_DEFAULT_PREFIX = 16
+METADATA_DEFAULT_IP = '169.254.169.254/%d' % METADATA_DEFAULT_PREFIX
 METADATA_PORT = 80
 
 
@@ -54,7 +56,11 @@ class DhcpAgent(object):
         cfg.BoolOpt('use_namespaces', default=True,
                     help=_("Allow overlapping IP.")),
         cfg.BoolOpt('enable_isolated_metadata', default=False,
-                    help=_("Support Metadata requests on isolated networks."))
+                    help=_("Support Metadata requests on isolated networks.")),
+        cfg.BoolOpt('enable_metadata_network', default=False,
+                    help=_("Allows for serving metadata requests from a "
+                           "dedicate network. Requires "
+                           "enable isolated_metadata = True "))
     ]
 
     def __init__(self, conf):
@@ -245,13 +251,37 @@ class DhcpAgent(object):
             self.call_driver('reload_allocations', network)
 
     def enable_isolated_metadata_proxy(self, network):
+
+        # The proxy might work for either a single network
+        # or all the networks connected via a router
+        # to the one passed as a parameter
+        quantum_lookup_param = '--network_id=%s' % network.id
+        meta_cidr = netaddr.IPNetwork(METADATA_DEFAULT_IP)
+        has_metadata_subnet = any(netaddr.IPNetwork(s.cidr) in meta_cidr
+                                  for s in network.subnets)
+        if (self.conf.enable_metadata_network and has_metadata_subnet):
+            router_ports = [port for port in network.ports
+                            if (port.device_owner ==
+                                constants.DEVICE_OWNER_ROUTER_INTF)]
+            if router_ports:
+                # Multiple router ports should not be allowed
+                if len(router_ports) > 1:
+                    LOG.warning(_("%(port_num)d router ports found on the "
+                                  "metadata access network. Only the port "
+                                  "%(port_id)s, for router %(router_id)s "
+                                  "will be considered"),
+                                {'port_num': len(router_ports),
+                                 'port_id': router_ports[0].id,
+                                 'router_id': router_ports[0].device_id})
+                quantum_lookup_param = ('--router_id=%s' %
+                                        router_ports[0].device_id)
+
         def callback(pid_file):
             return ['quantum-ns-metadata-proxy',
                     '--pid_file=%s' % pid_file,
-                    '--network_id=%s' % network.id,
+                    quantum_lookup_param,
                     '--state_path=%s' % self.conf.state_path,
                     '--metadata_port=%d' % METADATA_PORT]
-
         pm = external_process.ProcessManager(
             self.conf,
             network.id,
@@ -480,7 +510,9 @@ class DeviceManager(object):
             ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
             ip_cidrs.append(ip_cidr)
 
-        if self.conf.enable_isolated_metadata and self.conf.use_namespaces:
+        if (self.conf.enable_isolated_metadata and
+            self.conf.use_namespaces and
+            not self.conf.enable_metadata_network):
             ip_cidrs.append(METADATA_DEFAULT_IP)
 
         self.driver.init_l3(interface_name, ip_cidrs,
@@ -492,6 +524,19 @@ class DeviceManager(object):
                                      self.root_helper)
             device.route.pullup_route(interface_name)
 
+        if self.conf.enable_metadata_network:
+            meta_cidr = netaddr.IPNetwork(METADATA_DEFAULT_IP)
+            metadata_subnets = [s for s in network.subnets if
+                                netaddr.IPNetwork(s.cidr) in meta_cidr]
+            if metadata_subnets:
+                # Add a gateway so that packets can be routed back to VMs
+                device = ip_lib.IPDevice(interface_name,
+                                         self.root_helper,
+                                         namespace)
+                # Only 1 subnet on metadata access network
+                gateway_ip = metadata_subnets[0].gateway_ip
+                device.route.add_gateway(gateway_ip)
+
         return interface_name
 
     def destroy(self, network, device_name):
index 2a9c742d0be8a943cdb14e33b39884546558425f..1a4d1b19a6be4b6bb074044d402e9541e1e74524 100644 (file)
@@ -47,6 +47,8 @@ from quantum.extensions import portsecurity as psec
 from quantum.extensions import providernet as pnet
 from quantum.extensions import securitygroup as ext_sg
 from quantum.openstack.common import rpc
+from quantum.plugins.nicira.nicira_nvp_plugin.common import (metadata_access
+                                                             as nvp_meta)
 from quantum.plugins.nicira.nicira_nvp_plugin.common import (securitygroups
                                                              as nvp_sec)
 from quantum import policy
@@ -84,7 +86,11 @@ def parse_config():
         NVPCluster objects, 'plugin_config' is a dictionary with plugin
         parameters (currently only 'max_lp_per_bridged_ls').
     """
-    nvp_options = cfg.CONF.NVP
+    # Warn if metadata_dhcp_host_route option is specified
+    if cfg.CONF.metadata_dhcp_host_route:
+        LOG.warning(_("The metadata_dhcp_host_route is now obsolete, and "
+                      "will have no effect. Instead, please set the "
+                      "enable_isolated_metadata option in dhcp_agent.ini"))
     nvp_conf = config.ClusterConfigOptions(cfg.CONF)
     cluster_names = config.register_cluster_groups(nvp_conf)
     nvp_conf.log_opt_values(LOG, logging.DEBUG)
@@ -104,7 +110,7 @@ def parse_config():
              'default_l3_gw_service_uuid':
              nvp_conf[cluster_name].default_l3_gw_service_uuid})
     LOG.debug(_("Cluster options:%s"), clusters_options)
-    return nvp_options, clusters_options
+    return cfg.CONF.NVP, clusters_options
 
 
 class NVPRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
@@ -125,7 +131,9 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                   l3_db.L3_NAT_db_mixin,
                   portsecurity_db.PortSecurityDbMixin,
                   securitygroups_db.SecurityGroupDbMixin,
-                  nvp_sec.NVPSecurityGroups, qos_db.NVPQoSDbMixin):
+                  nvp_sec.NVPSecurityGroups,
+                  qos_db.NVPQoSDbMixin,
+                  nvp_meta.NvpMetadataAccess):
     """
     NvpPluginV2 is a Quantum plugin that provides L2 Virtual Network
     functionality using NVP.
@@ -671,26 +679,6 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                         "logical network %s"), network.id)
             raise nvp_exc.NvpNoMorePortsException(network=network.id)
 
-    def _ensure_metadata_host_route(self, context, fixed_ip_data,
-                                    is_delete=False):
-        subnet = self._get_subnet(context, fixed_ip_data['subnet_id'])
-        metadata_routes = [r for r in subnet.routes
-                           if r['destination'] == '169.254.169.254/32']
-        if metadata_routes:
-            # We should have only a single metadata route at any time
-            # because the route logic forbids two routes with the same
-            # destination. Update next hop with the provided IP address
-            if not is_delete:
-                metadata_routes[0].nexthop = fixed_ip_data['ip_address']
-            else:
-                context.session.delete(metadata_routes[0])
-        else:
-            # add the metadata route
-            route = models_v2.Route(subnet_id=subnet.id,
-                                    destination='169.254.169.254/32',
-                                    nexthop=fixed_ip_data['ip_address'])
-            context.session.add(route)
-
     def setup_rpc(self):
         # RPC support for dhcp
         self.topic = topics.PLUGIN
@@ -1100,16 +1088,6 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
         with context.session.begin(subtransactions=True):
             # First we allocate port in quantum database
             quantum_db = super(NvpPluginV2, self).create_port(context, port)
-            # If we have just created a dhcp port, and metadata request are
-            # forwarded there, we need to verify the appropriate host route is
-            # in place
-            if (cfg.CONF.metadata_dhcp_host_route and
-                (quantum_db.get('device_owner') ==
-                 constants.DEVICE_OWNER_DHCP)):
-                if (quantum_db.get('fixed_ips') and
-                    len(quantum_db.get('fixed_ips'))):
-                    self._ensure_metadata_host_route(
-                        context, quantum_db.get('fixed_ips')[0])
             # Update fields obtained from quantum db (eg: MAC address)
             port["port"].update(quantum_db)
             # port security extension checks
@@ -1172,16 +1150,6 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
             # copy values over
             ret_port.update(port['port'])
 
-            # TODO(salvatore-orlando): We might need transaction management
-            # but the change for metadata support should not be too disruptive
-            fixed_ip_data = port['port'].get('fixed_ips')
-            if (cfg.CONF.metadata_dhcp_host_route and
-                ret_port.get('device_owner') == constants.DEVICE_OWNER_DHCP
-                and fixed_ip_data):
-                    self._ensure_metadata_host_route(context,
-                                                     fixed_ip_data[0],
-                                                     is_delete=True)
-
             # populate port_security setting
             if psec.PORTSECURITY not in port['port']:
                 ret_port[psec.PORTSECURITY] = self._get_port_security_binding(
@@ -1526,6 +1494,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                     order=NVP_EXTGW_NAT_RULES_ORDER,
                     match_criteria={'source_ip_addresses': subnet['cidr']})
 
+        # Ensure the NVP logical router has a connection to a 'metadata access'
+        # network (with a proxy listening on its DHCP port), by creating it
+        # if needed.
+        self._handle_metadata_access_network(context, router_id)
         LOG.debug(_("Add_router_interface completed for subnet:%(subnet_id)s "
                     "and router:%(router_id)s"),
                   {'subnet_id': subnet_id, 'router_id': router_id})
@@ -1585,6 +1557,11 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                         {'q_port_id': port_id,
                          'nvp_port_id': lport['uuid']})
             return
+
+        # Ensure the connection to the 'metadata access network'
+        # is removed  (with the network) if this the last subnet
+        # on the router
+        self._handle_metadata_access_network(context, router_id)
         try:
             if not subnet:
                 subnet = self._get_subnet(context, subnet_id)
index fc98c7e8de9842601d006e57ae731768b0a73a6f..b26ae26ada84e6adfd1252400afb4a62935344dc 100644 (file)
@@ -34,6 +34,9 @@ nvp_opts = [
                       "(default -1 meaning do not time out)")),
     cfg.StrOpt('default_cluster_name',
                help=_("Default cluster name")),
+    cfg.BoolOpt('enable_metadata_access_network', default=True,
+                help=_("Enables dedicated connection to the metadata proxy "
+                       "for metadata server access via Quantum router")),
 ]
 
 cluster_opts = [
diff --git a/quantum/plugins/nicira/nicira_nvp_plugin/common/metadata_access.py b/quantum/plugins/nicira/nicira_nvp_plugin/common/metadata_access.py
new file mode 100644 (file)
index 0000000..8ec2c1e
--- /dev/null
@@ -0,0 +1,150 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Nicira, Inc.
+# All Rights Reserved
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless equired 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, VMware
+
+import netaddr
+
+from quantum.api.v2 import attributes
+from quantum.common import constants
+from quantum.common import exceptions as q_exc
+from quantum.db import l3_db
+from quantum.openstack.common import cfg
+from quantum.openstack.common import log as logging
+from quantum.openstack.common.notifier import api as notifier_api
+from quantum.plugins.nicira.nicira_nvp_plugin.common import (exceptions
+                                                             as nvp_exc)
+from quantum.plugins.nicira.nicira_nvp_plugin import NvpApiClient
+
+
+LOG = logging.getLogger(__name__)
+
+METADATA_DEFAULT_PREFIX = 30
+METADATA_SUBNET_CIDR = '169.254.169.252/%d' % METADATA_DEFAULT_PREFIX
+METADATA_GATEWAY_IP = '169.254.169.253'
+
+
+class NvpMetadataAccess(object):
+
+    def _find_metadata_port(self, context, ports):
+        for port in ports:
+            for fixed_ip in port['fixed_ips']:
+                cidr = netaddr.IPNetwork(
+                    self.get_subnet(context, fixed_ip['subnet_id'])['cidr'])
+                if cidr in netaddr.IPNetwork(METADATA_SUBNET_CIDR):
+                    return port
+
+    def _create_metadata_access_network(self, context, router_id):
+        # This will still ensure atomicity on Quantum DB
+        # context.elevated() creates a deep-copy context
+        ctx_elevated = context.elevated()
+        with ctx_elevated.session.begin(subtransactions=True):
+            # Add network
+            # Network name is likely to be truncated on NVP
+
+            net_data = {'name': ('meta-%s' % router_id)[:40],
+                        'tenant_id': '',  # intentionally not set
+                        'admin_state_up': True,
+                        'port_security_enabled': False,
+                        'shared': False,
+                        'status': constants.NET_STATUS_ACTIVE}
+            meta_net = self.create_network(ctx_elevated,
+                                           {'network': net_data})
+            # Add subnet
+            subnet_data = {'network_id': meta_net['id'],
+                           'tenant_id': '',  # intentionally not set
+                           'name': 'meta-%s' % router_id,
+                           'ip_version': 4,
+                           'shared': False,
+                           'cidr': METADATA_SUBNET_CIDR,
+                           'enable_dhcp': True,
+                           # Ensure default allocation pool is generated
+                           'allocation_pools': attributes.ATTR_NOT_SPECIFIED,
+                           'gateway_ip': METADATA_GATEWAY_IP,
+                           'dns_nameservers': [],
+                           'host_routes': []}
+            meta_sub = self.create_subnet(ctx_elevated,
+                                          {'subnet': subnet_data})
+            self.add_router_interface(ctx_elevated, router_id,
+                                      {'subnet_id': meta_sub['id']})
+            # We need to send a notification to the dhcp agent in order
+            # to start the metadata agent proxy
+            # Note: the publisher id is the same used in the api module
+            notifier_api.notify(context,
+                                notifier_api.publisher_id('network'),
+                                'network.create.end',
+                                notifier_api.CONF.default_notification_level,
+                                {'network': meta_net})
+
+    def _destroy_metadata_access_network(self, context, router_id, ports):
+
+        # context.elevated() creates a deep-copy context
+        ctx_elevated = context.elevated()
+        # This will still ensure atomicity on Quantum DB
+        with ctx_elevated.session.begin(subtransactions=True):
+            if ports:
+                meta_port = self._find_metadata_port(ctx_elevated, ports)
+                if not meta_port:
+                    return
+                meta_net_id = meta_port['network_id']
+                self.remove_router_interface(
+                    ctx_elevated, router_id, {'port_id': meta_port['id']})
+                # Remove network (this will remove the subnet too)
+                self.delete_network(ctx_elevated, meta_net_id)
+                # We need to send a notification to the dhcp agent in order
+                # to stop the metadata agent proxy
+                # Note: the publisher id is the same used in the api module
+                notifier_api.notify(
+                    context,
+                    notifier_api.publisher_id('network'),
+                    'network.delete.end',
+                    notifier_api.CONF.default_notification_level,
+                    {'network_id': meta_net_id})
+
+    def _handle_metadata_access_network(self, context, router_id):
+        if not cfg.CONF.NVP.enable_metadata_access_network:
+            LOG.debug(_("Metadata access network is disabled"))
+            return
+        # As we'll use a different device_owner for metadata interface
+        # this query will return only 'real' router interfaces
+        ctx_elevated = context.elevated()
+        device_filter = {'device_id': [router_id],
+                         'device_owner': [l3_db.DEVICE_OWNER_ROUTER_INTF]}
+        with ctx_elevated.session.begin(subtransactions=True):
+            ports = self.get_ports(ctx_elevated, filters=device_filter)
+            try:
+                if ports:
+                    if not self._find_metadata_port(ctx_elevated, ports):
+                        self._create_metadata_access_network(context,
+                                                             router_id)
+                    elif len(ports) == 1:
+                        # The only port left if the metadata port
+                        self._destroy_metadata_access_network(context,
+                                                              router_id,
+                                                              ports)
+                else:
+                    LOG.debug(_("No router interface found for router '%s'. "
+                                "No metadata access network should be "
+                                "created or destroyed"), router_id)
+            # TODO(salvatore-orlando): A better exception handling in the
+            # NVP plugin would allow us to improve error handling here
+            except (q_exc.QuantumException, nvp_exc.NvpPluginException,
+                    NvpApiClient.NvpApiException):
+                # Any exception here should be regarded as non-fatal
+                LOG.exception(_("An error occurred while operating on the "
+                                "metadata access network for router:'%s'"),
+                              router_id)
index 261120fc42d745c375b76ccc9e0756d0799ddc59..d5c527836ead5777b5a246cd5fb2e412f2060d4e 100644 (file)
@@ -19,6 +19,7 @@ import os
 
 import mock
 from oslo.config import cfg
+import netaddr
 import webob.exc
 
 import quantum.common.test_lib as test_lib
@@ -82,6 +83,7 @@ class NiciraPluginV2TestCase(test_plugin.QuantumDbPluginV2TestCase):
         instance.return_value.get_nvp_version.return_value = "2.999"
         instance.return_value.request.side_effect = _fake_request
         super(NiciraPluginV2TestCase, self).setUp(self._plugin_name)
+        cfg.CONF.set_override('enable_metadata_access_network', False, 'NVP')
 
     def tearDown(self):
         self.fc.reset_all()
@@ -259,10 +261,120 @@ class TestNiciraSecurityGroup(ext_sg.TestSecurityGroups,
 class TestNiciraL3NatTestCase(test_l3_plugin.L3NatDBTestCase,
                               NiciraPluginV2TestCase):
 
-        def test_floatingip_with_assoc_fails(self):
-            self._test_floatingip_with_assoc_fails(
-                'quantum.plugins.nicira.nicira_nvp_plugin.'
-                'QuantumPlugin.NvpPluginV2')
+    def test_floatingip_with_assoc_fails(self):
+        self._test_floatingip_with_assoc_fails(
+            'quantum.plugins.nicira.nicira_nvp_plugin.'
+            'QuantumPlugin.NvpPluginV2')
+
+    def _nvp_metadata_setup(self):
+        cfg.CONF.set_override('enable_metadata_access_network', True, 'NVP')
+
+    def _nvp_metadata_teardown(self):
+        cfg.CONF.set_override('enable_metadata_access_network', False, 'NVP')
+
+    def test_router_add_interface_subnet_with_metadata_access(self):
+        self._nvp_metadata_setup()
+        notifications = ['router.create.start',
+                         'router.create.end',
+                         'network.create.start',
+                         'network.create.end',
+                         'subnet.create.start',
+                         'subnet.create.end',
+                         'router.interface.create',
+                         'network.create.end',
+                         'router.interface.create',
+                         'router.interface.delete',
+                         'router.interface.delete',
+                         'network.delete.end']
+        self.test_router_add_interface_subnet(exp_notifications=notifications)
+        self._nvp_metadata_teardown()
+
+    def test_router_add_interface_port_with_metadata_access(self):
+        self._nvp_metadata_setup()
+        self.test_router_add_interface_port()
+        self._nvp_metadata_teardown()
+
+    def test_router_add_interface_dupsubnet_returns_400_with_metadata(self):
+        self._nvp_metadata_setup()
+        self.test_router_add_interface_dup_subnet1_returns_400()
+        self._nvp_metadata_teardown()
+
+    def test_router_add_interface_overlapped_cidr_returns_400_with(self):
+        self._nvp_metadata_setup()
+        self.test_router_add_interface_overlapped_cidr_returns_400()
+        self._nvp_metadata_teardown()
+
+    def test_router_remove_interface_inuse_returns_409_with_metadata(self):
+        self._nvp_metadata_setup()
+        self.test_router_remove_interface_inuse_returns_409()
+        self._nvp_metadata_teardown()
+
+    def test_router_remove_iface_wrong_sub_returns_409_with_metadata(self):
+        self._nvp_metadata_setup()
+        self.test_router_remove_interface_wrong_subnet_returns_409()
+        self._nvp_metadata_teardown()
+
+    def test_router_delete_with_metadata_access(self):
+        self._nvp_metadata_setup()
+        self.test_router_delete()
+        self._nvp_metadata_teardown()
+
+    def test_router_delete_with_port_existed_returns_409_with_metadata(self):
+        self._nvp_metadata_setup()
+        self.test_router_delete_with_port_existed_returns_409()
+        self._nvp_metadata_teardown()
+
+    def test_metadatata_network_created_with_router_interface_add(self):
+        self._nvp_metadata_setup()
+        with self.router() as r:
+            with self.subnet() as s:
+                body = self._router_interface_action('add',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None)
+                r_ports = self._list('ports')['ports']
+                self.assertEqual(len(r_ports), 2)
+                ips = []
+                for port in r_ports:
+                    ips.extend([netaddr.IPAddress(fixed_ip['ip_address'])
+                                for fixed_ip in port['fixed_ips']])
+                meta_cidr = netaddr.IPNetwork('169.254.0.0/16')
+                self.assertTrue(any([ip in meta_cidr for ip in ips]))
+                # Needed to avoid 409
+                body = self._router_interface_action('remove',
+                                                     r['router']['id'],
+                                                     s['subnet']['id'],
+                                                     None)
+        self._nvp_metadata_teardown()
+
+    def test_metadatata_network_removed_with_router_interface_remove(self):
+        self._nvp_metadata_setup()
+        with self.router() as r:
+            with self.subnet() as s:
+                self._router_interface_action('add', r['router']['id'],
+                                              s['subnet']['id'], None)
+                subnets = self._list('subnets')['subnets']
+                self.assertEqual(len(subnets), 2)
+                meta_cidr = netaddr.IPNetwork('169.254.0.0/16')
+                for subnet in subnets:
+                    cidr = netaddr.IPNetwork(subnet['cidr'])
+                    if meta_cidr == cidr or meta_cidr in cidr.supernet(16):
+                        meta_sub_id = subnet['id']
+                        meta_net_id = subnet['network_id']
+                ports = self._list(
+                    'ports',
+                    query_params='network_id=%s' % meta_net_id)['ports']
+                self.assertEqual(len(ports), 1)
+                meta_port_id = ports[0]['id']
+                self._router_interface_action('remove', r['router']['id'],
+                                              s['subnet']['id'], None)
+                self._show('networks', meta_net_id,
+                           webob.exc.HTTPNotFound.code)
+                self._show('ports', meta_port_id,
+                           webob.exc.HTTPNotFound.code)
+                self._show('subnets', meta_sub_id,
+                           webob.exc.HTTPNotFound.code)
+        self._nvp_metadata_teardown()
 
 
 class NvpQoSTestExtensionManager(object):
index e04d350037c8847e64717184aab8c8006bae113d..0b6e52537851ccae2696cad09d0d36814067c778 100644 (file)
@@ -14,7 +14,6 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
-
 import os
 import socket
 import sys
@@ -27,6 +26,7 @@ import unittest2 as unittest
 from quantum.agent.common import config
 from quantum.agent import dhcp_agent
 from quantum.agent.linux import interface
+from quantum.common import constants
 from quantum.common import exceptions
 from quantum.openstack.common import jsonutils
 
@@ -54,13 +54,20 @@ fake_subnet1 = FakeModel('bbbbbbbb-bbbb-bbbb-bbbbbbbbbbbb',
 
 fake_subnet2 = FakeModel('dddddddd-dddd-dddd-dddddddddddd',
                          network_id='12345678-1234-5678-1234567890ab',
-                         enable_dhcp=False)
+                         cidr='172.9.9.0/24', enable_dhcp=False)
 
 fake_subnet3 = FakeModel('bbbbbbbb-1111-2222-bbbbbbbbbbbb',
                          network_id='12345678-1234-5678-1234567890ab',
                          cidr='192.168.1.1/24', enable_dhcp=True)
 
+fake_meta_subnet = FakeModel('bbbbbbbb-1111-2222-bbbbbbbbbbbb',
+                             network_id='12345678-1234-5678-1234567890ab',
+                             cidr='169.254.169.252/30',
+                             gateway_ip='169.254.169.253', enable_dhcp=True)
+
 fake_fixed_ip = FakeModel('', subnet=fake_subnet1, ip_address='172.9.9.9')
+fake_meta_fixed_ip = FakeModel('', subnet=fake_meta_subnet,
+                               ip_address='169.254.169.254')
 
 fake_port1 = FakeModel('12345678-1234-aaaa-1234567890ab',
                        mac_address='aa:bb:cc:dd:ee:ff',
@@ -71,12 +78,25 @@ fake_port2 = FakeModel('12345678-1234-aaaa-123456789000',
                        mac_address='aa:bb:cc:dd:ee:99',
                        network_id='12345678-1234-5678-1234567890ab')
 
+fake_meta_port = FakeModel('12345678-1234-aaaa-1234567890ab',
+                           mac_address='aa:bb:cc:dd:ee:ff',
+                           network_id='12345678-1234-5678-1234567890ab',
+                           device_owner=constants.DEVICE_OWNER_ROUTER_INTF,
+                           device_id='forzanapoli',
+                           fixed_ips=[fake_meta_fixed_ip])
+
 fake_network = FakeModel('12345678-1234-5678-1234567890ab',
                          tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                          admin_state_up=True,
                          subnets=[fake_subnet1, fake_subnet2],
                          ports=[fake_port1])
 
+fake_meta_network = FakeModel('12345678-1234-5678-1234567890ab',
+                              tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                              admin_state_up=True,
+                              subnets=[fake_meta_subnet],
+                              ports=[fake_meta_port])
+
 fake_down_network = FakeModel('12345678-dddd-dddd-1234567890ab',
                               tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
                               admin_state_up=False,
@@ -417,6 +437,27 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
                 mock.call().disable()
             ])
 
+    def test_enable_isolated_metadata_proxy_with_metadata_network(self):
+        cfg.CONF.set_override('enable_metadata_network', True)
+        class_path = 'quantum.agent.linux.ip_lib.IPWrapper'
+        self.external_process_p.stop()
+        # Ensure the mock is restored if this test fail
+        try:
+            with mock.patch(class_path) as ip_wrapper:
+                self.dhcp.enable_isolated_metadata_proxy(fake_meta_network)
+                ip_wrapper.assert_has_calls([mock.call(
+                    'sudo',
+                    'qdhcp-12345678-1234-5678-1234567890ab'),
+                    mock.call().netns.execute(['quantum-ns-metadata-proxy',
+                                               mock.ANY,
+                                               '--router_id=forzanapoli',
+                                               mock.ANY,
+                                               mock.ANY])
+                ])
+        finally:
+            self.external_process_p.start()
+            cfg.CONF.set_override('enable_metadata_network', False)
+
     def test_network_create_end(self):
         payload = dict(network=dict(id=fake_network.id))
 
@@ -751,44 +792,61 @@ class TestDeviceManager(unittest.TestCase):
         self.device_exists = self.device_exists_p.start()
 
         self.dvr_cls_p = mock.patch('quantum.agent.linux.interface.NullDriver')
+        self.iproute_cls_p = mock.patch('quantum.agent.linux.'
+                                        'ip_lib.IpRouteCommand')
         driver_cls = self.dvr_cls_p.start()
+        iproute_cls = self.iproute_cls_p.start()
         self.mock_driver = mock.MagicMock()
         self.mock_driver.DEV_NAME_LEN = (
             interface.LinuxInterfaceDriver.DEV_NAME_LEN)
+        self.mock_iproute = mock.MagicMock()
         driver_cls.return_value = self.mock_driver
+        iproute_cls.return_value = self.mock_iproute
 
     def tearDown(self):
         self.dvr_cls_p.stop()
         self.device_exists_p.stop()
+        self.iproute_cls_p.stop()
         cfg.CONF.reset()
 
-    def _test_setup_helper(self, device_exists, reuse_existing=False):
+    def _test_setup_helper(self, device_exists, reuse_existing=False,
+                           metadata_access_network=False,
+                           net=None, port=None):
+        net = net or fake_network
+        port = port or fake_port1
         plugin = mock.Mock()
-        plugin.get_dhcp_port.return_value = fake_port1
+        plugin.get_dhcp_port.return_value = port or fake_port1
         self.device_exists.return_value = device_exists
         self.mock_driver.get_device_name.return_value = 'tap12345678-12'
 
         dh = dhcp_agent.DeviceManager(cfg.CONF, plugin)
-        interface_name = dh.setup(fake_network, reuse_existing)
+        interface_name = dh.setup(net, reuse_existing)
 
         self.assertEqual(interface_name, 'tap12345678-12')
 
         plugin.assert_has_calls([
-            mock.call.get_dhcp_port(fake_network.id, mock.ANY)])
+            mock.call.get_dhcp_port(net.id, mock.ANY)])
 
-        namespace = dhcp_agent.NS_PREFIX + fake_network.id
+        namespace = dhcp_agent.NS_PREFIX + net.id
 
+        if metadata_access_network:
+            expected_ips = ['169.254.169.254/30']
+        else:
+            expected_ips = ['172.9.9.9/24', '169.254.169.254/16']
         expected = [mock.call.init_l3('tap12345678-12',
-                                      ['172.9.9.9/24', '169.254.169.254/16'],
+                                      expected_ips,
                                       namespace=namespace)]
 
         if not reuse_existing:
             expected.insert(0,
-                            mock.call.plug(fake_network.id,
-                                           fake_port1.id,
+                            mock.call.plug(net.id,
+                                           port.id,
                                            'tap12345678-12',
                                            'aa:bb:cc:dd:ee:ff',
                                            namespace=namespace))
+        if metadata_access_network:
+            self.mock_iproute.assert_has_calls(
+                [mock.call.add_gateway('169.254.169.253')])
 
         self.mock_driver.assert_has_calls(expected)
 
@@ -802,6 +860,11 @@ class TestDeviceManager(unittest.TestCase):
     def test_setup_device_exists_reuse(self):
         self._test_setup_helper(True, True)
 
+    def test_setup_with_metadata_access_network(self):
+        cfg.CONF.set_override('enable_metadata_network', True)
+        self._test_setup_helper(False, metadata_access_network=True,
+                                net=fake_meta_network, port=fake_meta_port)
+
     def test_destroy(self):
         fake_network = FakeModel('12345678-1234-5678-1234567890ab',
                                  tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa')
index 0fb090cdba54bc44cf4e3eef458826e3eb356916..7e6d2f729026b7e1edc6eaeb50496a919ca550c9 100644 (file)
@@ -588,7 +588,16 @@ class L3NatDBTestCase(L3NatTestCaseBase):
                 fip['floatingip']['router_id'], None,
                 expected_code=exc.HTTPConflict.code)
 
-    def test_router_add_interface_subnet(self):
+    def test_router_add_interface_subnet(self, exp_notifications=None):
+        if not exp_notifications:
+            exp_notifications = ['router.create.start',
+                                 'router.create.end',
+                                 'network.create.start',
+                                 'network.create.end',
+                                 'subnet.create.start',
+                                 'subnet.create.end',
+                                 'router.interface.create',
+                                 'router.interface.delete']
         with self.router() as r:
             with self.subnet() as s:
                 body = self._router_interface_action('add',
@@ -609,17 +618,9 @@ class L3NatDBTestCase(L3NatTestCaseBase):
                 body = self._show('ports', r_port_id,
                                   expected_code=exc.HTTPNotFound.code)
 
-                self.assertEqual(len(test_notifier.NOTIFICATIONS), 8)
                 self.assertEqual(
                     set(n['event_type'] for n in test_notifier.NOTIFICATIONS),
-                    set(['router.create.start',
-                         'router.create.end',
-                         'network.create.start',
-                         'network.create.end',
-                         'subnet.create.start',
-                         'subnet.create.end',
-                         'router.interface.create',
-                         'router.interface.delete']))
+                    set(exp_notifications))
 
     def test_router_add_interface_subnet_with_bad_tenant_returns_404(self):
         with mock.patch('quantum.context.Context.to_dict') as tdict: