components/openstack/neutron/files/agent/solaris/dhcp.py
changeset 1944 56ac2df1785b
parent 1872 0b81e3d9f3ae
child 1987 6fa18b7a0af6
equal deleted inserted replaced
1943:1a27f000029f 1944:56ac2df1785b
       
     1 # vim: tabstop=4 shiftwidth=4 softtabstop=4
       
     2 
       
     3 # Copyright 2012 OpenStack Foundation
       
     4 # All Rights Reserved.
       
     5 #
     1 # Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
     6 # Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
     2 #
     7 #
     3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
     8 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
     4 #    not use this file except in compliance with the License. You may obtain
     9 #    not use this file except in compliance with the License. You may obtain
     5 #    a copy of the License at
    10 #    a copy of the License at
    16 
    21 
    17 import abc
    22 import abc
    18 import os
    23 import os
    19 import re
    24 import re
    20 import shutil
    25 import shutil
       
    26 import socket
    21 import StringIO
    27 import StringIO
       
    28 import sys
       
    29 import uuid
    22 
    30 
    23 import netaddr
    31 import netaddr
    24 
       
    25 from oslo.config import cfg
    32 from oslo.config import cfg
    26 from quantum.agent.linux import utils
    33 
    27 from quantum.openstack.common import log as logging
    34 from neutron.agent.linux import utils
    28 from quantum.openstack.common import uuidutils
    35 from neutron.agent.solaris import net_lib
       
    36 from neutron.common import exceptions
       
    37 from neutron.openstack.common import importutils
       
    38 from neutron.openstack.common import jsonutils
       
    39 from neutron.openstack.common import log as logging
       
    40 from neutron.openstack.common import uuidutils
    29 
    41 
    30 LOG = logging.getLogger(__name__)
    42 LOG = logging.getLogger(__name__)
    31 
    43 
    32 OPTS = [
    44 OPTS = [
    33     cfg.StrOpt('dhcp_confs',
    45     cfg.StrOpt('dhcp_confs',
    34                default='$state_path/dhcp',
    46                default='$state_path/dhcp',
    35                help=_('Location to store DHCP server config files')),
    47                help=_('Location to store DHCP server config files')),
    36     cfg.IntOpt('dhcp_lease_time',
       
    37                default=120,
       
    38                help=_('Lifetime of a DHCP lease in seconds')),
       
    39     cfg.StrOpt('dhcp_domain',
    48     cfg.StrOpt('dhcp_domain',
    40                default='openstacklocal',
    49                default='openstacklocal',
    41                help=_('Domain to use for building the hostnames')),
    50                help=_('Domain to use for building the hostnames')),
    42     cfg.StrOpt('dnsmasq_config_file',
    51     cfg.StrOpt('dnsmasq_config_file',
    43                default='',
    52                default='',
    44                help=_('Override the default dnsmasq settings with this file')),
    53                help=_('Override the default dnsmasq settings with this file')),
    45     cfg.StrOpt('dnsmasq_dns_server',
    54     cfg.StrOpt('dnsmasq_dns_server',
    46                help=_('Use another DNS server before any in '
    55                help=_('Use another DNS server before any in '
    47                       '/etc/resolv.conf.')),
    56                       '/etc/resolv.conf.')),
       
    57     cfg.IntOpt(
       
    58         'dnsmasq_lease_max',
       
    59         default=(2 ** 24),
       
    60         help=_('Limit number of leases to prevent a denial-of-service.')),
       
    61     cfg.StrOpt('interface_driver',
       
    62                help=_("The driver used to manage the virtual interface.")),
    48 ]
    63 ]
    49 
    64 
    50 IPV4 = 4
    65 IPV4 = 4
    51 IPV6 = 6
    66 IPV6 = 6
    52 UDP = 'udp'
    67 UDP = 'udp'
    53 TCP = 'tcp'
    68 TCP = 'tcp'
    54 DNS_PORT = 53
    69 DNS_PORT = 53
    55 DHCPV4_PORT = 67
    70 DHCPV4_PORT = 67
    56 DHCPV6_PORT = 467
    71 DHCPV6_PORT = 547
       
    72 METADATA_DEFAULT_PREFIX = 16
       
    73 METADATA_DEFAULT_IP = '169.254.169.254'
       
    74 METADATA_DEFAULT_CIDR = '%s/%d' % (METADATA_DEFAULT_IP,
       
    75                                    METADATA_DEFAULT_PREFIX)
       
    76 METADATA_PORT = 80
       
    77 WIN2k3_STATIC_DNS = 249
       
    78 
       
    79 
       
    80 class DictModel(object):
       
    81     """Convert dict into an object that provides attribute access to values."""
       
    82     def __init__(self, d):
       
    83         for key, value in d.iteritems():
       
    84             if isinstance(value, list):
       
    85                 value = [DictModel(item) if isinstance(item, dict) else item
       
    86                          for item in value]
       
    87             elif isinstance(value, dict):
       
    88                 value = DictModel(value)
       
    89 
       
    90             setattr(self, key, value)
       
    91 
       
    92 
       
    93 class NetModel(DictModel):
       
    94 
       
    95     def __init__(self, use_namespaces, d):
       
    96         super(NetModel, self).__init__(d)
       
    97 
       
    98         self._ns_name = None
       
    99 
       
   100     @property
       
   101     def namespace(self):
       
   102         return self._ns_name
    57 
   103 
    58 
   104 
    59 class DhcpBase(object):
   105 class DhcpBase(object):
    60     __metaclass__ = abc.ABCMeta
   106     __metaclass__ = abc.ABCMeta
    61 
   107 
    62     def __init__(self, conf, network, root_helper='sudo',
   108     def __init__(self, conf, network, root_helper='sudo',
    63                  device_delegate=None, namespace=None, version=None):
   109                  version=None, plugin=None):
    64         self.conf = conf
   110         self.conf = conf
    65         self.network = network
   111         self.network = network
    66         self.root_helper = root_helper
   112         self.root_helper = None
    67         self.device_delegate = device_delegate
   113         self.device_manager = DeviceManager(self.conf,
    68         self.namespace = namespace
   114                                             self.root_helper, plugin)
    69         self.version = version
   115         self.version = version
    70 
   116 
    71     @abc.abstractmethod
   117     @abc.abstractmethod
    72     def enable(self):
   118     def enable(self):
    73         """Enables DHCP for this network."""
   119         """Enables DHCP for this network."""
    78 
   124 
    79     def restart(self):
   125     def restart(self):
    80         """Restart the dhcp service for the network."""
   126         """Restart the dhcp service for the network."""
    81         self.disable(retain_port=True)
   127         self.disable(retain_port=True)
    82         self.enable()
   128         self.enable()
       
   129         self.device_manager.update(self.network)
    83 
   130 
    84     @abc.abstractproperty
   131     @abc.abstractproperty
    85     def active(self):
   132     def active(self):
    86         """Boolean representing the running state of the DHCP server."""
   133         """Boolean representing the running state of the DHCP server."""
    87 
   134 
    88     @abc.abstractmethod
   135     @abc.abstractmethod
       
   136     def release_lease(self, mac_address, removed_ips):
       
   137         """Release a DHCP lease."""
       
   138 
       
   139     @abc.abstractmethod
    89     def reload_allocations(self):
   140     def reload_allocations(self):
    90         """Force the DHCP server to reload the assignment database."""
   141         """Force the DHCP server to reload the assignment database."""
    91 
   142 
    92     @classmethod
   143     @classmethod
    93     def existing_dhcp_networks(cls, conf, root_helper):
   144     def existing_dhcp_networks(cls, conf, root_helper):
    94         """Return a list of existing networks ids (ones we have configs for)"""
   145         """Return a list of existing networks ids that we have configs for."""
    95 
   146 
    96         raise NotImplementedError
   147         raise NotImplementedError
    97 
   148 
    98     @classmethod
   149     @classmethod
    99     def check_version(cls):
   150     def check_version(cls):
   105 class DhcpLocalProcess(DhcpBase):
   156 class DhcpLocalProcess(DhcpBase):
   106     PORTS = []
   157     PORTS = []
   107 
   158 
   108     def _enable_dhcp(self):
   159     def _enable_dhcp(self):
   109         """check if there is a subnet within the network with dhcp enabled."""
   160         """check if there is a subnet within the network with dhcp enabled."""
   110         return any(s for s in self.network.subnets if s.enable_dhcp)
   161         for subnet in self.network.subnets:
       
   162             if subnet.enable_dhcp:
       
   163                 return True
       
   164         return False
   111 
   165 
   112     def enable(self):
   166     def enable(self):
   113         """Enables DHCP for this network by spawning a local process."""
   167         """Enables DHCP for this network by spawning a local process."""
   114         interface_name = self.device_delegate.setup(self.network,
   168         interface_name = self.device_manager.setup(self.network,
   115                                                     reuse_existing=True)
   169                                                    reuse_existing=True)
   116         if self.active:
   170         if self.active:
   117             self.restart()
   171             self.restart()
   118         elif self._enable_dhcp():
   172         elif self._enable_dhcp():
   119             self.interface_name = interface_name
   173             self.interface_name = interface_name
   120             self.spawn_process()
   174             self.spawn_process()
   123         """Disable DHCP for this network by killing the local process."""
   177         """Disable DHCP for this network by killing the local process."""
   124         pid = self.pid
   178         pid = self.pid
   125 
   179 
   126         if self.active:
   180         if self.active:
   127             cmd = ['kill', '-9', pid]
   181             cmd = ['kill', '-9', pid]
   128             utils.execute(cmd)
   182             utils.execute(cmd, self.root_helper)
   129 
       
   130             if not retain_port:
   183             if not retain_port:
   131                 self.device_delegate.destroy(self.network, self.interface_name)
   184                 self.device_manager.destroy(self.network, self.interface_name)
   132 
   185 
   133         elif pid:
   186         elif pid:
   134             LOG.debug(_('DHCP for %(net_id)s pid %(pid)d is stale, ignoring '
   187             LOG.debug(_('DHCP for %(net_id)s pid %(pid)d is stale, ignoring '
   135                         'command'), {'net_id': self.network.id, 'pid': pid})
   188                         'command'), {'net_id': self.network.id, 'pid': pid})
   136         else:
   189         else:
   147         """Returns the file name for a given kind of config file."""
   200         """Returns the file name for a given kind of config file."""
   148         confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
   201         confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
   149         conf_dir = os.path.join(confs_dir, self.network.id)
   202         conf_dir = os.path.join(confs_dir, self.network.id)
   150         if ensure_conf_dir:
   203         if ensure_conf_dir:
   151             if not os.path.isdir(conf_dir):
   204             if not os.path.isdir(conf_dir):
   152                 os.makedirs(conf_dir, 0755)
   205                 os.makedirs(conf_dir, 0o755)
   153 
   206 
   154         return os.path.join(conf_dir, kind)
   207         return os.path.join(conf_dir, kind)
   155 
   208 
   156     def _get_value_from_conf_file(self, kind, converter=None):
   209     def _get_value_from_conf_file(self, kind, converter=None):
   157         """A helper function to read a value from one of the state files."""
   210         """A helper function to read a value from one of the state files."""
   160 
   213 
   161         try:
   214         try:
   162             with open(file_name, 'r') as f:
   215             with open(file_name, 'r') as f:
   163                 try:
   216                 try:
   164                     return converter and converter(f.read()) or f.read()
   217                     return converter and converter(f.read()) or f.read()
   165                 except ValueError, e:
   218                 except ValueError:
   166                     msg = _('Unable to convert value in %s')
   219                     msg = _('Unable to convert value in %s')
   167         except IOError, e:
   220         except IOError:
   168             msg = _('Unable to access %s')
   221             msg = _('Unable to access %s')
   169 
   222 
   170         LOG.debug(msg % file_name)
   223         LOG.debug(msg % file_name)
   171         return None
   224         return None
   172 
   225 
   179     def active(self):
   232     def active(self):
   180         pid = self.pid
   233         pid = self.pid
   181         if pid is None:
   234         if pid is None:
   182             return False
   235             return False
   183 
   236 
   184         cmd = ['pargs', pid]
   237         cmd = ['/usr/bin/pargs', pid]
   185         try:
   238         try:
   186             return self.network.id in utils.execute(cmd)
   239             return self.network.id in utils.execute(cmd)
   187         except RuntimeError:
   240         except RuntimeError:
   188             return False
   241             return False
   189 
   242 
   202         pass
   255         pass
   203 
   256 
   204 
   257 
   205 class Dnsmasq(DhcpLocalProcess):
   258 class Dnsmasq(DhcpLocalProcess):
   206     # The ports that need to be opened when security policies are active
   259     # The ports that need to be opened when security policies are active
   207     # on the Quantum port used for DHCP.  These are provided as a convenience
   260     # on the Neutron port used for DHCP.  These are provided as a convenience
   208     # for users of this class.
   261     # for users of this class.
   209     PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)],
   262     PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)],
   210              IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)]}
   263              IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)],
       
   264              }
   211 
   265 
   212     _TAG_PREFIX = 'tag%d'
   266     _TAG_PREFIX = 'tag%d'
   213 
   267 
   214     QUANTUM_NETWORK_ID_KEY = 'QUANTUM_NETWORK_ID'
   268     NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID'
   215     QUANTUM_RELAY_SOCKET_PATH_KEY = 'QUANTUM_RELAY_SOCKET_PATH'
   269     NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH'
       
   270     MINIMUM_VERSION = 2.59
   216 
   271 
   217     @classmethod
   272     @classmethod
   218     def check_version(cls):
   273     def check_version(cls):
   219         # For Solaris, we rely on the packaging system to ensure a
   274         ver = 0
   220         # matching/supported version of dnsmasq
   275         try:
   221         pass
   276             cmd = ['/usr/lib/inet/dnsmasq', '--version']
       
   277             out = utils.execute(cmd)
       
   278             ver = re.findall("\d+.\d+", out)[0]
       
   279             is_valid_version = float(ver) >= cls.MINIMUM_VERSION
       
   280             # For Solaris, we rely on the packaging system to ensure a
       
   281             # matching/supported version of dnsmasq.
       
   282             if not is_valid_version:
       
   283                 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
       
   284                               'DHCP AGENT MAY NOT RUN CORRECTLY! '
       
   285                               'Please ensure that its version is %s '
       
   286                               'or above!'), cls.MINIMUM_VERSION)
       
   287         except (OSError, RuntimeError, IndexError, ValueError):
       
   288             LOG.warning(_('Unable to determine dnsmasq version. '
       
   289                           'Please ensure that its version is %s '
       
   290                           'or above!'), cls.MINIMUM_VERSION)
       
   291         return float(ver)
   222 
   292 
   223     @classmethod
   293     @classmethod
   224     def existing_dhcp_networks(cls, conf, root_helper):
   294     def existing_dhcp_networks(cls, conf, root_helper):
   225         """Return a list of existing networks ids (ones we have configs for)"""
   295         """Return a list of existing networks ids that we have configs for."""
   226 
   296 
   227         confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs))
   297         confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs))
   228 
       
   229         class FakeNetwork:
       
   230             def __init__(self, net_id):
       
   231                 self.id = net_id
       
   232 
   298 
   233         return [
   299         return [
   234             c for c in os.listdir(confs_dir)
   300             c for c in os.listdir(confs_dir)
   235             if (uuidutils.is_uuid_like(c) and
   301             if uuidutils.is_uuid_like(c)
   236                 cls(conf, FakeNetwork(c), root_helper).active)
       
   237         ]
   302         ]
   238 
   303 
   239     def spawn_process(self):
   304     def spawn_process(self):
   240         """Spawns a Dnsmasq process for the network."""
   305         """Spawns a Dnsmasq process for the network."""
   241         env = {
   306         env = {
   242             self.QUANTUM_NETWORK_ID_KEY: self.network.id
   307             self.NEUTRON_NETWORK_ID_KEY: self.network.id,
   243         }
   308         }
   244 
   309 
   245         cmd = [
   310         cmd = [
   246             '/usr/lib/inet/dnsmasq',
   311             '/usr/lib/inet/dnsmasq',
   247             '--no-hosts',
   312             '--no-hosts',
   250             '--bind-interfaces',
   315             '--bind-interfaces',
   251             '--interface=%s' % self.interface_name,
   316             '--interface=%s' % self.interface_name,
   252             '--except-interface=lo0',
   317             '--except-interface=lo0',
   253             '--pid-file=%s' % self.get_conf_file_name(
   318             '--pid-file=%s' % self.get_conf_file_name(
   254                 'pid', ensure_conf_dir=True),
   319                 'pid', ensure_conf_dir=True),
   255             # TODO(gmoodalb): calculate value from cidr (defaults to 150)
       
   256             # '--dhcp-lease-max=%s' % ?,
       
   257             '--dhcp-hostsfile=%s' % self._output_hosts_file(),
   320             '--dhcp-hostsfile=%s' % self._output_hosts_file(),
   258             '--dhcp-optsfile=%s' % self._output_opts_file(),
   321             '--dhcp-optsfile=%s' % self._output_opts_file(),
   259             # '--dhcp-script=%s' % self._lease_relay_script_path(),
       
   260             '--leasefile-ro',
   322             '--leasefile-ro',
   261         ]
   323         ]
   262 
   324 
       
   325         possible_leases = 0
   263         for i, subnet in enumerate(self.network.subnets):
   326         for i, subnet in enumerate(self.network.subnets):
   264             # if a subnet is specified to have dhcp disabled
   327             # if a subnet is specified to have dhcp disabled
   265             if not subnet.enable_dhcp:
   328             if not subnet.enable_dhcp:
   266                 continue
   329                 continue
   267             if subnet.ip_version == 4:
   330             if subnet.ip_version == 4:
   268                 mode = 'static'
   331                 mode = 'static'
   269             else:
   332             else:
   270                 # TODO(gmoodalb): how do we indicate other options
   333                 # TODO(gmoodalb): how do we indicate other options
   271                 # ra-only, slaac, ra-nameservers, and ra-stateless.
   334                 # ra-only, slaac, ra-nameservers, and ra-stateless.
   272                 # We need to also set the DUID for DHCPv6 server to use
   335                 # We need to also set the DUID for the DHCPv6 server to use
   273                 macaddr_cmd = ['/usr/sbin/dladm', 'show-linkprop',
   336                 macaddr_cmd = ['/usr/sbin/dladm', 'show-linkprop',
   274                                '-co', 'value', '-p', 'mac-address',
   337                                '-co', 'value', '-p', 'mac-address',
   275                                self.interface_name]
   338                                self.interface_name]
   276                 stdout = utils.execute(macaddr_cmd)
   339                 stdout = utils.execute(macaddr_cmd)
   277                 uid = stdout.splitlines()[0].strip()
   340                 uid = stdout.splitlines()[0].strip()
   278                 # IANA assigned ID for Oracle
   341                 # IANA assigned ID for Oracle
   279                 enterprise_id = '111'
   342                 enterprise_id = '111'
   280                 cmd.append('--dhcp-duid=%s,%s' % (enterprise_id, uid))
   343                 cmd.append('--dhcp-duid=%s,%s' % (enterprise_id, uid))
   281                 mode = 'static'
   344                 mode = 'static'
   282             cmd.append('--dhcp-range=set:%s,%s,%s,%ss' %
   345             if self.version >= self.MINIMUM_VERSION:
   283                        (self._TAG_PREFIX % i,
   346                 set_tag = 'set:'
   284                         netaddr.IPNetwork(subnet.cidr).network,
   347             else:
   285                         mode, self.conf.dhcp_lease_time))
   348                 set_tag = ''
       
   349 
       
   350             cidr = netaddr.IPNetwork(subnet.cidr)
       
   351 
       
   352             cmd.append('--dhcp-range=%s%s,%s,%s,%ss' %
       
   353                        (set_tag, self._TAG_PREFIX % i,
       
   354                         cidr.network,
       
   355                         mode,
       
   356                         self.conf.dhcp_lease_duration))
       
   357             possible_leases += cidr.size
       
   358 
       
   359         # Cap the limit because creating lots of subnets can inflate
       
   360         # this possible lease cap.
       
   361         cmd.append('--dhcp-lease-max=%d' %
       
   362                    min(possible_leases, self.conf.dnsmasq_lease_max))
   286 
   363 
   287         cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file)
   364         cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file)
   288         if self.conf.dnsmasq_dns_server:
   365         if self.conf.dnsmasq_dns_server:
   289             cmd.append('--server=%s' % self.conf.dnsmasq_dns_server)
   366             cmd.append('--server=%s' % self.conf.dnsmasq_dns_server)
   290 
   367 
   291         if self.conf.dhcp_domain:
   368         if self.conf.dhcp_domain:
   292             cmd.append('--domain=%s' % self.conf.dhcp_domain)
   369             cmd.append('--domain=%s' % self.conf.dhcp_domain)
   293         utils.execute(cmd)
   370 
       
   371         # TODO(gmoodalb): prepend the env vars before command
       
   372         utils.execute(cmd, self.root_helper)
       
   373 
       
   374     def release_lease(self, mac_address, removed_ips):
       
   375         # TODO(gmoodalb): we need to support dnsmasq's dhcp_release
       
   376         pass
   294 
   377 
   295     def reload_allocations(self):
   378     def reload_allocations(self):
   296         """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
   379         """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
   297 
   380 
   298         # If all subnets turn off dhcp, kill the process.
   381         # If all subnets turn off dhcp, kill the process.
   302                         'turned off DHCP: %s'), self.network.id)
   385                         'turned off DHCP: %s'), self.network.id)
   303             return
   386             return
   304 
   387 
   305         self._output_hosts_file()
   388         self._output_hosts_file()
   306         self._output_opts_file()
   389         self._output_opts_file()
   307 
       
   308         if self.active:
   390         if self.active:
   309             cmd = ['kill', '-HUP', self.pid]
   391             cmd = ['kill', '-HUP', self.pid]
   310             utils.execute(cmd)
   392             utils.execute(cmd, self.root_helper)
   311         else:
   393         else:
   312             LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid)
   394             LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid)
   313         LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
   395         LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
       
   396         self.device_manager.update(self.network)
   314 
   397 
   315     def _output_hosts_file(self):
   398     def _output_hosts_file(self):
   316         """Writes a dnsmasq compatible hosts file."""
   399         """Writes a dnsmasq compatible hosts file."""
   317         r = re.compile('[:.]')
   400         r = re.compile('[:.]')
   318         buf = StringIO.StringIO()
   401         buf = StringIO.StringIO()
   319 
   402 
   320         for port in self.network.ports:
   403         for port in self.network.ports:
   321             for alloc in port.fixed_ips:
   404             for alloc in port.fixed_ips:
   322                 name = 'host-%s.%s' % (r.sub('-', alloc.ip_address),
   405                 name = 'host-%s.%s' % (r.sub('-', alloc.ip_address),
   323                                        self.conf.dhcp_domain)
   406                                        self.conf.dhcp_domain)
   324                 buf.write('%s,%s,%s\n' %
   407                 set_tag = ''
   325                           (port.mac_address, name, alloc.ip_address))
   408                 if getattr(port, 'extra_dhcp_opts', False):
       
   409                     if self.version >= self.MINIMUM_VERSION:
       
   410                         set_tag = 'set:'
       
   411 
       
   412                     buf.write('%s,%s,%s,%s%s\n' %
       
   413                               (port.mac_address, name, alloc.ip_address,
       
   414                                set_tag, port.id))
       
   415                 else:
       
   416                     buf.write('%s,%s,%s\n' %
       
   417                               (port.mac_address, name, alloc.ip_address))
   326 
   418 
   327         name = self.get_conf_file_name('host')
   419         name = self.get_conf_file_name('host')
   328         utils.replace_file(name, buf.getvalue())
   420         utils.replace_file(name, buf.getvalue())
   329         return name
   421         return name
   330 
   422 
   331     def _output_opts_file(self):
   423     def _output_opts_file(self):
   332         """Write a dnsmasq compatible options file."""
   424         """Write a dnsmasq compatible options file."""
       
   425 
       
   426         if self.conf.enable_isolated_metadata:
       
   427             subnet_to_interface_ip = self._make_subnet_interface_ip_map()
   333 
   428 
   334         options = []
   429         options = []
   335         for i, subnet in enumerate(self.network.subnets):
   430         for i, subnet in enumerate(self.network.subnets):
   336             if not subnet.enable_dhcp:
   431             if not subnet.enable_dhcp:
   337                 continue
   432                 continue
   338             if subnet.dns_nameservers:
   433             if subnet.dns_nameservers:
   339                 options.append(
   434                 options.append(
   340                     self._format_option(i, 'dns-server',
   435                     self._format_option(i, 'dns-server',
   341                                         ','.join(subnet.dns_nameservers)))
   436                                         ','.join(subnet.dns_nameservers)))
   342 
   437 
   343             host_routes = ["%s,%s" % (hr.destination, hr.nexthop)
   438             gateway = subnet.gateway_ip
   344                            for hr in subnet.host_routes]
   439             host_routes = []
       
   440             for hr in subnet.host_routes:
       
   441                 if hr.destination == "0.0.0.0/0":
       
   442                     gateway = hr.nexthop
       
   443                 else:
       
   444                     host_routes.append("%s,%s" % (hr.destination, hr.nexthop))
       
   445 
       
   446             # Add host routes for isolated network segments
       
   447             enable_metadata = (
       
   448                 self.conf.enable_isolated_metadata
       
   449                 and not subnet.gateway_ip
       
   450                 and subnet.ip_version == 4)
       
   451 
       
   452             if enable_metadata:
       
   453                 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id]
       
   454                 host_routes.append(
       
   455                     '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)
       
   456                 )
   345 
   457 
   346             if host_routes:
   458             if host_routes:
   347                 options.append(
   459                 options.append(
   348                     self._format_option(i, 'classless-static-route',
   460                     self._format_option(i, 'classless-static-route',
   349                                         ','.join(host_routes)))
   461                                         ','.join(host_routes)))
       
   462                 options.append(
       
   463                     self._format_option(i, WIN2k3_STATIC_DNS,
       
   464                                         ','.join(host_routes)))
   350 
   465 
   351             if subnet.ip_version == 4:
   466             if subnet.ip_version == 4:
   352                 if subnet.gateway_ip:
   467                 if gateway:
   353                     options.append(self._format_option(i, 'router',
   468                     options.append(self._format_option(i, 'router', gateway))
   354                                                        subnet.gateway_ip))
       
   355                 else:
   469                 else:
   356                     options.append(self._format_option(i, 'router'))
   470                     options.append(self._format_option(i, 'router'))
       
   471 
       
   472         for port in self.network.ports:
       
   473             if getattr(port, 'extra_dhcp_opts', False):
       
   474                 options.extend(
       
   475                     self._format_option(port.id, opt.opt_name, opt.opt_value)
       
   476                     for opt in port.extra_dhcp_opts)
   357 
   477 
   358         name = self.get_conf_file_name('opts')
   478         name = self.get_conf_file_name('opts')
   359         utils.replace_file(name, '\n'.join(options))
   479         utils.replace_file(name, '\n'.join(options))
   360         return name
   480         return name
   361 
   481 
   362     def _format_option(self, index, option_name, *args):
   482     def _make_subnet_interface_ip_map(self):
   363         return ','.join(('tag:' + self._TAG_PREFIX % index,
   483         # TODO(gmoodalb): need to complete this when we support metadata
   364                          'option:%s' % option_name) + args)
   484         pass
       
   485 
       
   486     def _format_option(self, tag, option, *args):
       
   487         """Format DHCP option by option name or code."""
       
   488         if self.version >= self.MINIMUM_VERSION:
       
   489             set_tag = 'tag:'
       
   490         else:
       
   491             set_tag = ''
       
   492 
       
   493         option = str(option)
       
   494 
       
   495         if isinstance(tag, int):
       
   496             tag = self._TAG_PREFIX % tag
       
   497 
       
   498         if not option.isdigit():
       
   499             option = 'option:%s' % option
       
   500 
       
   501         return ','.join((set_tag + tag, '%s' % option) + args)
   365 
   502 
   366     @classmethod
   503     @classmethod
   367     def lease_update(cls):
   504     def lease_update(cls):
       
   505         network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY)
       
   506         dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY)
       
   507 
       
   508         action = sys.argv[1]
       
   509         if action not in ('add', 'del', 'old'):
       
   510             sys.exit()
       
   511 
       
   512         mac_address = sys.argv[2]
       
   513         ip_address = sys.argv[3]
       
   514 
       
   515         if action == 'del':
       
   516             lease_remaining = 0
       
   517         else:
       
   518             lease_remaining = int(os.environ.get('DNSMASQ_TIME_REMAINING', 0))
       
   519 
       
   520         data = dict(network_id=network_id, mac_address=mac_address,
       
   521                     ip_address=ip_address, lease_remaining=lease_remaining)
       
   522 
       
   523         if os.path.exists(dhcp_relay_socket):
       
   524             sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
       
   525             sock.connect(dhcp_relay_socket)
       
   526             sock.send(jsonutils.dumps(data))
       
   527             sock.close()
       
   528 
       
   529 
       
   530 class DeviceManager(object):
       
   531 
       
   532     def __init__(self, conf, root_helper, plugin):
       
   533         self.conf = conf
       
   534         self.root_helper = root_helper
       
   535         self.plugin = plugin
       
   536         if not conf.interface_driver:
       
   537             raise SystemExit(_('You must specify an interface driver'))
       
   538         try:
       
   539             self.driver = importutils.import_object(
       
   540                 conf.interface_driver, conf)
       
   541         except Exception as e:
       
   542             msg = (_("Error importing interface driver '%(driver)s': "
       
   543                    "%(inner)s") % {'driver': conf.interface_driver,
       
   544                                    'inner': e})
       
   545             raise SystemExit(msg)
       
   546 
       
   547     def get_interface_name(self, network, port):
       
   548         """Return interface(device) name for use by the DHCP process."""
       
   549         return self.driver.get_device_name(port)
       
   550 
       
   551     def get_device_id(self, network):
       
   552         """Return a unique DHCP device ID for this host on the network."""
       
   553         # There could be more than one dhcp server per network, so create
       
   554         # a device id that combines host and network ids
       
   555 
       
   556         host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
       
   557         return 'dhcp%s-%s' % (host_uuid, network.id)
       
   558 
       
   559     def setup_dhcp_port(self, network):
       
   560         """Create/update DHCP port for the host if needed and return port."""
       
   561 
       
   562         device_id = self.get_device_id(network)
       
   563         subnets = {}
       
   564         dhcp_enabled_subnet_ids = []
       
   565         for subnet in network.subnets:
       
   566             if subnet.enable_dhcp:
       
   567                 dhcp_enabled_subnet_ids.append(subnet.id)
       
   568                 subnets[subnet.id] = subnet
       
   569 
       
   570         dhcp_port = None
       
   571         for port in network.ports:
       
   572             port_device_id = getattr(port, 'device_id', None)
       
   573             if port_device_id == device_id:
       
   574                 port_fixed_ips = []
       
   575                 for fixed_ip in port.fixed_ips:
       
   576                     port_fixed_ips.append({'subnet_id': fixed_ip.subnet_id,
       
   577                                            'ip_address': fixed_ip.ip_address})
       
   578                     if fixed_ip.subnet_id in dhcp_enabled_subnet_ids:
       
   579                         dhcp_enabled_subnet_ids.remove(fixed_ip.subnet_id)
       
   580 
       
   581                 # If there are dhcp_enabled_subnet_ids here that means that
       
   582                 # we need to add those to the port and call update.
       
   583                 if dhcp_enabled_subnet_ids:
       
   584                     port_fixed_ips.extend(
       
   585                         [dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
       
   586                     dhcp_port = self.plugin.update_dhcp_port(
       
   587                         port.id, {'port': {'fixed_ips': port_fixed_ips}})
       
   588                 else:
       
   589                     dhcp_port = port
       
   590                 # break since we found port that matches device_id
       
   591                 break
       
   592 
       
   593         # DHCP port has not yet been created.
       
   594         if dhcp_port is None:
       
   595             LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
       
   596                         ' does not yet exist.'), {'device_id': device_id,
       
   597                                                   'network_id': network.id})
       
   598             port_dict = dict(
       
   599                 name='',
       
   600                 admin_state_up=True,
       
   601                 device_id=device_id,
       
   602                 network_id=network.id,
       
   603                 tenant_id=network.tenant_id,
       
   604                 fixed_ips=[dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
       
   605             dhcp_port = self.plugin.create_dhcp_port({'port': port_dict})
       
   606 
       
   607         # Convert subnet_id to subnet dict
       
   608         fixed_ips = [dict(subnet_id=fixed_ip.subnet_id,
       
   609                           ip_address=fixed_ip.ip_address,
       
   610                           subnet=subnets[fixed_ip.subnet_id])
       
   611                      for fixed_ip in dhcp_port.fixed_ips]
       
   612 
       
   613         ips = [DictModel(item) if isinstance(item, dict) else item
       
   614                for item in fixed_ips]
       
   615         dhcp_port.fixed_ips = ips
       
   616 
       
   617         return dhcp_port
       
   618 
       
   619     def setup(self, network, reuse_existing=False):
       
   620         """Create and initialize a device for network's DHCP on this host."""
       
   621         port = self.setup_dhcp_port(network)
       
   622         interface_name = self.get_interface_name(network, port)
       
   623 
       
   624         if net_lib.Datalink.datalink_exists(interface_name):
       
   625             if not reuse_existing:
       
   626                 raise exceptions.PreexistingDeviceFailure(
       
   627                     dev_name=interface_name)
       
   628 
       
   629                 LOG.debug(_('Reusing existing device: %s.'), interface_name)
       
   630         else:
       
   631             self.driver.plug(network.tenant_id, network.id,
       
   632                              port.id,
       
   633                              interface_name)
       
   634         ip_cidrs = []
       
   635         for fixed_ip in port.fixed_ips:
       
   636             subnet = fixed_ip.subnet
       
   637             net = netaddr.IPNetwork(subnet.cidr)
       
   638             ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
       
   639             ip_cidrs.append(ip_cidr)
       
   640 
       
   641         if (self.conf.enable_isolated_metadata and self.conf.use_namespaces):
       
   642             ip_cidrs.append(METADATA_DEFAULT_CIDR)
       
   643 
       
   644         self.driver.init_l3(interface_name, ip_cidrs)
       
   645 
       
   646         return interface_name
       
   647 
       
   648     def update(self, network):
       
   649         """Update device settings for the network's DHCP on this host."""
   368         pass
   650         pass
       
   651 
       
   652     def destroy(self, network, device_name):
       
   653         """Destroy the device used for the network's DHCP on this host."""
       
   654 
       
   655         self.driver.fini_l3(device_name)
       
   656         self.driver.unplug(device_name)
       
   657         self.plugin.release_dhcp_port(network.id,
       
   658                                       self.get_device_id(network))