From 7b23694ebfb0a0f2cb45767922bf17726e90a24f Mon Sep 17 00:00:00 2001 From: Christopher Collins Date: Mon, 25 Nov 2024 18:18:12 -0800 Subject: [PATCH] Distributed DNSMasq --- opflexagent/Dnsmasq.py | 362 ++++++++++++++++++++++++++++++++++ opflexagent/NDeviceManager.py | 352 +++++++++++++++++++++++++++++++++ opflexagent/dns_manager.py | 302 ++++++++++++++++++++++++++++ opflexagent/gbp_agent.py | 4 +- 4 files changed, 1019 insertions(+), 1 deletion(-) create mode 100644 opflexagent/Dnsmasq.py create mode 100644 opflexagent/NDeviceManager.py create mode 100644 opflexagent/dns_manager.py diff --git a/opflexagent/Dnsmasq.py b/opflexagent/Dnsmasq.py new file mode 100644 index 00000000..9bace8ea --- /dev/null +++ b/opflexagent/Dnsmasq.py @@ -0,0 +1,362 @@ +from neutron.agent.linux.dhcp import Dnsmasq as baseDnsmasq +from neutron.agent.linux.dhcp import port_requires_dhcp_configuration +from neutron.agent.linux.dhcp import DictModel +from neutron.common import utils as common_utils +from oslo_utils import excutils + +import os +import io +import netaddr +import json +import shutil +from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext +from neutron_lib import constants +from neutron_lib import exceptions +from neutron_lib.utils import file as file_utils +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import fileutils +from oslo_utils import netutils +from oslo_utils import uuidutils + +from neutron.agent.common import utils as agent_common_utils +from neutron.agent.linux import external_process +from neutron.agent.linux import ip_lib +from neutron.agent.linux import iptables_manager +from neutron.cmd import runtime_checks as checks +from neutron.common import _constants as common_constants +from neutron.common import utils as common_utils +from neutron.ipam import utils as ipam_utils +from opflexagent.NDeviceManager import DeviceManager + +HOST_DHCPV6_TAG = 'tag:dhcpv6,' +AS_MAPPING_DIR = "/var/lib/opflex/files/services" + +class Dnsmasq(baseDnsmasq): + def __init__(self, conf, network, process_monitor, version=None, + plugin=None, segment=None, log=None): + self.LOG = log + self.service_files = set() + super(baseDnsmasq, self).__init__(conf, network, process_monitor, + version, plugin) + self.device_manager = DeviceManager(self.conf, plugin) + + def enable(self): + """Enables DHCP for this network by spawning a local process.""" + self.LOG.warning("Enabling DHCP for a network") + try: + common_utils.wait_until_true(self._enable, timeout=300) + except common_utils.WaitTimeout: + self.LOG.error("Failed to start DHCP process for network %s", + self.network.id) + + def _output_hosts_file(self): + return + + def _get_dns_assignment(self, ip_address, dns_assignment): + return super(Dnsmasq, self)._get_dns_assignment(str(ip_address), dns_assignment) + + def _build_cmdline_callback(self, pid_file): + # We ignore local resolv.conf if dns servers are specified + # or if local resolution is explicitly disabled. + _no_resolv = ( + '--no-resolv' if self.conf.dnsmasq_dns_servers or + not self.conf.dnsmasq_local_resolv else '') + cmd = [ + 'dnsmasq', + '--no-hosts', + _no_resolv, + '--pid-file=%s' % pid_file, + '--dhcp-hostsfile=%s' % self.get_conf_file_name('host'), + '--addn-hosts=%s' % self.get_conf_file_name('addn_hosts'), + '--dhcp-optsfile=%s' % self.get_conf_file_name('opts'), + '--dhcp-leasefile=%s' % self.get_conf_file_name('leases'), + '--dhcp-match=set:ipxe,175', + '--dhcp-userclass=set:ipxe6,iPXE', + #'--local-service', + '--bind-dynamic', + ] + if not self.device_manager.driver.bridged: + cmd += [ + '--bridge-interface=%s,tap*' % self.interface_name, + ] + + possible_leases = 0 + for subnet in self._get_all_subnets(self.network): + mode = None + # if a subnet is specified to have dhcp disabled + if not subnet.enable_dhcp: + continue + if subnet.ip_version == 4: + mode = 'static' + else: + # Note(scollins) If the IPv6 attributes are not set, set it as + # static to preserve previous behavior + addr_mode = getattr(subnet, 'ipv6_address_mode', None) + ra_mode = getattr(subnet, 'ipv6_ra_mode', None) + if (addr_mode in [constants.DHCPV6_STATEFUL, + constants.DHCPV6_STATELESS] or + not addr_mode and not ra_mode): + mode = 'static' + + cidr = netaddr.IPNetwork(subnet.cidr) + + if self.conf.dhcp_lease_duration == -1: + lease = 'infinite' + else: + lease = '%ss' % self.conf.dhcp_lease_duration + + # mode is optional and is not set - skip it + if mode: + if subnet.ip_version == 4: + cmd.append('--dhcp-range=%s%s,%s,%s,%s,%s' % + ('set:', self._SUBNET_TAG_PREFIX % subnet.id, + cidr.network, mode, cidr.netmask, lease)) + else: + if cidr.prefixlen < 64: + self.LOG.debug('Ignoring subnet %(subnet)s, CIDR has ' + 'prefix length < 64: %(cidr)s', + {'subnet': subnet.id, 'cidr': cidr}) + continue + cmd.append('--dhcp-range=%s%s,%s,%s,%d,%s' % + ('set:', self._SUBNET_TAG_PREFIX % subnet.id, + cidr.network, mode, + cidr.prefixlen, lease)) + possible_leases += cidr.size + + mtu = getattr(self.network, 'mtu', 0) + # Do not advertise unknown mtu + if mtu > 0: + cmd.append('--dhcp-option-force=option:mtu,%d' % mtu) + + # Cap the limit because creating lots of subnets can inflate + # this possible lease cap. + cmd.append('--dhcp-lease-max=%d' % + min(possible_leases, self.conf.dnsmasq_lease_max)) + + if self.conf.dhcp_renewal_time > 0: + cmd.append('--dhcp-option-force=option:T1,%ds' % + self.conf.dhcp_renewal_time) + + if self.conf.dhcp_rebinding_time > 0: + cmd.append('--dhcp-option-force=option:T2,%ds' % + self.conf.dhcp_rebinding_time) + + cmd.append('--conf-file=%s' % + (self.conf.dnsmasq_config_file.strip() or '/dev/null')) + for server in self.conf.dnsmasq_dns_servers: + cmd.append('--server=%s' % server) + + if self.conf.dns_domain: + cmd.append('--domain=%s' % self.conf.dns_domain) + + if self.conf.dhcp_broadcast_reply: + cmd.append('--dhcp-broadcast') + + if self.conf.dnsmasq_base_log_dir: + log_dir = os.path.join( + self.conf.dnsmasq_base_log_dir, + self.network.id) + try: + if not os.path.exists(log_dir): + os.makedirs(log_dir) + except OSError: + self.LOG.error('Error while create dnsmasq log dir: %s', log_dir) + else: + log_filename = os.path.join(log_dir, 'dhcp_dns_log') + cmd.append('--log-queries') + cmd.append('--log-dhcp') + cmd.append('--log-facility=%s' % log_filename) + + return cmd + + def _iter_hosts(self, merge_addr6_list=False): + """Iterate over hosts. + + For each host on the network we yield a tuple containing: + ( + port, # a DictModel instance representing the port. + alloc, # a DictModel instance of the allocated ip and subnet. + # if alloc is None, it means there is no need to allocate + # an IPv6 address because of stateless DHCPv6 network. + host_name, # Host name. + name, # Canonical hostname in the format 'hostname[.domain]'. + no_dhcp, # A flag indicating that the address doesn't need a DHCP + # IP address. + no_opts, # A flag indication that options shouldn't be written + tag, # A dhcp-host tag to add to the configuration if supported + ) + """ + v6_nets = dict((subnet.id, subnet) for subnet in + self._get_all_subnets(self.network) + if subnet.ip_version == 6) + + for port in self.network.ports: + if not port_requires_dhcp_configuration(port): + continue + + fixed_ips = self._sort_fixed_ips_for_dnsmasq(port.fixed_ips, + v6_nets) + # TODO(hjensas): Drop this conditional and option once distros + # generally have dnsmasq supporting addr6 list and range. + if self.conf.dnsmasq_enable_addr6_list and merge_addr6_list: + fixed_ips = self._merge_alloc_addr6_list(fixed_ips, v6_nets) + # Confirm whether Neutron server supports dns_name attribute in the + # ports API + dns_assignment = getattr(port, 'dns_assignment', None) + for alloc in fixed_ips: + no_dhcp = False + no_opts = False + tag = '' + if alloc.subnet_id in v6_nets: + addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode + no_dhcp = addr_mode in (constants.IPV6_SLAAC, + constants.DHCPV6_STATELESS) + if self._is_dnsmasq_host_tag_supported(): + tag = HOST_DHCPV6_TAG + # we don't setup anything for SLAAC. It doesn't make sense + # to provide options for a client that won't use DHCP + no_opts = addr_mode == constants.IPV6_SLAAC + + hostname, fqdn = self._get_dns_assignment(alloc.ip_address, + dns_assignment) + + yield (port, alloc, hostname, fqdn, no_dhcp, no_opts, tag) + + def _output_config_files(self): + self._output_hosts_file() + self._output_addn_hosts_file() + self._output_opts_file() + self._output_service_file() + + def _output_hosts_file(self): + """Writes a dnsmasq compatible dhcp hosts file. + + The generated file is sent to the --dhcp-hostsfile option of dnsmasq, + and lists the hosts on the network which should receive a dhcp lease. + Each line in this file is in the form:: + + 'mac_address,FQDN,ip_address' + + IMPORTANT NOTE: a dnsmasq instance does not resolve hosts defined in + this file if it did not give a lease to a host listed in it (e.g.: + multiple dnsmasq instances on the same network if this network is on + multiple network nodes). This file is only defining hosts which + should receive a dhcp lease, the hosts resolution in itself is + defined by the `_output_addn_hosts_file` method. + """ + buf = io.StringIO() + filename = self.get_conf_file_name('host') + + self.LOG.debug('Building host file: %s', filename) + dhcp_enabled_subnet_ids = [s.id for s in + self._get_all_subnets(self.network) + if s.enable_dhcp] + # NOTE(ihrachyshka): the loop should not log anything inside it, to + # avoid potential performance drop when lots of hosts are dumped + for host_tuple in self._iter_hosts(merge_addr6_list=True): + port, alloc, hostname, name, no_dhcp, no_opts, tag = host_tuple + if no_dhcp: + if not no_opts and self._get_port_extra_dhcp_opts(port): + buf.write('%s,%s%s%s\n' % ( + port.mac_address, tag, + 'set:', self._PORT_TAG_PREFIX % port.id)) + continue + + # don't write ip address which belongs to a dhcp disabled subnet. + if alloc.subnet_id not in dhcp_enabled_subnet_ids: + continue + + ip_address = self._format_address_for_dnsmasq(alloc.ip_address) + + if self._get_port_extra_dhcp_opts(port): + client_id = self._get_client_id(port) + if client_id and len(port.extra_dhcp_opts) > 1: + buf.write('%s,%s%s%s,%s,%s,%s%s\n' % + (port.mac_address, tag, self._ID, client_id, + name, ip_address, 'set:', + self._PORT_TAG_PREFIX % port.id)) + elif client_id and len(port.extra_dhcp_opts) == 1: + buf.write('%s,%s%s%s,%s,%s\n' % + (port.mac_address, tag, self._ID, client_id, + name, ip_address)) + else: + buf.write('%s,%s%s,%s,%s%s\n' % + (port.mac_address, tag, name, ip_address, + 'set:', self._PORT_TAG_PREFIX % port.id)) + else: + buf.write('%s,%s%s,%s\n' % + (port.mac_address, tag, name, ip_address)) + + file_utils.replace_file(filename, buf.getvalue()) + self.LOG.debug('Done building host file %s\n%s' % (filename, buf.getvalue())) + return filename + + def _output_service_file(self): + self.LOG.debug("Outputting service file.") + pGatewayIp = "" + + for port in self.network.ports: + if 'network:router_interface' not in port.device_owner: + continue + pGatewayIp = port.fixed_ips[0].ip_address + break + + for port in self.network.ports: + if 'network:dhcp' not in port.device_owner: + continue + + uuid = port.id + interface_namespace = self.network.namespace + interface_name = "tap%s" % port.id[0:11] + proj = "prj_%s" % port.project_id + service_mac = "" + pIp = port.fixed_ips[0].ip_address + asFileName = "%s.as" % uuid + + ip_wrapper_root = ip_lib.IPWrapper(interface_namespace) + if_dev = ip_wrapper_root.device(interface_name) + + if if_dev.exists() is False: + return + + service_mac = str(if_dev.link.address) + + asvc = { + "uuid": uuid, + "interface-name": interface_name, + "service-mac": service_mac, + "domain-policy-space": proj, + "domain-name": "DefaultVRF", + "service-mapping": [ + { + "service-ip": str(pIp), + "gateway-ip": pGatewayIp, + "next-hop-ip": str(pIp), + }, + ], + } + + fileLoc = "%s/%s" % (AS_MAPPING_DIR, asFileName) + + self.LOG.warning("WRITE_SERVICE_FILE:\n%s\n%s" % (asvc, fileLoc)) + self.write_jsonfile(fileLoc, asvc) + self.service_files.add(fileLoc) + + def write_jsonfile(self, name, data): + try: + with open(name, "w") as f: + json.dump(data, f) + except Exception as e: + self.LOG.warning("Exception in writing file: %s", str(e)) + + def _remove_config_files(self): + shutil.rmtree(self.network_conf_dir, ignore_errors=True) + try: + for serviceFileLocation in self.service_files: + if AS_MAPPING_DIR not in serviceFileLocation: + continue + os.remove(serviceFileLocation) + except Exception as e: + self.LOG.warn("Dnsmasq: Exception in deleting file: %s", str(e)) \ No newline at end of file diff --git a/opflexagent/NDeviceManager.py b/opflexagent/NDeviceManager.py new file mode 100644 index 00000000..14eca043 --- /dev/null +++ b/opflexagent/NDeviceManager.py @@ -0,0 +1,352 @@ +import abc +import collections +import copy +import io +import itertools +import os +import re +import shutil +import signal +import time + +import netaddr +from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext +from neutron_lib import constants +from neutron_lib import exceptions +from neutron_lib.utils import file as file_utils +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import fileutils +from oslo_utils import netutils +from oslo_utils import uuidutils + +from neutron.agent.common import utils as agent_common_utils +from neutron.agent.linux import external_process +from neutron.agent.linux import ip_lib +from neutron.agent.linux import iptables_manager +from neutron.cmd import runtime_checks as checks +from neutron.common import _constants as common_constants +from neutron.common import utils as common_utils +from neutron.ipam import utils as ipam_utils +from neutron.agent.linux.dhcp import DictModel +from neutron.agent.linux.dhcp import DeviceManager as base_device_manager + +LOG = logging.getLogger(__name__) +SIGTERM_TIMEOUT = 5 + +DNS_PORT = 53 +WIN2k3_STATIC_DNS = 249 +NS_PREFIX = 'qdhcp-' +DNSMASQ_SERVICE_NAME = 'dnsmasq' +DHCP_RELEASE_TRIES = 3 +DHCP_RELEASE_TRIES_SLEEP = 0.3 +HOST_DHCPV6_TAG = 'tag:dhcpv6,' + +# this variable will be removed when neutron-lib is updated with this value +DHCP_OPT_CLIENT_ID_NUM = 61 + +class DeviceManager(base_device_manager): + + def __init__(self, conf, plugin): + super(DeviceManager, self).__init__(conf, plugin) + + def get_dhcp_agent_device_id(self, network_id, host): + # Split host so as to always use only the hostname and + # not the domain name. This will guarantee consistency + # whether a local hostname or an fqdn is passed in. + #import uuid + #local_hostname = host.split('.')[0] + host_uuid = "827da361-9c56-50f7-913f-5a01f7bfed2c" #uuid.uuid5(uuid.NAMESPACE_DNS, str(local_hostname)) + return 'dhcp%s-%s' % (host_uuid, network_id) + + def cleanup_stale_devices(self, network, dhcp_port): + super(DeviceManager, self).cleanup_stale_devices(network, dhcp_port) + + def plug(self, network, port, interface_name): + """Plug device settings for the network's DHCP on this host.""" + LOG.warning("NET_PLUGGING %s - %s" % (network.namespace, network)) + self.driver.plug(network.id, + port.id, + interface_name, + port.mac_address, + bridge='br-fabric', + namespace=network.namespace, + mtu=network.get('mtu')) + + def _update_dhcp_port(self, network, port): + for index in range(len(network.ports)): + if network.ports[index].id == port.id: + network.ports[index] = port + break + else: + LOG.warning("APPENDING_PORT") + #network.ports.append(port) + + + def setup(self, network, segment=None): + """Create and initialize a device for network's DHCP on this host.""" + try: + port = self.setup_dhcp_port(network, segment) + except Exception: + with excutils.save_and_reraise_exception(): + # clear everything out so we don't leave dangling interfaces + # if setup never succeeds in the future. + self.cleanup_stale_devices(network, dhcp_port=None) + self._update_dhcp_port(network, port) + interface_name = self.get_interface_name(network, port) + LOG.error("Interface name %s" % (interface_name)) + + # Disable acceptance of RAs in the namespace so we don't + # auto-configure an IPv6 address since we explicitly configure + # them on the device. This must be done before any interfaces + # are plugged since it could receive an RA by the time + # plug() returns, so we have to create the namespace first. + # It must also be done in the case there is an existing IPv6 + # address here created via SLAAC, since it will be deleted + # and added back statically in the call to init_l3() below. + if network.namespace: + ip_lib.IPWrapper().ensure_namespace(network.namespace) + ip_lib.set_ip_nonlocal_bind_for_namespace(network.namespace, 1, + root_namespace=True) + if netutils.is_ipv6_enabled(): + self.driver.configure_ipv6_ra(network.namespace, 'default', + constants.ACCEPT_RA_DISABLED) + + if ip_lib.ensure_device_is_ready(interface_name, + namespace=network.namespace): + LOG.debug('Reusing existing device: %s.', interface_name) + # force mtu on the port for in case it was changed for the network + mtu = getattr(network, 'mtu', 0) + if mtu: + self.driver.set_mtu(interface_name, mtu, + namespace=network.namespace) + else: + try: + self.plug(network, port, interface_name) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception('Unable to plug DHCP port for ' + 'network %s. Releasing port.', + network.id) + # We should unplug the interface in bridge side. + self.unplug(interface_name, network) + #self.plugin.release_dhcp_port(network.id, port.device_id) + + self.fill_dhcp_udp_checksums(namespace=network.namespace) + ip_cidrs = [] + for fixed_ip in port.fixed_ips: + subnet = fixed_ip.subnet + net = netaddr.IPNetwork(subnet.cidr) + ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen) + ip_cidrs.append(ip_cidr) + + if self.driver.use_gateway_ips: + # For each DHCP-enabled subnet, add that subnet's gateway + # IP address to the Linux device for the DHCP port. + for subnet in network.subnets: + if not subnet.enable_dhcp: + continue + gateway = subnet.gateway_ip + if gateway: + net = netaddr.IPNetwork(subnet.cidr) + ip_cidrs.append('%s/%s' % (gateway, net.prefixlen)) + + if self.conf.force_metadata or self.conf.enable_isolated_metadata: + ip_cidrs.append(constants.METADATA_CIDR) + if netutils.is_ipv6_enabled(): + ip_cidrs.append(common_constants.METADATA_V6_CIDR) + + self.driver.init_l3(interface_name, ip_cidrs, + namespace=network.namespace) + + self._set_default_route(network, interface_name) + self.cleanup_stale_devices(network, port) + + return interface_name + + def setup_dhcp_port(self, network, segment=None): + """Create/update DHCP port for the host if needed and return port.""" + + # The ID that the DHCP port will have (or already has). + #device_id = self.get_device_id(network, segment) + device_id = None + + # Get the set of DHCP-enabled local subnets on this network. + dhcp_subnets = {subnet.id: subnet for subnet in network.subnets + if subnet.enable_dhcp} + + # There are 3 cases: either the DHCP port already exists (but + # might need to be updated for a changed set of subnets); or + # some other code has already prepared a 'reserved' DHCP port, + # and we just need to adopt that; or we need to create a new + # DHCP port. Try each of those in turn until we have a DHCP + # port. + dhcp_port = self._setup_existing_dhcp_port(network, device_id, dhcp_subnets) + if dhcp_port is None: + raise exceptions.Conflict() + + #self._check_dhcp_port_subnet(dhcp_port, dhcp_subnets, network) + + # Convert subnet_id to subnet dict + fixed_ips = [dict(subnet_id=fixed_ip.subnet_id, + ip_address=fixed_ip.ip_address, + subnet=dhcp_subnets[fixed_ip.subnet_id]) + for fixed_ip in dhcp_port.fixed_ips + # we don't care about any ips on subnets irrelevant + # to us (e.g. auto ipv6 addresses) + if fixed_ip.subnet_id in dhcp_subnets] + + ips = [DictModel(item) if isinstance(item, dict) else item + for item in fixed_ips] + dhcp_port.fixed_ips = ips + + return dhcp_port + + + def _setup_existing_dhcp_port(self, network, device_id, dhcp_subnets): + """Set up the existing DHCP port, if there is one.""" + + # To avoid pylint thinking that port might be undefined after + # the following loop... + port = None + + # Look for an existing DHCP port for this network. + for port in network.ports: + #port_device_id = getattr(port, 'device_id', None) + #LOG.warning("PORT_DEVICE_ID %s vs %s (%s)" % (port_device_id, device_id, port)) + #if port_device_id == device_id: + if "network:dhcp" in str(port.device_owner) and "ACTIVE" in str(port.status): + # If using gateway IPs on this port, we can skip the + # following code, whose purpose is just to review and + # update the Neutron-allocated IP addresses for the + # port. + if self.driver.use_gateway_ips: + return port + # Otherwise break out, as we now have the DHCP port + # whose subnets and addresses we need to review. + break + else: + return None + + return port + # Compare what the subnets should be against what is already + # on the port. + dhcp_enabled_subnet_ids = set(dhcp_subnets) + port_subnet_ids = set(ip.subnet_id for ip in port.fixed_ips) + + # If those differ, we need to call update. + if dhcp_enabled_subnet_ids != port_subnet_ids: + # Collect the subnets and fixed IPs that the port already + # has, for subnets that are still in the DHCP-enabled set. + wanted_fixed_ips = [] + for fixed_ip in port.fixed_ips: + if fixed_ip.subnet_id in dhcp_enabled_subnet_ids: + wanted_fixed_ips.append( + {'subnet_id': fixed_ip.subnet_id, + 'ip_address': fixed_ip.ip_address}) + + # Add subnet IDs for new DHCP-enabled subnets. + wanted_fixed_ips.extend( + dict(subnet_id=s) + for s in dhcp_enabled_subnet_ids - port_subnet_ids) + + # Update the port to have the calculated subnets and fixed + # IPs. The Neutron server will allocate a fresh IP for + # each subnet that doesn't already have one. + port = self.plugin.update_dhcp_port( + port.id, + {'port': {'network_id': network.id, + 'fixed_ips': wanted_fixed_ips}}) + if not port: + raise exceptions.Conflict() + + return port + + def _setup_new_dhcp_port(self, network, device_id, dhcp_subnets): + """Create and set up new DHCP port for the specified network.""" + LOG.warning("Attempting to setup new dhcp port. Port will not be created.") + return None + + def _set_default_route_ip_version(self, network, device_name, ip_version): + device = ip_lib.IPDevice(device_name, namespace=network.namespace) + gateway = device.route.get_gateway(ip_version=ip_version) + if gateway: + gateway = gateway.get('gateway') + + for subnet in network.subnets: + skip_subnet = ( + subnet.ip_version != ip_version or + not subnet.enable_dhcp or + subnet.gateway_ip is None or + subnet.subnetpool_id == constants.IPV6_PD_POOL_ID) + + if skip_subnet: + continue + + if subnet.ip_version == constants.IP_VERSION_6: + # This is duplicating some of the API checks already done, + # but some of the functional tests call directly + prefixlen = netaddr.IPNetwork(subnet.cidr).prefixlen + if prefixlen == 0 or prefixlen > 126: + continue + modes = [constants.IPV6_SLAAC, constants.DHCPV6_STATELESS] + addr_mode = getattr(subnet, 'ipv6_address_mode', None) + ra_mode = getattr(subnet, 'ipv6_ra_mode', None) + if (prefixlen != 64 and + (addr_mode in modes or ra_mode in modes)): + continue + + if gateway != subnet.gateway_ip: + LOG.debug('Setting IPv%(version)s gateway for dhcp netns ' + 'on net %(n)s to %(ip)s', + {'n': network.id, 'ip': subnet.gateway_ip, + 'version': ip_version}) + + # Check for and remove the on-link route for the old + # gateway being replaced, if it is outside the subnet + is_old_gateway_not_in_subnet = (gateway and + not ipam_utils.check_subnet_ip( + subnet.cidr, gateway)) + if is_old_gateway_not_in_subnet: + onlink = device.route.list_onlink_routes(ip_version) + existing_onlink_routes = set(r['cidr'] for r in onlink) + if gateway in existing_onlink_routes: + device.route.delete_route(gateway, scope='link') + + is_new_gateway_not_in_subnet = (subnet.gateway_ip and + not ipam_utils.check_subnet_ip( + subnet.cidr, + subnet.gateway_ip)) + if is_new_gateway_not_in_subnet: + device.route.add_route(subnet.gateway_ip, scope='link') + device.route.add_gateway(subnet.gateway_ip) + + return + + # No subnets on the network have a valid gateway. Clean it up to avoid + # confusion from seeing an invalid gateway here. + if gateway is not None: + LOG.debug('Removing IPv%(version)s gateway for dhcp netns on ' + 'net %(n)s', + {'n': network.id, 'version': ip_version}) + + device.route.delete_gateway(gateway) + + def update(self, network, device_name): + super(DeviceManager, self).update(network, device_name) + + def unplug(self, device_name, network): + """Unplug device settings for the network's DHCP on this host.""" + self.driver.unplug(device_name, namespace=network.namespace, bridge='br-fabric') + + def destroy(self, network, device_name, segment=None): + """Destroy the device used for the network's DHCP on this host.""" + if device_name: + self.unplug(device_name, network) + else: + LOG.debug('No interface exists for network %s', network.id) + + def get_interface_name(self, network, port): + """Return interface(device) name for use by the DHCP process.""" + return self.driver.get_device_name(port) \ No newline at end of file diff --git a/opflexagent/dns_manager.py b/opflexagent/dns_manager.py new file mode 100644 index 00000000..fa1289c1 --- /dev/null +++ b/opflexagent/dns_manager.py @@ -0,0 +1,302 @@ +import collections +import copy +import functools +import subprocess # nosec +from oslo_config import cfg +import eventlet +import json + +from neutron_lib.agent import topics + +from opflexagent.Dnsmasq import Dnsmasq +from neutron.api.rpc.callbacks.consumer import registry +from neutron.api.rpc.callbacks import resources +from oslo_concurrency import lockutils +from oslo_log import log as logging +from oslo_utils import encodeutils + +from neutron.agent.dhcp.agent import DhcpPluginApi +from neutron.agent.linux import external_process +from neutron.agent.linux import dhcp +from neutron.common import config as common_config +from neutron.conf.agent import common as config +from neutron.conf.agent import dhcp as dhcp_config +from neutron.conf.agent.metadata import config as meta_conf +from neutron.conf.plugins.ml2.drivers import ovs_conf +from neutron import service as neutron_service +from neutron._i18n import _ +from neutron_lib.utils import helpers + +INTERFACE_DRIVER_OPTS = [ + cfg.StrOpt('interface_driver', + default='neutron.agent.linux.interface.OVSInterfaceDriver', + help=_("The driver used to manage the virtual interface.")), +] + +class convert_to_dot_notation(dict): + """ + Access dictionary attributes via dot notation + """ + + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + def __deepcopy__(self, memo=None): + return convert_to_dot_notation(copy.deepcopy(dict(self), memo=memo)) + + +class DnsManager(object): + def register_options(self, conf): + config.register_agent_state_opts_helper(conf) + config.register_availability_zone_opts_helper(conf) + dhcp_config.register_agent_dhcp_opts(conf) + meta_conf.register_meta_conf_opts(meta_conf.SHARED_OPTS, conf) + config.register_interface_opts(conf) + config.register_root_helper(conf) + conf.register_opts(INTERFACE_DRIVER_OPTS) + + + def __init__(self, logger): + self.name = "DNSManager" + self.root_helper = cfg.CONF.AGENT.root_helper + self.network_cache = {} + self.subnet_cache = {} + self.port_cache = {} + global LOG + LOG = logger + self.register_options(cfg.CONF) + self.dhcp_version = Dnsmasq.check_version() + registry.register(self.handle_networks, resources.NETWORK) + registry.register(self.handle_subnets, resources.SUBNET) + registry.register(self.handle_ports, resources.PORT) + self.conf = cfg.CONF + self._process_monitor = external_process.ProcessMonitor( + config=self.conf, + resource_type='dhcp') + self.plugin_rpc = DhcpPluginApi(topics.PLUGIN, self.conf.host) + + def has_ip(self, ipaddr, ns, nsport): + outp = self.sh("ip netns exec %s ip addr show dev %s" % + (ns, nsport)) + return 'net %s/' % (ipaddr, ) in outp + + def add_ip(self, ns, ipaddr, nsport): + if self.has_ip(ipaddr, ns, nsport): + return + SVC_IP_CIDR = 16 + self.sh("ip netns exec %s ip addr add %s/%s dev %s" % + (ns, ipaddr, SVC_IP_CIDR, nsport)) + + def add_default_route(self, ns, nexthop): + self.sh("ip netns exec %s ip route add default via %s" % + (ns, nexthop)) + + def sh(self, cmd, as_root=True): + if as_root and self.root_helper: + cmd = "%s %s" % (self.root_helper, cmd) + LOG.debug("%(name)s: Running command: %(cmd)s", + {'name': self.name, 'cmd': cmd}) + ret = '' + try: + sanitized_cmd = encodeutils.to_utf8(cmd) + data = subprocess.check_output( + sanitized_cmd, stderr=subprocess.STDOUT, shell=True) # nosec + ret = helpers.safe_decode_utf8(data) + except Exception as e: + LOG.error("%(name)s: In running command: %(cmd)s: %(exc)s", + {'name': self.name, 'cmd': cmd, 'exc': str(e)}) + LOG.debug("%(name)s: Command output: %(ret)s", + {'name': self.name, 'ret': ret}) + return ret + + + def build_network(self, net_id): + if self.network_cache.get(net_id) is None: + return None + nwork = self.network_cache[net_id] + nwork.subnets = self.subnet_cache.get(net_id) + nwork.ports = self.port_cache.get(net_id) + return nwork + + + def find_index(self, lst, condition): + return [i for i, elem in enumerate(lst) if condition(elem)] + + + def convert_subnet(self, subnet): + d = { + "id": subnet.id, + "name": subnet.name, + "tenant_id": subnet.tenant_id, + "network_id": subnet.network_id, + "ip_version": subnet.ip_version, + "subnetpool_id": subnet.subnetpool_id, + "enable_dhcp": subnet.enable_dhcp, + "ipv6_ra_mode": subnet.ipv6_ra_mode, + "ipv6_address_mode": subnet.ipv6_address_mode, + "gateway_ip": subnet.gateway_ip, + "cidr": subnet.cidr, + "allocation_pools": subnet.allocation_pools, + "host_routes": subnet.host_routes, + "dns_nameservers": subnet.dns_nameservers, + "shared": subnet.shared, + "description": subnet.description, + "service_types": subnet.service_types, + "created_at": subnet.created_at, + "updated_at": subnet.updated_at, + "revision_number": subnet.revision_number, + "project_id": subnet.project_id + } + return convert_to_dot_notation(d) + + def convert_port(self, port): + d = { + "device_id": port.device_id, + "admin_state_up": port.admin_state_up, + "allowed_address_pairs": port.allowed_address_pairs, + "binding_levels": port.binding_levels, + "bindings": port.bindings, + "data_plane_status": port.data_plane_status, + "device_owner": port.device_owner, + "dhcp_options": port.dhcp_options, + "distributed_bindings": port.distributed_bindings, + "dns": port.dns, + "fixed_ips": port.fixed_ips, + "id": port.id, + "mac_address": port.mac_address, + "network_id": port.network_id, + "project_id": port.project_id, + "qos_network_policy_id": port.qos_network_policy_id, + "qos_policy_id": port.qos_policy_id, + "revision_number": port.revision_number, + "security": port.security, + "security_group_ids": port.security_group_ids, + "status": port.status, + "updated_at": port.updated_at + } + return convert_to_dot_notation(d) + + def handle_subnets(self, ctx, resource_type, subnets, event_type): + LOG.debug('DNSMANAGER SUBNET EVENT %s for %s of type %s' % (resource_type, subnets, event_type)) + subnet = subnets[0] + subnet_net_id = subnet.network_id + if self.subnet_cache.get(subnet_net_id) is None: + self.subnet_cache[subnet_net_id] = [] + + if "id" in subnet: + currentIndex = self.find_index(self.subnet_cache[subnet_net_id], lambda e: e.id == subnet.id) + if len(currentIndex) == 0: + self.subnet_cache[subnet_net_id].append(self.convert_subnet(subnet)) + else: + self.subnet_cache[subnet_net_id][currentIndex[0]] = self.convert_subnet(subnet) + self.subnet_cache[subnet_net_id].sort(key=lambda x: x.id) + + if "updated" in str(event_type): + self.call_driver(self.build_network(subnet_net_id), str(event_type)) + + + def handle_ports(self, ctx, resource_type, ports, event_type): + port = ports[0] + if "network_id" not in port: + return + LOG.debug('DNSMANAGER PORT EVENT %s for %s of type %s' % (resource_type, ports, event_type)) + port_net_id = port.network_id + if self.port_cache.get(port_net_id) is None: + self.port_cache[port_net_id] = [] + + if "id" in port: + currentIndex = self.find_index(self.port_cache[port_net_id], lambda e: e.id == port.id) + if len(currentIndex) == 0: + self.port_cache[port_net_id].append(self.convert_port(port)) + else: + self.port_cache[port_net_id][currentIndex[0]] = self.convert_port(port) + self.port_cache[port_net_id].sort(key=lambda x: x.id) + + if "updated" in str(event_type): + self.call_driver(self.build_network(port_net_id), str(event_type)) + + def handle_networks(self, context, resource_type, networks, event_type): + if not networks: + LOG.error('Networks not present') + return + network = dhcp.NetModel(networks[0]) + if 'subnets' not in network: + network.subnets = [] + network.namespace = network._ns_name + LOG.debug('DNSMANAGER NETWORK EVENT %s for %s of type %s' % (resource_type, network, event_type)) + + if self.network_cache.get(network.id) is not None: + self.network_cache[network.id] = network + else: + self.network_cache[network.id] = network + + self.call_driver(self.build_network(network.id), str(event_type)) + + def call_driver(self, network, event_type): + if network is None: + LOG.debug("Empty network.") + return + if network.subnets is None: + LOG.debug("No subnets in network. skipping.") + return + if network.ports is None: + LOG.warning("No ports in network. skipping.") + return + sid_segment = {} + sid_subnets = collections.defaultdict(list) + action = 'enable' if event_type == 'updated' else 'disable' + + DHCP_NS = "qdhcp-%s" % network.id + + network.namespace = DHCP_NS + network._ns_name = DHCP_NS + + if str(event_type) == 'updated': + action = "enable" + # Create namespace, if needed + ns = self.sh("ip netns | grep %s ; true" % DHCP_NS) + if not ns: + self.sh("ip netns add %s" % DHCP_NS) + else: + action = "disable" + + LOG.debug("Dns Manager handling network resource event %s :: %s" % (event_type, network)) + + if 'segments' in network and network.segments: + # In case of multi-segments network, let's group network per + # segments. We can then create DHPC process per segmentation + # id. All subnets on a same network that are sharing the same + # segmentation id will be grouped. + for segment in network.segments: + sid_segment[segment.id] = segment + if 'subnets' in network and network.subnets: + for subnet in network.subnets: + sid_subnets[subnet.get('segment_id')].append(subnet) + if sid_subnets: + for seg_id, subnets in sid_subnets.items(): + segment = sid_segment.get(seg_id) + if segment and segment.segment_index == 0: + if action in ['enable', 'disable']: + self._call_driver( + 'disable', network, segment=None, block=True) + + net_seg = copy.deepcopy(network) + net_seg.subnets = subnets + self._call_driver( + action, net_seg, segment=sid_segment.get(seg_id)) + else: + self._call_driver(action, network, segment=None) + + def _call_driver(self, action, network, segment=None, **action_kwargs): + try: + driver = Dnsmasq(cfg.CONF, + network, + self._process_monitor, + self.dhcp_version, + self.plugin_rpc, + segment, + LOG) + returnValue = getattr(driver, action)(**action_kwargs) + except Exception as e: + LOG.warning("Dns Masq error: %s" % (e)) diff --git a/opflexagent/gbp_agent.py b/opflexagent/gbp_agent.py index 605e4331..b7e950e7 100644 --- a/opflexagent/gbp_agent.py +++ b/opflexagent/gbp_agent.py @@ -37,7 +37,7 @@ from neutron_lib import exceptions from neutron_lib.plugins.ml2 import ovs_constants as constants from opflexagent._i18n import _ -from opflexagent import as_metadata_manager +from opflexagent import as_metadata_manager, dns_manager from opflexagent import constants as ofcst from opflexagent import opflex_notify from opflexagent import rpc @@ -680,6 +680,8 @@ def main(): if not agent: sys.exit(1) + dnsManager = dns_manager.DnsManager(LOG) + LOG.info("Agent initialized successfully, now running... ") agent.daemon_loop()