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)