# Copyright 2013 VMware, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from neutron_lib.api.definitions import external_net as extnet_apidef from neutron_lib import constants as const from neutron_lib import exceptions as n_exc from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils from neutron.db import db_base_plugin_v2 from neutron.db import l3_db from vmware_nsx._i18n import _ from vmware_nsx.common import exceptions as p_exc from vmware_nsx.dhcp_meta import constants as d_const from vmware_nsx.nsxlib.mh import lsn as lsn_api LOG = logging.getLogger(__name__) dhcp_opts = [ cfg.ListOpt('extra_domain_name_servers', deprecated_group='NVP_DHCP', default=[], help=_('Comma separated list of additional ' 'domain name servers')), cfg.StrOpt('domain_name', deprecated_group='NVP_DHCP', default='openstacklocal', help=_('Domain to use for building the hostnames')), cfg.IntOpt('default_lease_time', default=43200, deprecated_group='NVP_DHCP', help=_("Default DHCP lease time")), ] metadata_opts = [ cfg.StrOpt('metadata_server_address', deprecated_group='NVP_METADATA', default='127.0.0.1', help=_("IP address used by Metadata server.")), cfg.PortOpt('metadata_server_port', deprecated_group='NVP_METADATA', default=8775, help=_("TCP Port used by Metadata server.")), cfg.StrOpt('metadata_shared_secret', deprecated_group='NVP_METADATA', default='', help=_('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.'), secret=True) ] def register_dhcp_opts(config): config.CONF.register_opts(dhcp_opts, group="NSX_DHCP") def register_metadata_opts(config): config.CONF.register_opts(metadata_opts, group="NSX_METADATA") class DhcpAgentNotifyAPI(object): def __init__(self, plugin, lsn_manager): self.plugin = plugin self.lsn_manager = lsn_manager self._handle_subnet_dhcp_access = {'create': self._subnet_create, 'update': self._subnet_update, 'delete': self._subnet_delete} def notify(self, context, data, methodname): [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 NSX 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']: network_id = subnet['network_id'] # Create port for DHCP service dhcp_port = { "name": "", "admin_state_up": True, "device_id": "", "device_owner": const.DEVICE_OWNER_DHCP, "network_id": network_id, "tenant_id": subnet["tenant_id"], "mac_address": const.ATTR_NOT_SPECIFIED, "fixed_ips": [{"subnet_id": subnet['id']}] } try: # This will end up calling handle_port_dhcp_access # down below as well as handle_port_metadata_access self.plugin.create_port(context, {'port': dhcp_port}) except p_exc.PortConfigurationError as e: LOG.error("Error while creating subnet %(cidr)s for " "network %(network)s. Please, contact " "administrator", {"cidr": subnet["cidr"], "network": network_id}) db_base_plugin_v2.NeutronDbPluginV2.delete_port( self.plugin, context, e.port_id) if clean_on_err: self.plugin.delete_subnet(context, subnet['id']) raise n_exc.Conflict() def _subnet_update(self, context, subnet): network_id = subnet['network_id'] try: lsn_id, lsn_port_id = self.lsn_manager.lsn_port_get( context, network_id, subnet['id']) self.lsn_manager.lsn_port_dhcp_configure( context, lsn_id, lsn_port_id, subnet) except p_exc.LsnPortNotFound: # It's possible that the subnet was created with dhcp off; # 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, d_const.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]) ports = self.plugin.get_ports(context, filters=filters) if ports: handle_port_dhcp_access( self.plugin, context, ports[0], 'create_port') else: self._subnet_create(context, subnet, clean_on_err=False) def _subnet_delete(self, context, subnet): # FIXME(armando-migliaccio): it looks like that a subnet filter # is ineffective; so filter by network for now. network_id = subnet['network_id'] filters = dict(network_id=[network_id], device_owner=[const.DEVICE_OWNER_DHCP]) # FIXME(armando-migliaccio): this may be race-y ports = self.plugin.get_ports(context, filters=filters) if ports: # This will end up calling handle_port_dhcp_access # 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 d_const.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_version() # 4.1 is the first and only release where DHCP in NSX # will have this feature, as experimental if ver.major == 4 and ver.minor == 1: cluster_id = cfg.CONF.default_service_cluster_uuid if not lsn_api.service_cluster_exists(cluster, cluster_id): raise p_exc.ServiceClusterUnavailable(cluster_id=cluster_id) else: raise p_exc.InvalidVersion(version=ver) def handle_network_dhcp_access(plugin, context, network, action): LOG.info("Performing DHCP %(action)s for resource: %(resource)s", {"action": action, "resource": network}) if action == 'create_network': network_id = network['id'] if network.get(extnet_apidef.EXTERNAL): LOG.info("Network %s is external: no LSN to create", network_id) return plugin.lsn_manager.lsn_create(context, network_id) elif action == 'delete_network': # NOTE(armando-migliaccio): on delete_network, network # is just the network id network_id = network plugin.lsn_manager.lsn_delete_by_network(context, network_id) LOG.info("Logical Services Node for network " "%s configured successfully", network_id) def handle_port_dhcp_access(plugin, context, port, action): LOG.info("Performing DHCP %(action)s for resource: %(resource)s", {"action": action, "resource": port}) if port["device_owner"] == const.DEVICE_OWNER_DHCP: network_id = port["network_id"] if action == "create_port": # at this point the port must have a subnet and a fixed ip subnet_id = port["fixed_ips"][0]['subnet_id'] subnet = plugin.get_subnet(context, subnet_id) subnet_data = { "mac_address": port["mac_address"], "ip_address": subnet['cidr'], "subnet_id": subnet['id'] } try: plugin.lsn_manager.lsn_port_dhcp_setup( context, network_id, port['id'], subnet_data, subnet) except p_exc.PortConfigurationError: LOG.error("Error while configuring DHCP for " "port %s", port['id']) raise n_exc.NeutronException() elif action == "delete_port": plugin.lsn_manager.lsn_port_dispose(context, network_id, port['mac_address']) elif port["device_owner"] != const.DEVICE_OWNER_DHCP: if port.get("fixed_ips"): # 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 for subnet %s: nothing " "to do", subnet_id) return host_data = { "mac_address": port["mac_address"], "ip_address": port["fixed_ips"][0]['ip_address'] } network_id = port["network_id"] if action == "create_port": handler = plugin.lsn_manager.lsn_port_dhcp_host_add elif action == "delete_port": handler = plugin.lsn_manager.lsn_port_dhcp_host_remove try: handler(context, network_id, subnet_id, host_data) except p_exc.PortConfigurationError: with excutils.save_and_reraise_exception(): if action == 'create_port': db_base_plugin_v2.NeutronDbPluginV2.delete_port( plugin, context, port['id']) LOG.info("DHCP for port %s configured successfully", port['id']) 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[extnet_apidef.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: with excutils.save_and_reraise_exception(): if not is_delete: db_base_plugin_v2.NeutronDbPluginV2.delete_port( plugin, context, port['id']) LOG.info("Metadata for port %s configured successfully", port['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.NsxPluginException: with excutils.save_and_reraise_exception(): if is_enabled: l3_db.L3_NAT_db_mixin.remove_router_interface( plugin, context, router_id, interface) LOG.info("Metadata for router %s handled successfully", router_id)