]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add support for NSX/NVP Metadata services
authorarmando-migliaccio <amigliaccio@nicira.com>
Thu, 26 Sep 2013 16:06:36 +0000 (09:06 -0700)
committerarmando-migliaccio <armamig@gmail.com>
Tue, 17 Dec 2013 16:00:20 +0000 (08:00 -0800)
This is a feature patch (2 of 3) that adds support for
Metadata services provided by the NSX (aka NVP) platform.

It also implements the handling of port events so that
dhcp and metadata configuration in NSX/NVP is updated
if port attributes such as fixed_ips and device_id are
updated.

Partial-implements blueprint nsx-integrated-services

Change-Id: Id2b9125b49c0e15e717605ec6ba3dea5d32ee755

etc/neutron/plugins/nicira/nvp.ini
neutron/plugins/nicira/NeutronPlugin.py
neutron/plugins/nicira/dhcp_meta/nvp.py
neutron/plugins/nicira/dhcp_meta/rpc.py
neutron/plugins/nicira/dhcpmeta_modes.py
neutron/plugins/nicira/nsxlib/lsn.py
neutron/tests/unit/nicira/test_dhcpmeta.py
neutron/tests/unit/nicira/test_lsn_lib.py

index 182a3d8d8a7554a415a2322b4f6554a9809e710c..57cce39b2e82847dec811b3d6c3414ceb4a23061 100644 (file)
 
 # Default DHCP lease time
 # default_lease_time = 43200
+
+[nvp_metadata]
+# IP address used by Metadata server
+# metadata_server_address = 127.0.0.1
+
+# TCP Port used by Metadata server
+# metadata_server_port = 8775
+
+# When proxying metadata requests, Neutron signs the Instance-ID header with a
+# shared secret to prevent spoofing. You may select any string for a secret,
+# but it MUST match with the configuration used by the Metadata server
+# metadata_shared_secret =
index 7dd0152007e164b228fe5a395566e96b97df1bc1..d46c3cf271ac3b588d9756eada1e1aaefbd9920a 100644 (file)
@@ -1552,7 +1552,7 @@ class NvpPluginV2(addr_pair_db.AllowedAddressPairsMixin,
             # router, but if it does, it should not happen within a
             # transaction, and it should be restored on rollback
             self.handle_router_metadata_access(
-                context, router_id, do_create=False)
+                context, router_id, interface=None)
             # Pre-delete checks
             # NOTE(salv-orlando): These checks will be repeated anyway when
             # calling the superclass. This is wasteful, but is the simplest
@@ -1654,7 +1654,8 @@ class NvpPluginV2(addr_pair_db.AllowedAddressPairsMixin,
         # 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_router_metadata_access(context, router_id)
+        self.handle_router_metadata_access(
+            context, router_id, interface=router_iface_info)
         LOG.debug(_("Add_router_interface completed for subnet:%(subnet_id)s "
                     "and router:%(router_id)s"),
                   {'subnet_id': subnet_id, 'router_id': router_id})
@@ -1698,7 +1699,8 @@ class NvpPluginV2(addr_pair_db.AllowedAddressPairsMixin,
         # Ensure the connection to the 'metadata access network'
         # is removed  (with the network) if this the last subnet
         # on the router
-        self.handle_router_metadata_access(context, router_id)
+        self.handle_router_metadata_access(
+            context, router_id, interface=info)
         try:
             if not subnet:
                 subnet = self._get_subnet(context, subnet_id)
index c4b046e889703f105e251cb098de42969cd2a3f6..15ffe8127e266678daffb8d6b1ad3e3952eaf9c8 100644 (file)
@@ -22,6 +22,8 @@ from neutron.api.v2 import attributes as attr
 from neutron.common import constants as const
 from neutron.common import exceptions as n_exc
 from neutron.db import db_base_plugin_v2
+from neutron.db import l3_db
+from neutron.extensions import external_net
 from neutron.openstack.common import log as logging
 from neutron.plugins.nicira.common import exceptions as p_exc
 from neutron.plugins.nicira.nsxlib import lsn as lsn_api
@@ -29,7 +31,17 @@ from neutron.plugins.nicira import nvplib
 
 
 LOG = logging.getLogger(__name__)
-
+# A unique MAC to quickly identify the LSN port used for metadata services
+# when dhcp on the subnet is off. Inspired by leet-speak for 'metadata'.
+METADATA_MAC = "fa:15:73:74:d4:74"
+METADATA_PORT_ID = 'metadata:id'
+METADATA_PORT_NAME = 'metadata:name'
+METADATA_DEVICE_ID = 'metadata:device'
+META_CONF = 'metadata-proxy'
+DHCP_CONF = 'dhcp'
+SPECIAL_OWNERS = (const.DEVICE_OWNER_DHCP,
+                  const.DEVICE_OWNER_ROUTER_GW,
+                  l3_db.DEVICE_OWNER_ROUTER_INTF)
 
 dhcp_opts = [
     cfg.ListOpt('extra_domain_name_servers',
@@ -44,10 +56,27 @@ dhcp_opts = [
 ]
 
 
+metadata_opts = [
+    cfg.StrOpt('metadata_server_address', default='127.0.0.1',
+               help=_("IP address used by Metadata server.")),
+    cfg.IntOpt('metadata_server_port',
+               default=8775,
+               help=_("TCP Port used by Metadata server.")),
+    cfg.StrOpt('metadata_shared_secret',
+               default='',
+               help=_('Shared secret to sign instance-id request'),
+               secret=True)
+]
+
+
 def register_dhcp_opts(config):
     config.CONF.register_opts(dhcp_opts, "NVP_DHCP")
 
 
+def register_metadata_opts(config):
+    config.CONF.register_opts(metadata_opts, "NVP_METADATA")
+
+
 class LsnManager(object):
     """Manage LSN entities associated with networks."""
 
@@ -161,6 +190,24 @@ class LsnManager(object):
             context, network_id, mac_address, raise_on_err=False)
         if lsn_port_id:
             self.lsn_port_delete(context, lsn_id, lsn_port_id)
+            if mac_address == METADATA_MAC:
+                try:
+                    lswitch_port = nvplib.get_port_by_neutron_tag(
+                        self.cluster, network_id, METADATA_PORT_ID)
+                    if lswitch_port:
+                        lswitch_port_id = lswitch_port['uuid']
+                        nvplib.delete_port(
+                            self.cluster, network_id, lswitch_port_id)
+                    else:
+                        LOG.warn(_("Metadata port not found while attempting "
+                                   "to delete it from network %s"), network_id)
+                except (n_exc.PortNotFoundOnNetwork,
+                        nvplib.NvpApiClient.NvpApiException):
+                    LOG.warn(_("Metadata port not found while attempting "
+                               "to delete it from network %s"), network_id)
+        else:
+            LOG.warn(_("Unable to find Logical Services Node "
+                       "Port with MAC %s"), mac_address)
 
     def lsn_port_dhcp_setup(
         self, context, network_id, port_id, port_data, subnet_config=None):
@@ -187,6 +234,36 @@ class LsnManager(object):
         else:
             return (lsn_id, lsn_port_id)
 
+    def lsn_port_metadata_setup(self, context, lsn_id, subnet):
+        """Connect subnet to specified LSN."""
+        data = {
+            "mac_address": METADATA_MAC,
+            "ip_address": subnet['cidr'],
+            "subnet_id": subnet['id']
+        }
+        network_id = subnet['network_id']
+        tenant_id = subnet['tenant_id']
+        lswitch_port_id = None
+        try:
+            lswitch_port_id = nvplib.create_lport(
+                self.cluster, network_id, tenant_id,
+                METADATA_PORT_ID, METADATA_PORT_NAME,
+                METADATA_DEVICE_ID, True)['uuid']
+            lsn_port_id = self.lsn_port_create(self.cluster, lsn_id, data)
+        except (n_exc.NotFound, p_exc.NvpPluginException,
+                nvplib.NvpApiClient.NvpApiException):
+            raise p_exc.PortConfigurationError(
+                net_id=network_id, lsn_id=lsn_id, port_id=lswitch_port_id)
+        else:
+            try:
+                lsn_api.lsn_port_plug_network(
+                    self.cluster, lsn_id, lsn_port_id, lswitch_port_id)
+            except p_exc.LsnConfigurationConflict:
+                self.lsn_port_delete(self.cluster, lsn_id, lsn_port_id)
+                nvplib.delete_port(self.cluster, network_id, lswitch_port_id)
+                raise p_exc.PortConfigurationError(
+                    net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id)
+
     def lsn_port_dhcp_configure(self, context, lsn_id, lsn_port_id, subnet):
         """Enable/disable dhcp services with the given config options."""
         is_enabled = subnet["enable_dhcp"]
@@ -214,6 +291,36 @@ class LsnManager(object):
             LOG.error(err_msg)
             raise p_exc.NvpPluginException(err_msg=err_msg)
 
+    def lsn_metadata_configure(self, context, subnet_id, is_enabled):
+        """Configure metadata service for the specified subnet."""
+        subnet = self.plugin.get_subnet(context, subnet_id)
+        network_id = subnet['network_id']
+        meta_conf = cfg.CONF.NVP_METADATA
+        metadata_options = {
+            'metadata_server_ip': meta_conf.metadata_server_address,
+            'metadata_server_port': meta_conf.metadata_server_port,
+            'metadata_proxy_shared_secret': meta_conf.metadata_shared_secret
+        }
+        try:
+            lsn_id = self.lsn_get(context, network_id)
+            lsn_api.lsn_metadata_configure(
+                self.cluster, lsn_id, is_enabled, metadata_options)
+        except (p_exc.LsnNotFound, nvplib.NvpApiClient.NvpApiException):
+            err_msg = (_('Unable to configure metadata access '
+                         'for subnet %s') % subnet_id)
+            LOG.error(err_msg)
+            raise p_exc.NvpPluginException(err_msg=err_msg)
+        if is_enabled:
+            try:
+                # test that the lsn port exists
+                self.lsn_port_get(context, network_id, subnet_id)
+            except p_exc.LsnPortNotFound:
+                # this might happen if subnet had dhcp off when created
+                # so create one, and wire it
+                self.lsn_port_metadata_setup(context, lsn_id, subnet)
+        else:
+            self.lsn_port_dispose(context, network_id, METADATA_MAC)
+
     def _lsn_port_host_conf(self, context, network_id, subnet_id, data, hdlr):
         lsn_id = None
         lsn_port_id = None
@@ -228,7 +335,7 @@ class LsnManager(object):
                 net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id)
 
     def lsn_port_dhcp_host_add(self, context, network_id, subnet_id, host):
-        """Add dhcp host entry from LSN port configuration."""
+        """Add dhcp host entry to LSN port configuration."""
         self._lsn_port_host_conf(context, network_id, subnet_id, host,
                                  lsn_api.lsn_port_dhcp_host_add)
 
@@ -237,6 +344,34 @@ class LsnManager(object):
         self._lsn_port_host_conf(context, network_id, subnet_id, host,
                                  lsn_api.lsn_port_dhcp_host_remove)
 
+    def lsn_port_meta_host_add(self, context, network_id, subnet_id, host):
+        """Add metadata host entry to LSN port configuration."""
+        self._lsn_port_host_conf(context, network_id, subnet_id, host,
+                                 lsn_api.lsn_port_metadata_host_add)
+
+    def lsn_port_meta_host_remove(self, context, network_id, subnet_id, host):
+        """Remove meta host entry from LSN port configuration."""
+        self._lsn_port_host_conf(context, network_id, subnet_id, host,
+                                 lsn_api.lsn_port_metadata_host_remove)
+
+    def lsn_port_update(
+        self, context, network_id, subnet_id, dhcp=None, meta=None):
+        """Update the specified configuration for the LSN port."""
+        if not dhcp and not meta:
+            return
+        try:
+            lsn_id, lsn_port_id = self.lsn_port_get(
+                context, network_id, subnet_id, raise_on_err=False)
+            if dhcp and lsn_id and lsn_port_id:
+                lsn_api.lsn_port_host_entries_update(
+                    self.cluster, lsn_id, lsn_port_id, DHCP_CONF, dhcp)
+            if meta and lsn_id and lsn_port_id:
+                lsn_api.lsn_port_host_entries_update(
+                    self.cluster, lsn_id, lsn_port_id, META_CONF, meta)
+        except nvplib.NvpApiClient.NvpApiException:
+            raise p_exc.PortConfigurationError(
+                net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id)
+
 
 class DhcpAgentNotifyAPI(object):
 
@@ -251,6 +386,33 @@ class DhcpAgentNotifyAPI(object):
         [resource, action, _e] = methodname.split('.')
         if resource == 'subnet':
             self._handle_subnet_dhcp_access[action](context, data['subnet'])
+        elif resource == 'port' and action == 'update':
+            self._port_update(context, data['port'])
+
+    def _port_update(self, context, port):
+        # With no fixed IP's there's nothing that can be updated
+        if not port["fixed_ips"]:
+            return
+        network_id = port['network_id']
+        subnet_id = port["fixed_ips"][0]['subnet_id']
+        filters = {'network_id': [network_id]}
+        # Because NVP does not support updating a single host entry we
+        # got to build the whole list from scratch and update in bulk
+        ports = self.plugin.get_ports(context, filters)
+        if not ports:
+            return
+        dhcp_conf = [
+            {'mac_address': p['mac_address'],
+             'ip_address': p["fixed_ips"][0]['ip_address']}
+            for p in ports if is_user_port(p)
+        ]
+        meta_conf = [
+            {'instance_id': p['device_id'],
+             'ip_address': p["fixed_ips"][0]['ip_address']}
+            for p in ports if is_user_port(p, check_dev_id=True)
+        ]
+        self.lsn_manager.lsn_port_update(
+            context, network_id, subnet_id, dhcp=dhcp_conf, meta=meta_conf)
 
     def _subnet_create(self, context, subnet, clean_on_err=True):
         if subnet['enable_dhcp']:
@@ -268,7 +430,7 @@ class DhcpAgentNotifyAPI(object):
             }
             try:
                 # This will end up calling handle_port_dhcp_access
-                # down below
+                # down below as well as handle_port_metadata_access
                 self.plugin.create_port(context, {'port': dhcp_port})
             except p_exc.PortConfigurationError as e:
                 err_msg = (_("Error while creating subnet %(cidr)s for "
@@ -292,7 +454,13 @@ class DhcpAgentNotifyAPI(object):
                 context, lsn_id, lsn_port_id, subnet)
         except p_exc.LsnPortNotFound:
             # It's possible that the subnet was created with dhcp off;
-            # check that a dhcp port exists first and provision it
+            # check if the subnet was uplinked onto a router, and if so
+            # remove the patch attachment between the metadata port and
+            # the lsn port, in favor on the one we'll be creating during
+            # _subnet_create
+            self.lsn_manager.lsn_port_dispose(
+                context, network_id, METADATA_MAC)
+            # also, check that a dhcp port exists first and provision it
             # accordingly
             filters = dict(network_id=[network_id],
                            device_owner=[const.DEVICE_OWNER_DHCP])
@@ -313,10 +481,15 @@ class DhcpAgentNotifyAPI(object):
         ports = self.plugin.get_ports(context, filters=filters)
         if ports:
             # This will end up calling handle_port_dhcp_access
-            # down below
+            # down below as well as handle_port_metadata_access
             self.plugin.delete_port(context, ports[0]['id'])
 
 
+def is_user_port(p, check_dev_id=False):
+    usable = p['fixed_ips'] and p['device_owner'] not in SPECIAL_OWNERS
+    return usable if not check_dev_id else usable and p['device_id']
+
+
 def check_services_requirements(cluster):
     ver = cluster.api_client.get_nvp_version()
     # It sounds like 4.1 is the first one where DHCP in NSX/NVP
@@ -374,7 +547,8 @@ def handle_port_dhcp_access(plugin, context, port, action):
             # do something only if there are IP's and dhcp is enabled
             subnet_id = port["fixed_ips"][0]['subnet_id']
             if not plugin.get_subnet(context, subnet_id)['enable_dhcp']:
-                LOG.info(_("DHCP is disabled: nothing to do"))
+                LOG.info(_("DHCP is disabled for subnet %s: nothing "
+                           "to do"), subnet_id)
                 return
             host_data = {
                 "mac_address": port["mac_address"],
@@ -395,11 +569,50 @@ def handle_port_dhcp_access(plugin, context, port, action):
     LOG.info(_("DHCP for port %s configured successfully"), port['id'])
 
 
-def handle_port_metadata_access(context, port, is_delete=False):
-    # TODO(armando-migliaccio)
-    LOG.info('%s port with data %s' % (is_delete, port))
+def handle_port_metadata_access(plugin, context, port, is_delete=False):
+    if is_user_port(port, check_dev_id=True):
+        network_id = port["network_id"]
+        network = plugin.get_network(context, network_id)
+        if network[external_net.EXTERNAL]:
+            LOG.info(_("Network %s is external: nothing to do"), network_id)
+            return
+        subnet_id = port["fixed_ips"][0]['subnet_id']
+        host_data = {
+            "instance_id": port["device_id"],
+            "tenant_id": port["tenant_id"],
+            "ip_address": port["fixed_ips"][0]['ip_address']
+        }
+        LOG.info(_("Configuring metadata entry for port %s"), port)
+        if not is_delete:
+            handler = plugin.lsn_manager.lsn_port_meta_host_add
+        else:
+            handler = plugin.lsn_manager.lsn_port_meta_host_remove
+        try:
+            handler(context, network_id, subnet_id, host_data)
+        except p_exc.PortConfigurationError:
+            if not is_delete:
+                db_base_plugin_v2.NeutronDbPluginV2.delete_port(
+                    plugin, context, port['id'])
+            raise
+        LOG.info(_("Metadata for port %s configured successfully"), port['id'])
 
 
-def handle_router_metadata_access(plugin, context, router_id, do_create=True):
-    # TODO(armando-migliaccio)
-    LOG.info('%s router %s' % (do_create, router_id))
+def handle_router_metadata_access(plugin, context, router_id, interface=None):
+    LOG.info(_("Handle metadata access via router: %(r)s and "
+               "interface %(i)s") % {'r': router_id, 'i': interface})
+    if interface:
+        try:
+            plugin.get_port(context, interface['port_id'])
+            is_enabled = True
+        except n_exc.NotFound:
+            is_enabled = False
+        subnet_id = interface['subnet_id']
+        try:
+            plugin.lsn_manager.lsn_metadata_configure(
+                context, subnet_id, is_enabled)
+        except p_exc.NvpPluginException:
+            if is_enabled:
+                l3_db.L3_NAT_db_mixin.remove_router_interface(
+                    plugin, context, router_id, interface)
+            raise
+    LOG.info(_("Metadata for router %s handled successfully"), router_id)
index 4bf2561e4bbc4d3ac9b50f79aa8450952481fce6..dd13753489d355be45a72b8d445f4356ac0db212 100644 (file)
@@ -80,7 +80,7 @@ def handle_port_dhcp_access(plugin, context, port_data, action):
         _notify_rpc_agent(context, {'subnet': subnet}, 'subnet.update.end')
 
 
-def handle_port_metadata_access(context, port, is_delete=False):
+def handle_port_metadata_access(plugin, context, port, is_delete=False):
     if (cfg.CONF.NVP.metadata_mode == config.MetadataModes.INDIRECT and
         port.get('device_owner') == const.DEVICE_OWNER_DHCP):
         if port.get('fixed_ips', []) or is_delete:
@@ -112,7 +112,7 @@ def handle_port_metadata_access(context, port, is_delete=False):
                 context.session.add(route)
 
 
-def handle_router_metadata_access(plugin, context, router_id, do_create=True):
+def handle_router_metadata_access(plugin, context, router_id, interface=None):
     if cfg.CONF.NVP.metadata_mode != config.MetadataModes.DIRECT:
         LOG.debug(_("Metadata access network is disabled"))
         return
@@ -128,7 +128,7 @@ def handle_router_metadata_access(plugin, context, router_id, do_create=True):
         plugin, ctx_elevated, filters=device_filter)
     try:
         if ports:
-            if (do_create and
+            if (interface and
                 not _find_metadata_port(plugin, ctx_elevated, ports)):
                 _create_metadata_access_network(
                     plugin, ctx_elevated, router_id)
index 45a5a96d6ba869c469875ee21155a58590108ba4..8a857fdaad55bbd88214f8098222dedf2b47ad95 100644 (file)
@@ -77,6 +77,7 @@ class DhcpMetadataAccess(object):
             self.supported_extension_aliases.remove(
                 "dhcp_agent_scheduler")
         nvp_svc.register_dhcp_opts(cfg)
+        nvp_svc.register_metadata_opts(cfg)
         self.lsn_manager = nvp_svc.LsnManager(self)
         self.agent_notifiers[const.AGENT_TYPE_DHCP] = (
             nvp_svc.DhcpAgentNotifyAPI(self, self.lsn_manager))
@@ -106,9 +107,10 @@ class DhcpMetadataAccess(object):
         self.handle_port_dhcp_access_delegate(self, context, port_data, action)
 
     def handle_port_metadata_access(self, context, port, is_delete=False):
-        self.handle_port_metadata_access_delegate(context, port, is_delete)
+        self.handle_port_metadata_access_delegate(self, context,
+                                                  port, is_delete)
 
     def handle_router_metadata_access(self, context,
-                                      router_id, do_create=True):
+                                      router_id, interface=None):
         self.handle_metadata_access_delegate(self, context,
-                                             router_id, do_create)
+                                             router_id, interface)
index f10c59d4fceaea3c6da06f663446ebb93b5e86cf..38966b441fdbf1fbfe9c163fdfc829291cd4f177 100644 (file)
@@ -33,6 +33,7 @@ HTTP_PUT = "PUT"
 SERVICECLUSTER_RESOURCE = "service-cluster"
 LSERVICESNODE_RESOURCE = "lservices-node"
 LSERVICESNODEPORT_RESOURCE = "lport/%s" % LSERVICESNODE_RESOURCE
+SUPPORTED_METADATA_OPTIONS = ['metadata_proxy_shared_secret']
 
 LOG = log.getLogger(__name__)
 
@@ -82,6 +83,18 @@ def lsn_delete(cluster, lsn_id):
                cluster=cluster)
 
 
+def lsn_port_host_entries_update(
+    cluster, lsn_id, lsn_port_id, conf, hosts_data):
+    hosts_obj = {'hosts': hosts_data}
+    do_request(HTTP_PUT,
+               _build_uri_path(LSERVICESNODEPORT_RESOURCE,
+                               parent_resource_id=lsn_id,
+                               resource_id=lsn_port_id,
+                               extra_action=conf),
+               json.dumps(hosts_obj),
+               cluster=cluster)
+
+
 def lsn_port_create(cluster, lsn_id, port_data):
     port_obj = {
         "ip_address": port_data["ip_address"],
@@ -151,6 +164,18 @@ def lsn_port_plug_network(cluster, lsn_id, lsn_port_id, lswitch_port_id):
         raise nvp_exc.LsnConfigurationConflict(lsn_id=lsn_id)
 
 
+def _lsn_configure_action(
+    cluster, lsn_id, action, is_enabled, obj):
+    lsn_obj = {"enabled": is_enabled}
+    lsn_obj.update(obj)
+    do_request(HTTP_PUT,
+               _build_uri_path(LSERVICESNODE_RESOURCE,
+                               resource_id=lsn_id,
+                               extra_action=action),
+               json.dumps(lsn_obj),
+               cluster=cluster)
+
+
 def _lsn_port_configure_action(
     cluster, lsn_id, lsn_port_id, action, is_enabled, obj):
     do_request(HTTP_PUT,
@@ -179,6 +204,22 @@ def lsn_port_dhcp_configure(
         cluster, lsn_id, lsn_port_id, 'dhcp', is_enabled, dhcp_obj)
 
 
+def lsn_metadata_configure(
+        cluster, lsn_id, is_enabled=True, metadata_info=None):
+    opts = [
+        "%s=%s" % (opt, metadata_info[opt])
+        for opt in SUPPORTED_METADATA_OPTIONS
+        if metadata_info.get(opt)
+    ]
+    meta_obj = {
+        'metadata_server_ip': metadata_info['metadata_server_ip'],
+        'metadata_server_port': metadata_info['metadata_server_port'],
+        'misc_options': opts
+    }
+    _lsn_configure_action(
+        cluster, lsn_id, 'metadata-proxy', is_enabled, meta_obj)
+
+
 def _lsn_port_host_action(
     cluster, lsn_id, lsn_port_id, host_obj, extra_action, action):
     do_request(HTTP_POST,
@@ -199,3 +240,13 @@ def lsn_port_dhcp_host_add(cluster, lsn_id, lsn_port_id, host_data):
 def lsn_port_dhcp_host_remove(cluster, lsn_id, lsn_port_id, host_data):
     _lsn_port_host_action(
         cluster, lsn_id, lsn_port_id, host_data, 'dhcp', 'remove_host')
+
+
+def lsn_port_metadata_host_add(cluster, lsn_id, lsn_port_id, host_data):
+    _lsn_port_host_action(
+        cluster, lsn_id, lsn_port_id, host_data, 'metadata-proxy', 'add_host')
+
+
+def lsn_port_metadata_host_remove(cluster, lsn_id, lsn_port_id, host_data):
+    _lsn_port_host_action(cluster, lsn_id, lsn_port_id,
+                          host_data, 'metadata-proxy', 'remove_host')
index 7c4663757c9f186620be2e52b334aae7802dbb0d..a3e116cf6a54dec58f3a8de414a9158d7f41c337 100644 (file)
@@ -36,10 +36,12 @@ class LsnManagerTestCase(base.BaseTestCase):
         self.lsn_id = 'foo_lsn_id'
         self.mac = 'aa:bb:cc:dd:ee:ff'
         self.lsn_port_id = 'foo_lsn_port_id'
+        self.tenant_id = 'foo_tenant_id'
         self.manager = nvp.LsnManager(mock.Mock())
         self.mock_lsn_api_p = mock.patch.object(nvp, 'lsn_api')
         self.mock_lsn_api = self.mock_lsn_api_p.start()
         nvp.register_dhcp_opts(cfg)
+        nvp.register_metadata_opts(cfg)
         self.addCleanup(cfg.CONF.reset)
         self.addCleanup(self.mock_lsn_api_p.stop)
 
@@ -290,6 +292,86 @@ class LsnManagerTestCase(base.BaseTestCase):
         self._test_lsn_port_dhcp_configure_with_subnet(
             expected, routes=['8.8.8.8', '9.9.9.9'])
 
+    def _test_lsn_metadata_configure(self, is_enabled):
+        with mock.patch.object(self.manager, 'lsn_port_dispose') as f:
+            self.manager.plugin.get_subnet.return_value = (
+                {'network_id': self.net_id})
+            self.manager.lsn_metadata_configure(mock.ANY,
+                                                self.sub_id, is_enabled)
+            expected = {
+                'metadata_server_port': 8775,
+                'metadata_server_ip': '127.0.0.1',
+                'metadata_proxy_shared_secret': ''
+            }
+            self.mock_lsn_api.lsn_metadata_configure.assert_called_once_with(
+                mock.ANY, mock.ANY, is_enabled, expected)
+            if is_enabled:
+                self.assertEqual(
+                    1, self.mock_lsn_api.lsn_port_by_subnet_get.call_count)
+            else:
+                self.assertEqual(1, f.call_count)
+
+    def test_lsn_metadata_configure_enabled(self):
+        self._test_lsn_metadata_configure(True)
+
+    def test_lsn_metadata_configure_disabled(self):
+        self._test_lsn_metadata_configure(False)
+
+    def test_lsn_metadata_configure_not_found(self):
+        self.mock_lsn_api.lsn_metadata_configure.side_effect = (
+            p_exc.LsnNotFound(entity='lsn', entity_id=self.lsn_id))
+        self.manager.plugin.get_subnet.return_value = (
+            {'network_id': self.net_id})
+        self.assertRaises(p_exc.NvpPluginException,
+                          self.manager.lsn_metadata_configure,
+                          mock.ANY, self.sub_id, True)
+
+    def test_lsn_port_metadata_setup(self):
+        subnet = {
+            'cidr': '0.0.0.0/0',
+            'id': self.sub_id,
+            'network_id': self.net_id,
+            'tenant_id': self.tenant_id
+        }
+        with mock.patch.object(nvp.nvplib, 'create_lport') as f:
+            f.return_value = {'uuid': self.port_id}
+            self.manager.lsn_port_metadata_setup(mock.ANY, self.lsn_id, subnet)
+            self.assertEqual(1, self.mock_lsn_api.lsn_port_create.call_count)
+            self.mock_lsn_api.lsn_port_plug_network.assert_called_once_with(
+                mock.ANY, self.lsn_id, mock.ANY, self.port_id)
+
+    def test_lsn_port_metadata_setup_raise_not_found(self):
+        subnet = {
+            'cidr': '0.0.0.0/0',
+            'id': self.sub_id,
+            'network_id': self.net_id,
+            'tenant_id': self.tenant_id
+        }
+        with mock.patch.object(nvp.nvplib, 'create_lport') as f:
+            f.side_effect = n_exc.NotFound
+            self.assertRaises(p_exc.PortConfigurationError,
+                              self.manager.lsn_port_metadata_setup,
+                              mock.ANY, self.lsn_id, subnet)
+
+    def test_lsn_port_metadata_setup_raise_conflict(self):
+        subnet = {
+            'cidr': '0.0.0.0/0',
+            'id': self.sub_id,
+            'network_id': self.net_id,
+            'tenant_id': self.tenant_id
+        }
+        with mock.patch.object(nvp.nvplib, 'create_lport') as f:
+            with mock.patch.object(nvp.nvplib, 'delete_port') as g:
+                f.return_value = {'uuid': self.port_id}
+                self.mock_lsn_api.lsn_port_plug_network.side_effect = (
+                    p_exc.LsnConfigurationConflict(lsn_id=self.lsn_id))
+                self.assertRaises(p_exc.PortConfigurationError,
+                                  self.manager.lsn_port_metadata_setup,
+                                  mock.ANY, self.lsn_id, subnet)
+                self.assertEqual(1,
+                                 self.mock_lsn_api.lsn_port_delete.call_count)
+                self.assertEqual(1, g.call_count)
+
     def _test_lsn_port_dispose_with_values(self, lsn_id, lsn_port_id, count):
         with mock.patch.object(self.manager,
                                'lsn_port_get_by_mac',
@@ -302,6 +384,17 @@ class LsnManagerTestCase(base.BaseTestCase):
         self._test_lsn_port_dispose_with_values(
             self.lsn_id, self.lsn_port_id, 1)
 
+    def test_lsn_port_dispose_meta_mac(self):
+        self.mac = nvp.METADATA_MAC
+        with mock.patch.object(nvp.nvplib, 'get_port_by_neutron_tag') as f:
+            with mock.patch.object(nvp.nvplib, 'delete_port') as g:
+                f.return_value = {'uuid': self.port_id}
+                self._test_lsn_port_dispose_with_values(
+                    self.lsn_id, self.lsn_port_id, 1)
+                f.assert_called_once_with(
+                    mock.ANY, self.net_id, nvp.METADATA_PORT_ID)
+                g.assert_called_once_with(mock.ANY, self.net_id, self.port_id)
+
     def test_lsn_port_dispose_lsn_not_found(self):
         self._test_lsn_port_dispose_with_values(None, None, 0)
 
@@ -334,6 +427,33 @@ class LsnManagerTestCase(base.BaseTestCase):
                               self.manager._lsn_port_host_conf, mock.ANY,
                               self.net_id, self.sub_id, mock.ANY, mock.Mock())
 
+    def _test_lsn_port_update(self, dhcp=None, meta=None):
+        self.manager.lsn_port_update(
+            mock.ANY, self.net_id, self.sub_id, dhcp, meta)
+        count = 1 if dhcp else 0
+        count = count + 1 if meta else count
+        self.assertEqual(count, (self.mock_lsn_api.
+                                 lsn_port_host_entries_update.call_count))
+
+    def test_lsn_port_update(self):
+        self._test_lsn_port_update()
+
+    def test_lsn_port_update_dhcp_meta(self):
+        self._test_lsn_port_update(mock.ANY, mock.ANY)
+
+    def test_lsn_port_update_dhcp_and_nometa(self):
+        self._test_lsn_port_update(mock.ANY, None)
+
+    def test_lsn_port_update_nodhcp_and_nmeta(self):
+        self._test_lsn_port_update(None, mock.ANY)
+
+    def test_lsn_port_update_raise_error(self):
+        self.mock_lsn_api.lsn_port_host_entries_update.side_effect = (
+            NvpApiException)
+        self.assertRaises(p_exc.PortConfigurationError,
+                          self.manager.lsn_port_update,
+                          mock.ANY, mock.ANY, mock.ANY, mock.ANY)
+
 
 class DhcpAgentNotifyAPITestCase(base.BaseTestCase):
 
@@ -343,6 +463,107 @@ class DhcpAgentNotifyAPITestCase(base.BaseTestCase):
         self.plugin = self.notifier.plugin
         self.lsn_manager = self.notifier.lsn_manager
 
+    def _test_notify_port_update(
+        self, ports, expected_count, expected_args=None):
+        port = {
+            'id': 'foo_port_id',
+            'network_id': 'foo_network_id',
+            'fixed_ips': [{'subnet_id': 'foo_subnet_id'}]
+        }
+        self.notifier.plugin.get_ports.return_value = ports
+        self.notifier.notify(mock.ANY, {'port': port}, 'port.update.end')
+        self.lsn_manager.lsn_port_update.assert_has_calls(expected_args)
+
+    def test_notify_ports_update_no_ports(self):
+        self._test_notify_port_update(None, 0, [])
+        self._test_notify_port_update([], 0, [])
+
+    def test_notify_ports_update_one_port(self):
+        ports = [{
+            'fixed_ips': [{'subnet_id': 'foo_subnet_id',
+                           'ip_address': '1.2.3.4'}],
+            'device_id': 'foo_device_id',
+            'device_owner': 'foo_device_owner',
+            'mac_address': 'fa:16:3e:da:1d:46'
+        }]
+        call_args = mock.call(
+            mock.ANY, 'foo_network_id', 'foo_subnet_id',
+            dhcp=[{'ip_address': '1.2.3.4',
+                   'mac_address': 'fa:16:3e:da:1d:46'}],
+            meta=[{'instance_id': 'foo_device_id',
+                   'ip_address': '1.2.3.4'}])
+        self._test_notify_port_update(ports, 1, call_args)
+
+    def test_notify_ports_update_ports_with_empty_device_id(self):
+        ports = [{
+            'fixed_ips': [{'subnet_id': 'foo_subnet_id',
+                           'ip_address': '1.2.3.4'}],
+            'device_id': '',
+            'device_owner': 'foo_device_owner',
+            'mac_address': 'fa:16:3e:da:1d:46'
+        }]
+        call_args = mock.call(
+            mock.ANY, 'foo_network_id', 'foo_subnet_id',
+            dhcp=[{'ip_address': '1.2.3.4',
+                   'mac_address': 'fa:16:3e:da:1d:46'}],
+            meta=[]
+        )
+        self._test_notify_port_update(ports, 1, call_args)
+
+    def test_notify_ports_update_ports_with_no_fixed_ips(self):
+        ports = [{
+            'fixed_ips': [],
+            'device_id': 'foo_device_id',
+            'device_owner': 'foo_device_owner',
+            'mac_address': 'fa:16:3e:da:1d:46'
+        }]
+        call_args = mock.call(
+            mock.ANY, 'foo_network_id', 'foo_subnet_id', dhcp=[], meta=[])
+        self._test_notify_port_update(ports, 1, call_args)
+
+    def test_notify_ports_update_ports_with_no_fixed_ips_and_no_device(self):
+        ports = [{
+            'fixed_ips': [],
+            'device_id': '',
+            'device_owner': 'foo_device_owner',
+            'mac_address': 'fa:16:3e:da:1d:46'
+        }]
+        call_args = mock.call(
+            mock.ANY, 'foo_network_id', 'foo_subnet_id', dhcp=[], meta=[])
+        self._test_notify_port_update(ports, 0, call_args)
+
+    def test_notify_ports_update_with_special_ports(self):
+        ports = [{'fixed_ips': [],
+                  'device_id': '',
+                  'device_owner': 'network:dhcp',
+                  'mac_address': 'fa:16:3e:da:1d:46'},
+                 {'fixed_ips': [{'subnet_id': 'foo_subnet_id',
+                                 'ip_address': '1.2.3.4'}],
+                  'device_id': 'foo_device_id',
+                  'device_owner': 'network:router_gateway',
+                  'mac_address': 'fa:16:3e:da:1d:46'}]
+        call_args = mock.call(
+            mock.ANY, 'foo_network_id', 'foo_subnet_id', dhcp=[], meta=[])
+        self._test_notify_port_update(ports, 0, call_args)
+
+    def test_notify_ports_update_many_ports(self):
+        ports = [{'fixed_ips': [],
+                  'device_id': '',
+                  'device_owner': 'foo_device_owner',
+                  'mac_address': 'fa:16:3e:da:1d:46'},
+                 {'fixed_ips': [{'subnet_id': 'foo_subnet_id',
+                                 'ip_address': '1.2.3.4'}],
+                  'device_id': 'foo_device_id',
+                  'device_owner': 'foo_device_owner',
+                  'mac_address': 'fa:16:3e:da:1d:46'}]
+        call_args = mock.call(
+            mock.ANY, 'foo_network_id', 'foo_subnet_id',
+            dhcp=[{'ip_address': '1.2.3.4',
+                   'mac_address': 'fa:16:3e:da:1d:46'}],
+            meta=[{'instance_id': 'foo_device_id',
+                   'ip_address': '1.2.3.4'}])
+        self._test_notify_port_update(ports, 1, call_args)
+
     def _test_notify_subnet_action(self, action):
         with mock.patch.object(self.notifier, '_subnet_%s' % action) as f:
             self.notifier._handle_subnet_dhcp_access[action] = f
@@ -631,3 +852,148 @@ class DhcpTestCase(base.BaseTestCase):
     def test_handle_delete_user_port_no_fixed_ips(self):
         self._test_handle_user_port_no_fixed_ips(
             'delete_port', self.plugin.lsn_manager.lsn_port_dhcp_host_remove)
+
+
+class MetadataTestCase(base.BaseTestCase):
+
+    def setUp(self):
+        super(MetadataTestCase, self).setUp()
+        self.plugin = mock.Mock()
+        self.plugin.lsn_manager = mock.Mock()
+
+    def _test_handle_port_metadata_access_special_owners(
+        self, owner, dev_id='foo_device_id', ips=None):
+        port = {
+            'id': 'foo_port_id',
+            'device_owner': owner,
+            'device_id': dev_id,
+            'fixed_ips': ips or []
+        }
+        nvp.handle_port_metadata_access(self.plugin, mock.ANY, port, mock.ANY)
+        self.assertFalse(
+            self.plugin.lsn_manager.lsn_port_meta_host_add.call_count)
+        self.assertFalse(
+            self.plugin.lsn_manager.lsn_port_meta_host_remove.call_count)
+
+    def test_handle_port_metadata_access_external_network(self):
+        port = {
+            'id': 'foo_port_id',
+            'device_owner': 'foo_device_owner',
+            'device_id': 'foo_device_id',
+            'network_id': 'foo_network_id',
+            'fixed_ips': [{'subnet_id': 'foo_subnet'}]
+        }
+        self.plugin.get_network.return_value = {'router:external': True}
+        nvp.handle_port_metadata_access(self.plugin, mock.ANY, port, mock.ANY)
+        self.assertFalse(
+            self.plugin.lsn_manager.lsn_port_meta_host_add.call_count)
+        self.assertFalse(
+            self.plugin.lsn_manager.lsn_port_meta_host_remove.call_count)
+
+    def test_handle_port_metadata_access_dhcp_port(self):
+        self._test_handle_port_metadata_access_special_owners(
+            'network:dhcp', [{'subnet_id': 'foo_subnet'}])
+
+    def test_handle_port_metadata_access_router_port(self):
+        self._test_handle_port_metadata_access_special_owners(
+            'network:router_interface', [{'subnet_id': 'foo_subnet'}])
+
+    def test_handle_port_metadata_access_no_device_id(self):
+        self._test_handle_port_metadata_access_special_owners(
+            'network:dhcp', '')
+
+    def test_handle_port_metadata_access_no_fixed_ips(self):
+        self._test_handle_port_metadata_access_special_owners(
+            'foo', 'foo', None)
+
+    def _test_handle_port_metadata_access(self, is_delete, raise_exc=False):
+        port = {
+            'id': 'foo_port_id',
+            'device_owner': 'foo_device_id',
+            'network_id': 'foo_network_id',
+            'device_id': 'foo_device_id',
+            'tenant_id': 'foo_tenant_id',
+            'fixed_ips': [
+                {'subnet_id': 'foo_subnet_id', 'ip_address': '1.2.3.4'}
+            ]
+        }
+        meta = {
+            'instance_id': port['device_id'],
+            'tenant_id': port['tenant_id'],
+            'ip_address': port['fixed_ips'][0]['ip_address']
+        }
+        self.plugin.get_network.return_value = {'router:external': False}
+        if is_delete:
+            mock_func = self.plugin.lsn_manager.lsn_port_meta_host_remove
+        else:
+            mock_func = self.plugin.lsn_manager.lsn_port_meta_host_add
+        if raise_exc:
+            mock_func.side_effect = p_exc.PortConfigurationError(
+                lsn_id='foo_lsn_id', net_id='foo_net_id', port_id=None)
+            with mock.patch.object(nvp.db_base_plugin_v2.NeutronDbPluginV2,
+                                   'delete_port') as d:
+                self.assertRaises(p_exc.PortConfigurationError,
+                                  nvp.handle_port_metadata_access,
+                                  self.plugin, mock.ANY, port,
+                                  is_delete=is_delete)
+                if not is_delete:
+                    d.assert_called_once_with(mock.ANY, mock.ANY, port['id'])
+                else:
+                    self.assertFalse(d.call_count)
+        else:
+            nvp.handle_port_metadata_access(
+                self.plugin, mock.ANY, port, is_delete=is_delete)
+        mock_func.assert_called_once_with(mock.ANY, mock.ANY, mock.ANY, meta)
+
+    def test_handle_port_metadata_access_on_delete_true(self):
+        self._test_handle_port_metadata_access(True)
+
+    def test_handle_port_metadata_access_on_delete_false(self):
+        self._test_handle_port_metadata_access(False)
+
+    def test_handle_port_metadata_access_on_delete_true_raise(self):
+        self._test_handle_port_metadata_access(True, raise_exc=True)
+
+    def test_handle_port_metadata_access_on_delete_false_raise(self):
+        self._test_handle_port_metadata_access(False, raise_exc=True)
+
+    def _test_handle_router_metadata_access(
+        self, is_port_found, raise_exc=False):
+        subnet = {
+            'id': 'foo_subnet_id',
+            'network_id': 'foo_network_id'
+        }
+        interface = {
+            'subnet_id': subnet['id'],
+            'port_id': 'foo_port_id'
+        }
+        mock_func = self.plugin.lsn_manager.lsn_metadata_configure
+        if not is_port_found:
+            self.plugin.get_port.side_effect = n_exc.NotFound
+        if raise_exc:
+            with mock.patch.object(nvp.l3_db.L3_NAT_db_mixin,
+                                   'remove_router_interface') as d:
+                mock_func.side_effect = p_exc.NvpPluginException(err_msg='')
+                self.assertRaises(p_exc.NvpPluginException,
+                                  nvp.handle_router_metadata_access,
+                                  self.plugin, mock.ANY, 'foo_router_id',
+                                  interface)
+                d.assert_called_once_with(mock.ANY, mock.ANY, 'foo_router_id',
+                                          interface)
+        else:
+            nvp.handle_router_metadata_access(
+                self.plugin, mock.ANY, 'foo_router_id', interface)
+            mock_func.assert_called_once_with(
+                mock.ANY, subnet['id'], is_port_found)
+
+    def test_handle_router_metadata_access_add_interface(self):
+        self._test_handle_router_metadata_access(True)
+
+    def test_handle_router_metadata_access_delete_interface(self):
+        self._test_handle_router_metadata_access(False)
+
+    def test_handle_router_metadata_access_raise_error_on_add(self):
+        self._test_handle_router_metadata_access(True, raise_exc=True)
+
+    def test_handle_router_metadata_access_raise_error_on_delete(self):
+        self._test_handle_router_metadata_access(True, raise_exc=False)
index 88e3fcb95442efbc5d721c0fbc1a71aed7e13b96..86daa39aa241ef309006a09add032f77e7f33330 100644 (file)
@@ -112,6 +112,31 @@ class LSNTestCase(base.BaseTestCase):
             "DELETE",
             "/ws.v1/lservices-node/%s" % lsn_id, cluster=self.cluster)
 
+    def _test_lsn_port_host_entries_update(self, lsn_type, hosts_data):
+        lsn_id = 'foo_lsn_id'
+        lsn_port_id = 'foo_lsn_port_id'
+        lsnlib.lsn_port_host_entries_update(
+            self.cluster, lsn_id, lsn_port_id, lsn_type, hosts_data)
+        self.mock_request.assert_called_once_with(
+            'PUT',
+            '/ws.v1/lservices-node/%s/lport/%s/%s' % (lsn_id,
+                                                      lsn_port_id,
+                                                      lsn_type),
+            json.dumps({'hosts': hosts_data}),
+            cluster=self.cluster)
+
+    def test_lsn_port_dhcp_entries_update(self):
+        hosts_data = [{"ip_address": "11.22.33.44",
+                       "mac_address": "aa:bb:cc:dd:ee:ff"},
+                      {"ip_address": "44.33.22.11",
+                       "mac_address": "ff:ee:dd:cc:bb:aa"}]
+        self._test_lsn_port_host_entries_update("dhcp", hosts_data)
+
+    def test_lsn_port_metadata_entries_update(self):
+        hosts_data = [{"ip_address": "11.22.33.44",
+                       "device_id": "foo_vm_uuid"}]
+        self._test_lsn_port_host_entries_update("metadata-proxy", hosts_data)
+
     def test_lsn_port_create(self):
         port_data = {
             "ip_address": "1.2.3.0/24",
@@ -230,6 +255,50 @@ class LSNTestCase(base.BaseTestCase):
         self._test_lsn_port_dhcp_configure(
             lsn_id, lsn_port_id, is_enabled, opts)
 
+    def _test_lsn_metadata_configure(
+        self, lsn_id, is_enabled, opts, expected_opts):
+        lsnlib.lsn_metadata_configure(
+            self.cluster, lsn_id, is_enabled, opts)
+        lsn_obj = {"enabled": is_enabled}
+        lsn_obj.update(expected_opts)
+        self.mock_request.assert_has_calls([
+            mock.call("PUT",
+                      "/ws.v1/lservices-node/%s/metadata-proxy" % lsn_id,
+                      json.dumps(lsn_obj),
+                      cluster=self.cluster),
+        ])
+
+    def test_lsn_port_metadata_configure_empty_secret(self):
+        lsn_id = "foo_lsn_id"
+        is_enabled = True
+        opts = {
+            "metadata_server_ip": "1.2.3.4",
+            "metadata_server_port": "8775"
+        }
+        expected_opts = {
+            "metadata_server_ip": "1.2.3.4",
+            "metadata_server_port": "8775",
+            "misc_options": []
+        }
+        self._test_lsn_metadata_configure(
+            lsn_id, is_enabled, opts, expected_opts)
+
+    def test_lsn_metadata_configure_with_secret(self):
+        lsn_id = "foo_lsn_id"
+        is_enabled = True
+        opts = {
+            "metadata_server_ip": "1.2.3.4",
+            "metadata_server_port": "8775",
+            "metadata_proxy_shared_secret": "foo_secret"
+        }
+        expected_opts = {
+            "metadata_server_ip": "1.2.3.4",
+            "metadata_server_port": "8775",
+            "misc_options": ["metadata_proxy_shared_secret=foo_secret"]
+        }
+        self._test_lsn_metadata_configure(
+            lsn_id, is_enabled, opts, expected_opts)
+
     def _test_lsn_port_host_action(
             self, lsn_port_action_func, extra_action, action, host):
         lsn_id = "foo_lsn_id"
@@ -256,3 +325,19 @@ class LSNTestCase(base.BaseTestCase):
         }
         self._test_lsn_port_host_action(
             lsnlib.lsn_port_dhcp_host_remove, "dhcp", "remove_host", host)
+
+    def test_lsn_port_metadata_host_add(self):
+        host = {
+            "ip_address": "1.2.3.4",
+            "instance_id": "foo_instance_id"
+        }
+        self._test_lsn_port_host_action(lsnlib.lsn_port_metadata_host_add,
+                                        "metadata-proxy", "add_host", host)
+
+    def test_lsn_port_metadata_host_remove(self):
+        host = {
+            "ip_address": "1.2.3.4",
+            "instance_id": "foo_instance_id"
+        }
+        self._test_lsn_port_host_action(lsnlib.lsn_port_metadata_host_remove,
+                                        "metadata-proxy", "remove_host", host)