components/openstack/neutron/files/agent/solaris/dhcp.py
branchs11u2-sru
changeset 4156 4b1def16fe9b
parent 3619 639868f63ef4
equal deleted inserted replaced
4146:097063f324c0 4156:4b1def16fe9b
    18 #    under the License.
    18 #    under the License.
    19 #
    19 #
    20 # @author: Girish Moodalbail, Oracle, Inc.
    20 # @author: Girish Moodalbail, Oracle, Inc.
    21 
    21 
    22 import abc
    22 import abc
       
    23 import collections
    23 import os
    24 import os
    24 import re
    25 import re
    25 import shutil
    26 import shutil
    26 import socket
    27 import socket
    27 import StringIO
       
    28 import sys
    28 import sys
    29 import uuid
    29 import uuid
    30 
    30 
    31 import netaddr
    31 import netaddr
    32 from oslo.config import cfg
    32 from oslo.config import cfg
       
    33 import six
    33 
    34 
    34 from neutron.agent.linux import utils
    35 from neutron.agent.linux import utils
    35 from neutron.agent.solaris import net_lib
    36 from neutron.agent.solaris import net_lib
    36 from neutron.common import constants
    37 from neutron.common import constants
    37 from neutron.common import exceptions
    38 from neutron.common import exceptions
    50                default='openstacklocal',
    51                default='openstacklocal',
    51                help=_('Domain to use for building the hostnames')),
    52                help=_('Domain to use for building the hostnames')),
    52     cfg.StrOpt('dnsmasq_config_file',
    53     cfg.StrOpt('dnsmasq_config_file',
    53                default='',
    54                default='',
    54                help=_('Override the default dnsmasq settings with this file')),
    55                help=_('Override the default dnsmasq settings with this file')),
    55     cfg.StrOpt('dnsmasq_dns_server',
    56     cfg.ListOpt('dnsmasq_dns_servers',
    56                help=_('Use another DNS server before any in '
    57                 help=_('Comma-separated list of the DNS servers which will be '
    57                       '/etc/resolv.conf.')),
    58                        'used as forwarders.'),
       
    59                 deprecated_name='dnsmasq_dns_server'),
       
    60     cfg.BoolOpt('dhcp_delete_namespaces', default=False,
       
    61                 help=_("Delete namespace after removing a dhcp server.")),
    58     cfg.IntOpt(
    62     cfg.IntOpt(
    59         'dnsmasq_lease_max',
    63         'dnsmasq_lease_max',
    60         default=(2 ** 24),
    64         default=(2 ** 24),
    61         help=_('Limit number of leases to prevent a denial-of-service.')),
    65         help=_('Limit number of leases to prevent a denial-of-service.')),
    62     cfg.StrOpt('interface_driver',
       
    63                help=_("The driver used to manage the virtual interface.")),
       
    64 ]
    66 ]
    65 
    67 
    66 IPV4 = 4
    68 IPV4 = 4
    67 IPV6 = 6
    69 IPV6 = 6
    68 UDP = 'udp'
    70 UDP = 'udp'
    76                                    METADATA_DEFAULT_PREFIX)
    78                                    METADATA_DEFAULT_PREFIX)
    77 METADATA_PORT = 80
    79 METADATA_PORT = 80
    78 WIN2k3_STATIC_DNS = 249
    80 WIN2k3_STATIC_DNS = 249
    79 
    81 
    80 
    82 
    81 class DictModel(object):
    83 class DictModel(dict):
    82     """Convert dict into an object that provides attribute access to values."""
    84     """Convert dict into an object that provides attribute access to values."""
    83     def __init__(self, d):
    85 
    84         for key, value in d.iteritems():
    86     def __init__(self, *args, **kwargs):
    85             if isinstance(value, list):
    87         """Convert dict values to DictModel values."""
    86                 value = [DictModel(item) if isinstance(item, dict) else item
    88         super(DictModel, self).__init__(*args, **kwargs)
    87                          for item in value]
    89 
    88             elif isinstance(value, dict):
    90         def needs_upgrade(item):
    89                 value = DictModel(value)
    91             """Check if `item` is a dict and needs to be changed to DictModel.
    90 
    92             """
    91             setattr(self, key, value)
    93             return isinstance(item, dict) and not isinstance(item, DictModel)
       
    94 
       
    95         def upgrade(item):
       
    96             """Upgrade item if it needs to be upgraded."""
       
    97             if needs_upgrade(item):
       
    98                 return DictModel(item)
       
    99             else:
       
   100                 return item
       
   101 
       
   102         for key, value in self.iteritems():
       
   103             if isinstance(value, (list, tuple)):
       
   104                 # Keep the same type but convert dicts to DictModels
       
   105                 self[key] = type(value)(
       
   106                     (upgrade(item) for item in value)
       
   107                 )
       
   108             elif needs_upgrade(value):
       
   109                 # Change dict instance values to DictModel instance values
       
   110                 self[key] = DictModel(value)
       
   111 
       
   112     def __getattr__(self, name):
       
   113         try:
       
   114             return self[name]
       
   115         except KeyError as e:
       
   116             raise AttributeError(e)
       
   117 
       
   118     def __setattr__(self, name, value):
       
   119         self[name] = value
       
   120 
       
   121     def __delattr__(self, name):
       
   122         del self[name]
    92 
   123 
    93 
   124 
    94 class NetModel(DictModel):
   125 class NetModel(DictModel):
    95 
   126 
    96     def __init__(self, use_namespaces, d):
   127     def __init__(self, use_namespaces, d):
   101     @property
   132     @property
   102     def namespace(self):
   133     def namespace(self):
   103         return self._ns_name
   134         return self._ns_name
   104 
   135 
   105 
   136 
       
   137 @six.add_metaclass(abc.ABCMeta)
   106 class DhcpBase(object):
   138 class DhcpBase(object):
   107     __metaclass__ = abc.ABCMeta
       
   108 
   139 
   109     def __init__(self, conf, network, root_helper='sudo',
   140     def __init__(self, conf, network, root_helper='sudo',
   110                  version=None, plugin=None):
   141                  version=None, plugin=None):
   111         self.conf = conf
   142         self.conf = conf
   112         self.network = network
   143         self.network = network
   125 
   156 
   126     def restart(self):
   157     def restart(self):
   127         """Restart the dhcp service for the network."""
   158         """Restart the dhcp service for the network."""
   128         self.disable(retain_port=True)
   159         self.disable(retain_port=True)
   129         self.enable()
   160         self.enable()
   130         self.device_manager.update(self.network)
       
   131 
   161 
   132     @abc.abstractproperty
   162     @abc.abstractproperty
   133     def active(self):
   163     def active(self):
   134         """Boolean representing the running state of the DHCP server."""
   164         """Boolean representing the running state of the DHCP server."""
   135 
   165 
   136     @abc.abstractmethod
   166     @abc.abstractmethod
   137     def release_lease(self, mac_address, removed_ips):
       
   138         """Release a DHCP lease."""
       
   139 
       
   140     @abc.abstractmethod
       
   141     def reload_allocations(self):
   167     def reload_allocations(self):
   142         """Force the DHCP server to reload the assignment database."""
   168         """Force the DHCP server to reload the assignment database."""
   143 
   169 
   144     @classmethod
   170     @classmethod
   145     def existing_dhcp_networks(cls, conf, root_helper):
   171     def existing_dhcp_networks(cls, conf, root_helper):
   146         """Return a list of existing networks ids that we have configs for."""
   172         """Return a list of existing networks ids that we have configs for."""
   147 
   173 
   148         raise NotImplementedError
   174         raise NotImplementedError()
   149 
   175 
   150     @classmethod
   176     @classmethod
   151     def check_version(cls):
   177     def check_version(cls):
   152         """Execute version checks on DHCP server."""
   178         """Execute version checks on DHCP server."""
   153 
   179 
   154         raise NotImplementedError
   180         raise NotImplementedError()
       
   181 
       
   182     @classmethod
       
   183     def get_isolated_subnets(cls, network):
       
   184         """Returns a dict indicating whether or not a subnet is isolated"""
       
   185         raise NotImplementedError()
       
   186 
       
   187     @classmethod
       
   188     def should_enable_metadata(cls, conf, network):
       
   189         """True if the metadata-proxy should be enabled for the network."""
       
   190         raise NotImplementedError()
   155 
   191 
   156 
   192 
   157 class DhcpLocalProcess(DhcpBase):
   193 class DhcpLocalProcess(DhcpBase):
   158     PORTS = []
   194     PORTS = []
   159 
   195 
   164                 return True
   200                 return True
   165         return False
   201         return False
   166 
   202 
   167     def enable(self):
   203     def enable(self):
   168         """Enables DHCP for this network by spawning a local process."""
   204         """Enables DHCP for this network by spawning a local process."""
   169         interface_name = self.device_manager.setup(self.network,
       
   170                                                    reuse_existing=True)
       
   171         if self.active:
   205         if self.active:
   172             self.restart()
   206             self.restart()
   173         elif self._enable_dhcp():
   207         elif self._enable_dhcp():
       
   208             interface_name = self.device_manager.setup(self.network)
   174             self.interface_name = interface_name
   209             self.interface_name = interface_name
   175             self.spawn_process()
   210             self.spawn_process()
   176 
   211 
   177     def disable(self, retain_port=False):
   212     def disable(self, retain_port=False):
   178         """Disable DHCP for this network by killing the local process."""
   213         """Disable DHCP for this network by killing the local process."""
   179         pid = self.pid
   214         pid = self.pid
   180 
   215 
   181         if self.active:
   216         if pid:
   182             cmd = ['kill', '-9', pid]
   217             if self.active:
   183             utils.execute(cmd, self.root_helper)
   218                 cmd = ['kill', '-9', pid]
       
   219                 utils.execute(cmd, self.root_helper)
       
   220             else:
       
   221                 LOG.debug(_('DHCP for %(net_id)s is stale, pid %(pid)d '
       
   222                             'does not exist, performing cleanup'),
       
   223                           {'net_id': self.network.id, 'pid': pid})
   184             if not retain_port:
   224             if not retain_port:
   185                 self.device_manager.destroy(self.network, self.interface_name)
   225                 self.device_manager.destroy(self.network,
   186 
   226                                             self.interface_name)
   187         elif pid:
       
   188             LOG.debug(_('DHCP for %(net_id)s pid %(pid)d is stale, ignoring '
       
   189                         'command'), {'net_id': self.network.id, 'pid': pid})
       
   190         else:
   227         else:
   191             LOG.debug(_('No DHCP started for %s'), self.network.id)
   228             LOG.debug(_('No DHCP started for %s'), self.network.id)
   192 
   229 
   193         self._remove_config_files()
   230         self._remove_config_files()
   194 
   231 
   266 
   303 
   267     _TAG_PREFIX = 'tag%d'
   304     _TAG_PREFIX = 'tag%d'
   268 
   305 
   269     NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID'
   306     NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID'
   270     NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH'
   307     NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH'
   271     MINIMUM_VERSION = 2.59
   308     MINIMUM_VERSION = 2.63
       
   309     MINIMUM_IPV6_VERSION = 2.67
   272 
   310 
   273     @classmethod
   311     @classmethod
   274     def check_version(cls):
   312     def check_version(cls):
   275         ver = 0
   313         ver = 0
   276         try:
   314         try:
   283             if not is_valid_version:
   321             if not is_valid_version:
   284                 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
   322                 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
   285                               'DHCP AGENT MAY NOT RUN CORRECTLY! '
   323                               'DHCP AGENT MAY NOT RUN CORRECTLY! '
   286                               'Please ensure that its version is %s '
   324                               'Please ensure that its version is %s '
   287                               'or above!'), cls.MINIMUM_VERSION)
   325                               'or above!'), cls.MINIMUM_VERSION)
       
   326                 raise SystemExit(1)
       
   327             is_valid_version = float(ver) >= cls.MINIMUM_IPV6_VERSION
       
   328             if not is_valid_version:
       
   329                 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
       
   330                               'DHCP AGENT MAY NOT RUN CORRECTLY WHEN '
       
   331                               'SERVING IPV6 STATEFUL SUBNETS! '
       
   332                               'Please ensure that its version is %s '
       
   333                               'or above!'), cls.MINIMUM_IPV6_VERSION)
   288         except (OSError, RuntimeError, IndexError, ValueError):
   334         except (OSError, RuntimeError, IndexError, ValueError):
   289             LOG.warning(_('Unable to determine dnsmasq version. '
   335             LOG.warning(_('Unable to determine dnsmasq version. '
   290                           'Please ensure that its version is %s '
   336                           'Please ensure that its version is %s '
   291                           'or above!'), cls.MINIMUM_VERSION)
   337                           'or above!'), cls.MINIMUM_VERSION)
       
   338             raise SystemExit(1)
   292         return float(ver)
   339         return float(ver)
   293 
   340 
   294     @classmethod
   341     @classmethod
   295     def existing_dhcp_networks(cls, conf, root_helper):
   342     def existing_dhcp_networks(cls, conf, root_helper):
   296         """Return a list of existing networks ids that we have configs for."""
   343         """Return a list of existing networks ids that we have configs for."""
   317             '--interface=%s' % self.interface_name,
   364             '--interface=%s' % self.interface_name,
   318             '--except-interface=lo0',
   365             '--except-interface=lo0',
   319             '--pid-file=%s' % self.get_conf_file_name(
   366             '--pid-file=%s' % self.get_conf_file_name(
   320                 'pid', ensure_conf_dir=True),
   367                 'pid', ensure_conf_dir=True),
   321             '--dhcp-hostsfile=%s' % self._output_hosts_file(),
   368             '--dhcp-hostsfile=%s' % self._output_hosts_file(),
       
   369             '--addn-hosts=%s' % self._output_addn_hosts_file(),
   322             '--dhcp-optsfile=%s' % self._output_opts_file(),
   370             '--dhcp-optsfile=%s' % self._output_opts_file(),
   323             '--leasefile-ro',
   371             '--leasefile-ro',
   324         ]
   372         ]
   325 
   373 
   326         possible_leases = 0
   374         possible_leases = 0
   327         for i, subnet in enumerate(self.network.subnets):
   375         for i, subnet in enumerate(self.network.subnets):
       
   376             mode = None
   328             # if a subnet is specified to have dhcp disabled
   377             # if a subnet is specified to have dhcp disabled
   329             if not subnet.enable_dhcp:
   378             if not subnet.enable_dhcp:
   330                 continue
   379                 continue
   331             if subnet.ip_version == 4:
   380             if subnet.ip_version == 4:
   332                 mode = 'static'
   381                 mode = 'static'
   333             else:
   382             else:
   334                 # TODO(gmoodalb): how do we indicate other options
       
   335                 # ra-only, slaac, ra-nameservers, and ra-stateless.
       
   336                 # We need to also set the DUID for the DHCPv6 server to use
   383                 # We need to also set the DUID for the DHCPv6 server to use
   337                 macaddr_cmd = ['/usr/sbin/dladm', 'show-linkprop',
   384                 macaddr_cmd = ['/usr/sbin/dladm', 'show-linkprop',
   338                                '-co', 'value', '-p', 'mac-address',
   385                                '-co', 'value', '-p', 'mac-address',
   339                                self.interface_name]
   386                                self.interface_name]
   340                 stdout = utils.execute(macaddr_cmd)
   387                 stdout = utils.execute(macaddr_cmd)
   341                 uid = stdout.splitlines()[0].strip()
   388                 uid = stdout.splitlines()[0].strip()
       
   389                 # format the MAC address
       
   390                 uid = ':'.join(['%.2x' % w for w in netaddr.EUI(uid).words])
   342                 # IANA assigned ID for Oracle
   391                 # IANA assigned ID for Oracle
   343                 enterprise_id = '111'
   392                 enterprise_id = '111'
   344                 cmd.append('--dhcp-duid=%s,%s' % (enterprise_id, uid))
   393                 cmd.append('--dhcp-duid=%s,%s' % (enterprise_id, uid))
   345                 mode = 'static'
   394 
   346             if self.version >= self.MINIMUM_VERSION:
   395                 # Note(scollins) If the IPv6 attributes are not set, set it as
   347                 set_tag = 'set:'
   396                 # static to preserve previous behavior
       
   397                 addr_mode = getattr(subnet, 'ipv6_address_mode', None)
       
   398                 ra_mode = getattr(subnet, 'ipv6_ra_mode', None)
       
   399                 if (addr_mode in [constants.DHCPV6_STATEFUL,
       
   400                                   constants.DHCPV6_STATELESS] or
       
   401                         not addr_mode and not ra_mode):
       
   402                     mode = 'static'
       
   403 
       
   404             cidr = netaddr.IPNetwork(subnet.cidr)
       
   405 
       
   406             if self.conf.dhcp_lease_duration == -1:
       
   407                 lease = 'infinite'
   348             else:
   408             else:
   349                 set_tag = ''
   409                 lease = '%ss' % self.conf.dhcp_lease_duration
   350 
   410 
   351             cidr = netaddr.IPNetwork(subnet.cidr)
   411             # mode is optional and is not set - skip it
   352 
   412             if mode:
   353             cmd.append('--dhcp-range=%s%s,%s,%s,%ss' %
   413                 if subnet.ip_version == 4:
   354                        (set_tag, self._TAG_PREFIX % i,
   414                     cmd.append('--dhcp-range=%s%s,%s,%s,%s' %
   355                         cidr.network,
   415                                ('set:', self._TAG_PREFIX % i,
   356                         mode,
   416                                 cidr.network, mode, lease))
   357                         self.conf.dhcp_lease_duration))
   417                 else:
   358             possible_leases += cidr.size
   418                     cmd.append('--dhcp-range=%s%s,%s,%s,%d,%s' %
       
   419                                ('set:', self._TAG_PREFIX % i,
       
   420                                 cidr.network, mode,
       
   421                                 cidr.prefixlen, lease))
       
   422                 possible_leases += cidr.size
   359 
   423 
   360         # Cap the limit because creating lots of subnets can inflate
   424         # Cap the limit because creating lots of subnets can inflate
   361         # this possible lease cap.
   425         # this possible lease cap.
   362         cmd.append('--dhcp-lease-max=%d' %
   426         cmd.append('--dhcp-lease-max=%d' %
   363                    min(possible_leases, self.conf.dnsmasq_lease_max))
   427                    min(possible_leases, self.conf.dnsmasq_lease_max))
   364 
   428 
   365         cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file)
   429         cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file)
   366         if self.conf.dnsmasq_dns_server:
   430         if self.conf.dnsmasq_dns_servers:
   367             cmd.append('--server=%s' % self.conf.dnsmasq_dns_server)
   431             cmd.extend(
       
   432                 '--server=%s' % server
       
   433                 for server in self.conf.dnsmasq_dns_servers)
   368 
   434 
   369         if self.conf.dhcp_domain:
   435         if self.conf.dhcp_domain:
   370             cmd.append('--domain=%s' % self.conf.dhcp_domain)
   436             cmd.append('--domain=%s' % self.conf.dhcp_domain)
   371 
   437 
   372         # TODO(gmoodalb): prepend the env vars before command
   438         # TODO(gmoodalb): prepend the env vars before command
   373         utils.execute(cmd, self.root_helper)
   439         utils.execute(cmd, self.root_helper)
   374 
   440 
   375     def release_lease(self, mac_address, removed_ips):
   441     def _release_lease(self, mac_address, ip):
   376         """Release a DHCP lease."""
   442         """Release a DHCP lease."""
   377         for ip in removed_ips or []:
   443         cmd = ['/usr/lib/inet/dhcp_release', self.interface_name,
   378             cmd = ['/usr/lib/inet/dhcp_release', self.interface_name,
   444                ip, mac_address]
   379                    ip, mac_address]
   445         utils.execute(cmd, self.root_helper)
   380             utils.execute(cmd, self.root_helper)
       
   381 
   446 
   382     def reload_allocations(self):
   447     def reload_allocations(self):
   383         """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
   448         """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
   384 
   449 
   385         # If all subnets turn off dhcp, kill the process.
   450         # If all subnets turn off dhcp, kill the process.
   387             self.disable()
   452             self.disable()
   388             LOG.debug(_('Killing dhcpmasq for network since all subnets have '
   453             LOG.debug(_('Killing dhcpmasq for network since all subnets have '
   389                         'turned off DHCP: %s'), self.network.id)
   454                         'turned off DHCP: %s'), self.network.id)
   390             return
   455             return
   391 
   456 
       
   457         self._release_unused_leases()
   392         self._output_hosts_file()
   458         self._output_hosts_file()
       
   459         self._output_addn_hosts_file()
   393         self._output_opts_file()
   460         self._output_opts_file()
   394         if self.active:
   461         if self.active:
   395             cmd = ['kill', '-HUP', self.pid]
   462             cmd = ['kill', '-HUP', self.pid]
   396             utils.execute(cmd, self.root_helper)
   463             utils.execute(cmd, self.root_helper)
   397         else:
   464         else:
   398             LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid)
   465             LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid)
   399         LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
   466         LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
   400         self.device_manager.update(self.network)
   467         self.device_manager.update(self.network, self.interface_name)
   401 
   468 
   402     def _output_hosts_file(self):
   469     def _iter_hosts(self):
   403         """Writes a dnsmasq compatible hosts file."""
   470         """Iterate over hosts.
   404         r = re.compile('[:.]')
   471 
   405         buf = StringIO.StringIO()
   472         For each host on the network we yield a tuple containing:
   406 
   473         (
       
   474             port,  # a DictModel instance representing the port.
       
   475             alloc,  # a DictModel instance of the allocated ip and subnet.
       
   476             host_name,  # Host name.
       
   477             name,  # Canonical hostname in the format 'hostname[.domain]'.
       
   478         )
       
   479         """
       
   480         v6_nets = dict((subnet.id, subnet) for subnet in
       
   481                        self.network.subnets if subnet.ip_version == 6)
   407         for port in self.network.ports:
   482         for port in self.network.ports:
   408             for alloc in port.fixed_ips:
   483             for alloc in port.fixed_ips:
   409                 name = 'host-%s.%s' % (r.sub('-', alloc.ip_address),
   484                 # Note(scollins) Only create entries that are
   410                                        self.conf.dhcp_domain)
   485                 # associated with the subnet being managed by this
   411                 set_tag = ''
   486                 # dhcp agent
   412                 if getattr(port, 'extra_dhcp_opts', False):
   487                 if alloc.subnet_id in v6_nets:
   413                     if self.version >= self.MINIMUM_VERSION:
   488                     addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode
   414                         set_tag = 'set:'
   489                     if addr_mode != constants.DHCPV6_STATEFUL:
   415 
   490                         continue
   416                     buf.write('%s,%s,%s,%s%s\n' %
   491                 hostname = 'host-%s' % alloc.ip_address.replace(
   417                               (port.mac_address, name, alloc.ip_address,
   492                     '.', '-').replace(':', '-')
   418                                set_tag, port.id))
   493                 fqdn = hostname
   419                 else:
   494                 if self.conf.dhcp_domain:
   420                     buf.write('%s,%s,%s\n' %
   495                     fqdn = '%s.%s' % (fqdn, self.conf.dhcp_domain)
   421                               (port.mac_address, name, alloc.ip_address))
   496                 yield (port, alloc, hostname, fqdn)
   422 
   497 
   423         name = self.get_conf_file_name('host')
   498     def _output_hosts_file(self):
   424         utils.replace_file(name, buf.getvalue())
   499         """Writes a dnsmasq compatible dhcp hosts file.
   425         return name
   500 
       
   501         The generated file is sent to the --dhcp-hostsfile option of dnsmasq,
       
   502         and lists the hosts on the network which should receive a dhcp lease.
       
   503         Each line in this file is in the form::
       
   504 
       
   505             'mac_address,FQDN,ip_address'
       
   506 
       
   507         IMPORTANT NOTE: a dnsmasq instance does not resolve hosts defined in
       
   508         this file if it did not give a lease to a host listed in it (e.g.:
       
   509         multiple dnsmasq instances on the same network if this network is on
       
   510         multiple network nodes). This file is only defining hosts which
       
   511         should receive a dhcp lease, the hosts resolution in itself is
       
   512         defined by the `_output_addn_hosts_file` method.
       
   513         """
       
   514         buf = six.StringIO()
       
   515         filename = self.get_conf_file_name('host')
       
   516 
       
   517         LOG.debug(_('Building host file: %s'), filename)
       
   518         for (port, alloc, hostname, name) in self._iter_hosts():
       
   519             # (dzyu) Check if it is legal ipv6 address, if so, need wrap
       
   520             # it with '[]' to let dnsmasq to distinguish MAC address from
       
   521             # IPv6 address.
       
   522             ip_address = alloc.ip_address
       
   523             if netaddr.valid_ipv6(ip_address):
       
   524                 ip_address = '[%s]' % ip_address
       
   525 
       
   526             LOG.debug(_('Adding %(mac)s : %(name)s : %(ip)s'),
       
   527                       {"mac": port.mac_address, "name": name,
       
   528                        "ip": ip_address})
       
   529 
       
   530             if getattr(port, 'extra_dhcp_opts', False):
       
   531                 buf.write('%s,%s,%s,%s%s\n' %
       
   532                           (port.mac_address, name, ip_address,
       
   533                            'set:', port.id))
       
   534             else:
       
   535                 buf.write('%s,%s,%s\n' %
       
   536                           (port.mac_address, name, ip_address))
       
   537 
       
   538         utils.replace_file(filename, buf.getvalue())
       
   539         LOG.debug(_('Done building host file %s'), filename)
       
   540         return filename
       
   541 
       
   542     def _read_hosts_file_leases(self, filename):
       
   543         leases = set()
       
   544         if os.path.exists(filename):
       
   545             with open(filename) as f:
       
   546                 for l in f.readlines():
       
   547                     host = l.strip().split(',')
       
   548                     leases.add((host[2], host[0]))
       
   549         return leases
       
   550 
       
   551     def _release_unused_leases(self):
       
   552         filename = self.get_conf_file_name('host')
       
   553         old_leases = self._read_hosts_file_leases(filename)
       
   554 
       
   555         new_leases = set()
       
   556         for port in self.network.ports:
       
   557             for alloc in port.fixed_ips:
       
   558                 new_leases.add((alloc.ip_address, port.mac_address))
       
   559 
       
   560         for ip, mac in old_leases - new_leases:
       
   561             self._release_lease(mac, ip)
       
   562 
       
   563     def _output_addn_hosts_file(self):
       
   564         """Writes a dnsmasq compatible additional hosts file.
       
   565 
       
   566         The generated file is sent to the --addn-hosts option of dnsmasq,
       
   567         and lists the hosts on the network which should be resolved even if
       
   568         the dnsmaq instance did not give a lease to the host (see the
       
   569         `_output_hosts_file` method).
       
   570         Each line in this file is in the same form as a standard /etc/hosts
       
   571         file.
       
   572         """
       
   573         buf = six.StringIO()
       
   574         for (port, alloc, hostname, fqdn) in self._iter_hosts():
       
   575             # It is compulsory to write the `fqdn` before the `hostname` in
       
   576             # order to obtain it in PTR responses.
       
   577             buf.write('%s\t%s %s\n' % (alloc.ip_address, fqdn, hostname))
       
   578         addn_hosts = self.get_conf_file_name('addn_hosts')
       
   579         utils.replace_file(addn_hosts, buf.getvalue())
       
   580         return addn_hosts
   426 
   581 
   427     def _output_opts_file(self):
   582     def _output_opts_file(self):
   428         """Write a dnsmasq compatible options file."""
   583         """Write a dnsmasq compatible options file."""
   429 
   584 
   430         if self.conf.enable_isolated_metadata:
   585         if self.conf.enable_isolated_metadata:
   431             subnet_to_interface_ip = self._make_subnet_interface_ip_map()
   586             subnet_to_interface_ip = self._make_subnet_interface_ip_map()
   432 
   587 
   433         options = []
   588         options = []
       
   589 
       
   590         isolated_subnets = self.get_isolated_subnets(self.network)
       
   591         dhcp_ips = collections.defaultdict(list)
       
   592         subnet_idx_map = {}
   434         for i, subnet in enumerate(self.network.subnets):
   593         for i, subnet in enumerate(self.network.subnets):
   435             if not subnet.enable_dhcp:
   594             if (not subnet.enable_dhcp or
       
   595                 (subnet.ip_version == 6 and
       
   596                  getattr(subnet, 'ipv6_address_mode', None)
       
   597                  in [None, constants.IPV6_SLAAC])):
   436                 continue
   598                 continue
   437             if subnet.dns_nameservers:
   599             if subnet.dns_nameservers:
   438                 options.append(
   600                 options.append(
   439                     self._format_option(i, 'dns-server',
   601                     self._format_option(
   440                                         ','.join(subnet.dns_nameservers)))
   602                         subnet.ip_version, i, 'dns-server',
       
   603                         ','.join(
       
   604                             Dnsmasq._convert_to_literal_addrs(
       
   605                                 subnet.ip_version, subnet.dns_nameservers))))
       
   606             else:
       
   607                 # use the dnsmasq ip as nameservers only if there is no
       
   608                 # dns-server submitted by the server
       
   609                 subnet_idx_map[subnet.id] = i
       
   610 
       
   611             if self.conf.dhcp_domain and subnet.ip_version == 6:
       
   612                 options.append('tag:tag%s,option6:domain-search,%s' %
       
   613                                (i, ''.join(self.conf.dhcp_domain)))
   441 
   614 
   442             gateway = subnet.gateway_ip
   615             gateway = subnet.gateway_ip
   443             host_routes = []
   616             host_routes = []
   444             for hr in subnet.host_routes:
   617             for hr in subnet.host_routes:
   445                 if hr.destination == "0.0.0.0/0":
   618                 if hr.destination == "0.0.0.0/0":
   446                     gateway = hr.nexthop
   619                     if not gateway:
       
   620                         gateway = hr.nexthop
   447                 else:
   621                 else:
   448                     host_routes.append("%s,%s" % (hr.destination, hr.nexthop))
   622                     host_routes.append("%s,%s" % (hr.destination, hr.nexthop))
   449 
   623 
   450             # Add host routes for isolated network segments
   624             # Add host routes for isolated network segments
   451             enable_metadata = (
   625 
   452                 self.conf.enable_isolated_metadata
   626             if (isolated_subnets[subnet.id] and
   453                 and not subnet.gateway_ip
   627                     self.conf.enable_isolated_metadata and
   454                 and subnet.ip_version == 4)
   628                     subnet.ip_version == 4):
   455 
       
   456             if enable_metadata:
       
   457                 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id]
   629                 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id]
   458                 host_routes.append(
   630                 host_routes.append(
   459                     '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)
   631                     '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)
   460                 )
   632                 )
   461 
   633 
   462             if host_routes:
       
   463                 options.append(
       
   464                     self._format_option(i, 'classless-static-route',
       
   465                                         ','.join(host_routes)))
       
   466                 options.append(
       
   467                     self._format_option(i, WIN2k3_STATIC_DNS,
       
   468                                         ','.join(host_routes)))
       
   469 
       
   470             if subnet.ip_version == 4:
   634             if subnet.ip_version == 4:
       
   635                 if host_routes:
       
   636                     if gateway:
       
   637                         host_routes.append("%s,%s" % ("0.0.0.0/0", gateway))
       
   638                     options.append(
       
   639                         self._format_option(subnet.ip_version, i,
       
   640                                             'classless-static-route',
       
   641                                             ','.join(host_routes)))
       
   642                     options.append(
       
   643                         self._format_option(subnet.ip_version, i,
       
   644                                             WIN2k3_STATIC_DNS,
       
   645                                             ','.join(host_routes)))
       
   646 
   471                 if gateway:
   647                 if gateway:
   472                     options.append(self._format_option(i, 'router', gateway))
   648                     options.append(self._format_option(subnet.ip_version,
       
   649                                                        i, 'router',
       
   650                                                        gateway))
   473                 else:
   651                 else:
   474                     options.append(self._format_option(i, 'router'))
   652                     options.append(self._format_option(subnet.ip_version,
       
   653                                                        i, 'router'))
   475 
   654 
   476         for port in self.network.ports:
   655         for port in self.network.ports:
   477             if getattr(port, 'extra_dhcp_opts', False):
   656             if getattr(port, 'extra_dhcp_opts', False):
   478                 options.extend(
   657                 for ip_version in (4, 6):
   479                     self._format_option(port.id, opt.opt_name, opt.opt_value)
   658                     if any(
   480                     for opt in port.extra_dhcp_opts)
   659                         netaddr.IPAddress(ip.ip_address).version == ip_version
       
   660                             for ip in port.fixed_ips):
       
   661                         options.extend(
       
   662                             # TODO(xuhanp):Instead of applying extra_dhcp_opts
       
   663                             # to both DHCPv4 and DHCPv6, we need to find a new
       
   664                             # way to specify options for v4 and v6
       
   665                             # respectively. We also need to validate the option
       
   666                             # before applying it.
       
   667                             self._format_option(ip_version, port.id,
       
   668                                                 opt.opt_name, opt.opt_value)
       
   669                             for opt in port.extra_dhcp_opts)
       
   670 
       
   671             # provides all dnsmasq ip as dns-server if there is more than
       
   672             # one dnsmasq for a subnet and there is no dns-server submitted
       
   673             # by the server
       
   674             if port.device_owner == constants.DEVICE_OWNER_DHCP:
       
   675                 for ip in port.fixed_ips:
       
   676                     i = subnet_idx_map.get(ip.subnet_id)
       
   677                     if i is None:
       
   678                         continue
       
   679                     dhcp_ips[i].append(ip.ip_address)
       
   680 
       
   681         for i, ips in dhcp_ips.items():
       
   682             for ip_version in (4, 6):
       
   683                 vx_ips = [ip for ip in ips
       
   684                           if netaddr.IPAddress(ip).version == ip_version]
       
   685                 if vx_ips:
       
   686                     options.append(
       
   687                         self._format_option(
       
   688                             ip_version, i, 'dns-server',
       
   689                             ','.join(
       
   690                                 Dnsmasq._convert_to_literal_addrs(ip_version,
       
   691                                                                   vx_ips))))
   481 
   692 
   482         name = self.get_conf_file_name('opts')
   693         name = self.get_conf_file_name('opts')
   483         utils.replace_file(name, '\n'.join(options))
   694         utils.replace_file(name, '\n'.join(options))
   484         return name
   695         return name
   485 
   696 
   486     def _make_subnet_interface_ip_map(self):
   697     def _make_subnet_interface_ip_map(self):
   487         # TODO(gmoodalb): need to complete this when we support metadata
   698         # TODO(gmoodalb): need to complete this when we support metadata
   488         pass
   699         pass
   489 
   700 
   490     def _format_option(self, tag, option, *args):
   701     def _format_option(self, ip_version, tag, option, *args):
   491         """Format DHCP option by option name or code."""
   702         """Format DHCP option by option name or code."""
   492         if self.version >= self.MINIMUM_VERSION:
       
   493             set_tag = 'tag:'
       
   494         else:
       
   495             set_tag = ''
       
   496 
       
   497         option = str(option)
   703         option = str(option)
   498 
   704 
   499         if isinstance(tag, int):
   705         if isinstance(tag, int):
   500             tag = self._TAG_PREFIX % tag
   706             tag = self._TAG_PREFIX % tag
   501 
   707 
   502         if not option.isdigit():
   708         if not option.isdigit():
   503             option = 'option:%s' % option
   709             if ip_version == 4:
   504 
   710                 option = 'option:%s' % option
   505         return ','.join((set_tag + tag, '%s' % option) + args)
   711             else:
       
   712                 option = 'option6:%s' % option
       
   713 
       
   714         return ','.join(('tag:' + tag, '%s' % option) + args)
       
   715 
       
   716     @staticmethod
       
   717     def _convert_to_literal_addrs(ip_version, ips):
       
   718         if ip_version == 4:
       
   719             return ips
       
   720         return ['[' + ip + ']' for ip in ips]
       
   721 
       
   722     @classmethod
       
   723     def get_isolated_subnets(cls, network):
       
   724         """Returns a dict indicating whether or not a subnet is isolated
       
   725 
       
   726         A subnet is considered non-isolated if there is a port connected to
       
   727         the subnet, and the port's ip address matches that of the subnet's
       
   728         gateway. The port must be owned by a nuetron router.
       
   729         """
       
   730         isolated_subnets = collections.defaultdict(lambda: True)
       
   731         subnets = dict((subnet.id, subnet) for subnet in network.subnets)
       
   732 
       
   733         for port in network.ports:
       
   734             if port.device_owner != constants.DEVICE_OWNER_ROUTER_INTF:
       
   735                 continue
       
   736             for alloc in port.fixed_ips:
       
   737                 if subnets[alloc.subnet_id].gateway_ip == alloc.ip_address:
       
   738                     isolated_subnets[alloc.subnet_id] = False
       
   739 
       
   740         return isolated_subnets
       
   741 
       
   742     @classmethod
       
   743     def should_enable_metadata(cls, conf, network):
       
   744         """True if there exists a subnet for which a metadata proxy is needed
       
   745         """
       
   746         return False
   506 
   747 
   507     @classmethod
   748     @classmethod
   508     def lease_update(cls):
   749     def lease_update(cls):
   509         network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY)
   750         network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY)
   510         dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY)
   751         dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY)
   536     def __init__(self, conf, root_helper, plugin):
   777     def __init__(self, conf, root_helper, plugin):
   537         self.conf = conf
   778         self.conf = conf
   538         self.root_helper = root_helper
   779         self.root_helper = root_helper
   539         self.plugin = plugin
   780         self.plugin = plugin
   540         if not conf.interface_driver:
   781         if not conf.interface_driver:
   541             raise SystemExit(_('You must specify an interface driver'))
   782             msg = _('An interface driver must be specified')
       
   783             LOG.error(msg)
       
   784             raise SystemExit(1)
   542         try:
   785         try:
   543             self.driver = importutils.import_object(
   786             self.driver = importutils.import_object(
   544                 conf.interface_driver, conf)
   787                 conf.interface_driver, conf)
   545         except Exception as e:
   788         except Exception as e:
   546             msg = (_("Error importing interface driver '%(driver)s': "
   789             msg = (_("Error importing interface driver '%(driver)s': "
   547                    "%(inner)s") % {'driver': conf.interface_driver,
   790                    "%(inner)s") % {'driver': conf.interface_driver,
   548                                    'inner': e})
   791                                    'inner': e})
   549             raise SystemExit(msg)
   792             LOG.error(msg)
       
   793             raise SystemExit(1)
   550 
   794 
   551     def get_interface_name(self, network, port):
   795     def get_interface_name(self, network, port):
   552         """Return interface(device) name for use by the DHCP process."""
   796         """Return interface(device) name for use by the DHCP process."""
   553         return self.driver.get_device_name(port)
   797         return self.driver.get_device_name(port)
   554 
   798 
   557         # There could be more than one dhcp server per network, so create
   801         # There could be more than one dhcp server per network, so create
   558         # a device id that combines host and network ids
   802         # a device id that combines host and network ids
   559 
   803 
   560         host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
   804         host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
   561         return 'dhcp%s-%s' % (host_uuid, network.id)
   805         return 'dhcp%s-%s' % (host_uuid, network.id)
       
   806 
       
   807     def _set_default_route(self, network, device_name):
       
   808         """Sets the default gateway for this dhcp namespace.
       
   809 
       
   810         This method is idempotent and will only adjust the route if adjusting
       
   811         it would change it from what it already is.  This makes it safe to call
       
   812         and avoids unnecessary perturbation of the system.
       
   813         """
       
   814         # we do not support namespaces
       
   815         pass
   562 
   816 
   563     def setup_dhcp_port(self, network):
   817     def setup_dhcp_port(self, network):
   564         """Create/update DHCP port for the host if needed and return port."""
   818         """Create/update DHCP port for the host if needed and return port."""
   565 
   819 
   566         device_id = self.get_device_id(network)
   820         device_id = self.get_device_id(network)
   593                 # we need to add those to the port and call update.
   847                 # we need to add those to the port and call update.
   594                 if dhcp_enabled_subnet_ids:
   848                 if dhcp_enabled_subnet_ids:
   595                     port_fixed_ips.extend(
   849                     port_fixed_ips.extend(
   596                         [dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
   850                         [dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
   597                     dhcp_port = self.plugin.update_dhcp_port(
   851                     dhcp_port = self.plugin.update_dhcp_port(
   598                         port.id, {'port': {'fixed_ips': port_fixed_ips}})
   852                         port.id, {'port': {'network_id': network.id,
       
   853                                            'fixed_ips': port_fixed_ips}})
       
   854                     if not dhcp_port:
       
   855                         raise exceptions.Conflict()
   599                 else:
   856                 else:
   600                     dhcp_port = port
   857                     dhcp_port = port
   601                 # break since we found port that matches device_id
   858                 # break since we found port that matches device_id
   602                 break
   859                 break
       
   860 
       
   861         # check for a reserved DHCP port
       
   862         if dhcp_port is None:
       
   863             LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
       
   864                         ' does not yet exist. Checking for a reserved port.'),
       
   865                       {'device_id': device_id, 'network_id': network.id})
       
   866             for port in network.ports:
       
   867                 port_device_id = getattr(port, 'device_id', None)
       
   868                 if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT:
       
   869                     dhcp_port = self.plugin.update_dhcp_port(
       
   870                         port.id, {'port': {'network_id': network.id,
       
   871                                            'device_id': device_id}})
       
   872                     if dhcp_port:
       
   873                         break
   603 
   874 
   604         # DHCP port has not yet been created.
   875         # DHCP port has not yet been created.
   605         if dhcp_port is None:
   876         if dhcp_port is None:
   606             LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
   877             LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
   607                         ' does not yet exist.'), {'device_id': device_id,
   878                         ' does not yet exist.'), {'device_id': device_id,
   613                 network_id=network.id,
   884                 network_id=network.id,
   614                 tenant_id=network.tenant_id,
   885                 tenant_id=network.tenant_id,
   615                 fixed_ips=[dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
   886                 fixed_ips=[dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
   616             dhcp_port = self.plugin.create_dhcp_port({'port': port_dict})
   887             dhcp_port = self.plugin.create_dhcp_port({'port': port_dict})
   617 
   888 
       
   889         if not dhcp_port:
       
   890             raise exceptions.Conflict()
       
   891 
   618         # Convert subnet_id to subnet dict
   892         # Convert subnet_id to subnet dict
   619         fixed_ips = [dict(subnet_id=fixed_ip.subnet_id,
   893         fixed_ips = [dict(subnet_id=fixed_ip.subnet_id,
   620                           ip_address=fixed_ip.ip_address,
   894                           ip_address=fixed_ip.ip_address,
   621                           subnet=subnets[fixed_ip.subnet_id])
   895                           subnet=subnets[fixed_ip.subnet_id])
   622                      for fixed_ip in dhcp_port.fixed_ips]
   896                      for fixed_ip in dhcp_port.fixed_ips]
   625                for item in fixed_ips]
   899                for item in fixed_ips]
   626         dhcp_port.fixed_ips = ips
   900         dhcp_port.fixed_ips = ips
   627 
   901 
   628         return dhcp_port
   902         return dhcp_port
   629 
   903 
   630     def setup(self, network, reuse_existing=False):
   904     def setup(self, network):
   631         """Create and initialize a device for network's DHCP on this host."""
   905         """Create and initialize a device for network's DHCP on this host."""
   632         port = self.setup_dhcp_port(network)
   906         port = self.setup_dhcp_port(network)
   633         interface_name = self.get_interface_name(network, port)
   907         interface_name = self.get_interface_name(network, port)
   634 
   908 
   635         if net_lib.Datalink.datalink_exists(interface_name):
   909         if net_lib.Datalink.datalink_exists(interface_name):
   636             if not reuse_existing:
   910             LOG.debug(_('Reusing existing device: %s.'), interface_name)
   637                 raise exceptions.PreexistingDeviceFailure(
       
   638                     dev_name=interface_name)
       
   639 
       
   640                 LOG.debug(_('Reusing existing device: %s.'), interface_name)
       
   641         else:
   911         else:
   642             self.driver.plug(network.tenant_id, network.id,
   912             self.driver.plug(network.tenant_id, network.id,
   643                              port.id,
   913                              port.id, interface_name)
   644                              interface_name)
       
   645         ip_cidrs = []
   914         ip_cidrs = []
   646         for fixed_ip in port.fixed_ips:
   915         for fixed_ip in port.fixed_ips:
   647             subnet = fixed_ip.subnet
   916             subnet = fixed_ip.subnet
   648             net = netaddr.IPNetwork(subnet.cidr)
   917             net = netaddr.IPNetwork(subnet.cidr)
   649             ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
   918             ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
   650             ip_cidrs.append(ip_cidr)
   919             ip_cidrs.append(ip_cidr)
   651 
   920 
   652         if (self.conf.enable_isolated_metadata and self.conf.use_namespaces):
   921         if (self.conf.enable_isolated_metadata and
       
   922                 self.conf.use_namespaces):
   653             ip_cidrs.append(METADATA_DEFAULT_CIDR)
   923             ip_cidrs.append(METADATA_DEFAULT_CIDR)
   654 
   924 
   655         self.driver.init_l3(interface_name, ip_cidrs)
   925         self.driver.init_l3(interface_name, ip_cidrs)
   656 
   926 
       
   927         if self.conf.use_namespaces:
       
   928             self._set_default_route(network, interface_name)
       
   929 
   657         return interface_name
   930         return interface_name
   658 
   931 
   659     def update(self, network):
   932     def update(self, network, device_name):
   660         """Update device settings for the network's DHCP on this host."""
   933         """Update device settings for the network's DHCP on this host."""
   661         pass
   934         pass
   662 
   935 
   663     def destroy(self, network, device_name):
   936     def destroy(self, network, device_name):
   664         """Destroy the device used for the network's DHCP on this host."""
   937         """Destroy the device used for the network's DHCP on this host."""