From 5d7e4676318a5a6e938adf9ccd501e99a28a61c0 Mon Sep 17 00:00:00 2001 From: armando-migliaccio Date: Thu, 26 Sep 2013 09:06:36 -0700 Subject: [PATCH] Add support for NSX/NVP Metadata services 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 | 12 + neutron/plugins/nicira/NeutronPlugin.py | 8 +- neutron/plugins/nicira/dhcp_meta/nvp.py | 237 ++++++++++++- neutron/plugins/nicira/dhcp_meta/rpc.py | 6 +- neutron/plugins/nicira/dhcpmeta_modes.py | 8 +- neutron/plugins/nicira/nsxlib/lsn.py | 51 +++ neutron/tests/unit/nicira/test_dhcpmeta.py | 366 +++++++++++++++++++++ neutron/tests/unit/nicira/test_lsn_lib.py | 85 +++++ 8 files changed, 752 insertions(+), 21 deletions(-) diff --git a/etc/neutron/plugins/nicira/nvp.ini b/etc/neutron/plugins/nicira/nvp.ini index 182a3d8d8a..57cce39b2e 100644 --- a/etc/neutron/plugins/nicira/nvp.ini +++ b/etc/neutron/plugins/nicira/nvp.ini @@ -169,3 +169,15 @@ # 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 = diff --git a/neutron/plugins/nicira/NeutronPlugin.py b/neutron/plugins/nicira/NeutronPlugin.py index 7dd0152007..d46c3cf271 100644 --- a/neutron/plugins/nicira/NeutronPlugin.py +++ b/neutron/plugins/nicira/NeutronPlugin.py @@ -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) diff --git a/neutron/plugins/nicira/dhcp_meta/nvp.py b/neutron/plugins/nicira/dhcp_meta/nvp.py index c4b046e889..15ffe8127e 100644 --- a/neutron/plugins/nicira/dhcp_meta/nvp.py +++ b/neutron/plugins/nicira/dhcp_meta/nvp.py @@ -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) diff --git a/neutron/plugins/nicira/dhcp_meta/rpc.py b/neutron/plugins/nicira/dhcp_meta/rpc.py index 4bf2561e4b..dd13753489 100644 --- a/neutron/plugins/nicira/dhcp_meta/rpc.py +++ b/neutron/plugins/nicira/dhcp_meta/rpc.py @@ -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) diff --git a/neutron/plugins/nicira/dhcpmeta_modes.py b/neutron/plugins/nicira/dhcpmeta_modes.py index 45a5a96d6b..8a857fdaad 100644 --- a/neutron/plugins/nicira/dhcpmeta_modes.py +++ b/neutron/plugins/nicira/dhcpmeta_modes.py @@ -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) diff --git a/neutron/plugins/nicira/nsxlib/lsn.py b/neutron/plugins/nicira/nsxlib/lsn.py index f10c59d4fc..38966b441f 100644 --- a/neutron/plugins/nicira/nsxlib/lsn.py +++ b/neutron/plugins/nicira/nsxlib/lsn.py @@ -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') diff --git a/neutron/tests/unit/nicira/test_dhcpmeta.py b/neutron/tests/unit/nicira/test_dhcpmeta.py index 7c4663757c..a3e116cf6a 100644 --- a/neutron/tests/unit/nicira/test_dhcpmeta.py +++ b/neutron/tests/unit/nicira/test_dhcpmeta.py @@ -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) diff --git a/neutron/tests/unit/nicira/test_lsn_lib.py b/neutron/tests/unit/nicira/test_lsn_lib.py index 88e3fcb954..86daa39aa2 100644 --- a/neutron/tests/unit/nicira/test_lsn_lib.py +++ b/neutron/tests/unit/nicira/test_lsn_lib.py @@ -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)