components/openstack/neutron/files/agent/solaris/dhcp.py
branchs11u3-sru
changeset 6035 c9748fcc32de
parent 4072 db0cec748ec0
child 6444 bf62eba2612a
equal deleted inserted replaced
6016:a477397bba8b 6035:c9748fcc32de
    14 #    Unless required by applicable law or agreed to in writing, software
    14 #    Unless required by applicable law or agreed to in writing, software
    15 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    15 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    16 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    16 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    17 #    License for the specific language governing permissions and limitations
    17 #    License for the specific language governing permissions and limitations
    18 #    under the License.
    18 #    under the License.
    19 #
       
    20 # @author: Girish Moodalbail, Oracle, Inc.
       
    21 
    19 
    22 import abc
    20 import abc
    23 import collections
       
    24 import os
       
    25 import re
       
    26 import shutil
       
    27 import socket
       
    28 import sys
       
    29 import uuid
       
    30 
       
    31 import netaddr
    21 import netaddr
       
    22 
    32 from oslo.config import cfg
    23 from oslo.config import cfg
    33 import six
    24 from oslo_log import log as logging
    34 
    25 
    35 from neutron.agent.linux import utils
    26 from neutron.agent.linux import utils
       
    27 from neutron.agent.linux import dhcp
    36 from neutron.agent.solaris import net_lib
    28 from neutron.agent.solaris import net_lib
    37 from neutron.common import constants
    29 from neutron.common import constants
    38 from neutron.common import exceptions
    30 from neutron.common import exceptions
    39 from neutron.openstack.common import importutils
    31 from neutron.common import ipv6_utils
    40 from neutron.openstack.common import jsonutils
    32 
    41 from neutron.openstack.common import log as logging
       
    42 from neutron.openstack.common import uuidutils
       
    43 
    33 
    44 LOG = logging.getLogger(__name__)
    34 LOG = logging.getLogger(__name__)
    45 
    35 
    46 OPTS = [
    36 
    47     cfg.StrOpt('dhcp_confs',
    37 class Dnsmasq(dhcp.Dnsmasq):
    48                default='$state_path/dhcp',
    38     """ Wrapper around Linux implementation of Dnsmasq."""
    49                help=_('Location to store DHCP server config files')),
    39 
    50     cfg.StrOpt('dhcp_domain',
    40     def __init__(self, conf, network, process_monitor, version=None,
    51                default='openstacklocal',
    41                  plugin=None):
    52                help=_('Domain to use for building the hostnames')),
    42         super(Dnsmasq, self).__init__(conf, network, process_monitor,
    53     cfg.StrOpt('dnsmasq_config_file',
    43                                       version, plugin)
    54                default='',
    44         self.device_manager = DeviceManager(self.conf, plugin)
    55                help=_('Override the default dnsmasq settings with this file')),
    45 
    56     cfg.ListOpt('dnsmasq_dns_servers',
    46     def _build_cmdline_callback(self, pid_file):
    57                 help=_('Comma-separated list of the DNS servers which will be '
       
    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.")),
       
    62     cfg.IntOpt(
       
    63         'dnsmasq_lease_max',
       
    64         default=(2 ** 24),
       
    65         help=_('Limit number of leases to prevent a denial-of-service.')),
       
    66 ]
       
    67 
       
    68 IPV4 = 4
       
    69 IPV6 = 6
       
    70 UDP = 'udp'
       
    71 TCP = 'tcp'
       
    72 DNS_PORT = 53
       
    73 DHCPV4_PORT = 67
       
    74 DHCPV6_PORT = 547
       
    75 METADATA_DEFAULT_PREFIX = 16
       
    76 METADATA_DEFAULT_IP = '169.254.169.254'
       
    77 METADATA_DEFAULT_CIDR = '%s/%d' % (METADATA_DEFAULT_IP,
       
    78                                    METADATA_DEFAULT_PREFIX)
       
    79 METADATA_PORT = 80
       
    80 WIN2k3_STATIC_DNS = 249
       
    81 
       
    82 
       
    83 class DictModel(dict):
       
    84     """Convert dict into an object that provides attribute access to values."""
       
    85 
       
    86     def __init__(self, *args, **kwargs):
       
    87         """Convert dict values to DictModel values."""
       
    88         super(DictModel, self).__init__(*args, **kwargs)
       
    89 
       
    90         def needs_upgrade(item):
       
    91             """Check if `item` is a dict and needs to be changed to DictModel.
       
    92             """
       
    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]
       
   123 
       
   124 
       
   125 class NetModel(DictModel):
       
   126 
       
   127     def __init__(self, use_namespaces, d):
       
   128         super(NetModel, self).__init__(d)
       
   129 
       
   130         self._ns_name = None
       
   131 
       
   132     @property
       
   133     def namespace(self):
       
   134         return self._ns_name
       
   135 
       
   136 
       
   137 @six.add_metaclass(abc.ABCMeta)
       
   138 class DhcpBase(object):
       
   139 
       
   140     def __init__(self, conf, network, root_helper='sudo',
       
   141                  version=None, plugin=None):
       
   142         self.conf = conf
       
   143         self.network = network
       
   144         self.root_helper = None
       
   145         self.device_manager = DeviceManager(self.conf,
       
   146                                             self.root_helper, plugin)
       
   147         self.version = version
       
   148 
       
   149     @abc.abstractmethod
       
   150     def enable(self):
       
   151         """Enables DHCP for this network."""
       
   152 
       
   153     @abc.abstractmethod
       
   154     def disable(self, retain_port=False):
       
   155         """Disable dhcp for this network."""
       
   156 
       
   157     def restart(self):
       
   158         """Restart the dhcp service for the network."""
       
   159         self.disable(retain_port=True)
       
   160         self.enable()
       
   161 
       
   162     @abc.abstractproperty
       
   163     def active(self):
       
   164         """Boolean representing the running state of the DHCP server."""
       
   165 
       
   166     @abc.abstractmethod
       
   167     def reload_allocations(self):
       
   168         """Force the DHCP server to reload the assignment database."""
       
   169 
       
   170     @classmethod
       
   171     def existing_dhcp_networks(cls, conf, root_helper):
       
   172         """Return a list of existing networks ids that we have configs for."""
       
   173 
       
   174         raise NotImplementedError()
       
   175 
       
   176     @classmethod
       
   177     def check_version(cls):
       
   178         """Execute version checks on DHCP server."""
       
   179 
       
   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()
       
   191 
       
   192 
       
   193 class DhcpLocalProcess(DhcpBase):
       
   194     PORTS = []
       
   195 
       
   196     def _enable_dhcp(self):
       
   197         """check if there is a subnet within the network with dhcp enabled."""
       
   198         for subnet in self.network.subnets:
       
   199             if subnet.enable_dhcp:
       
   200                 return True
       
   201         return False
       
   202 
       
   203     def enable(self):
       
   204         """Enables DHCP for this network by spawning a local process."""
       
   205         if self.active:
       
   206             self.restart()
       
   207         elif self._enable_dhcp():
       
   208             interface_name = self.device_manager.setup(self.network)
       
   209             self.interface_name = interface_name
       
   210             self.spawn_process()
       
   211 
       
   212     def disable(self, retain_port=False):
       
   213         """Disable DHCP for this network by killing the local process."""
       
   214         pid = self.pid
       
   215 
       
   216         if pid:
       
   217             if self.active:
       
   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})
       
   224             if not retain_port:
       
   225                 self.device_manager.destroy(self.network,
       
   226                                             self.interface_name)
       
   227         else:
       
   228             LOG.debug(_('No DHCP started for %s'), self.network.id)
       
   229 
       
   230         self._remove_config_files()
       
   231 
       
   232     def _remove_config_files(self):
       
   233         confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
       
   234         conf_dir = os.path.join(confs_dir, self.network.id)
       
   235         shutil.rmtree(conf_dir, ignore_errors=True)
       
   236 
       
   237     def get_conf_file_name(self, kind, ensure_conf_dir=False):
       
   238         """Returns the file name for a given kind of config file."""
       
   239         confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
       
   240         conf_dir = os.path.join(confs_dir, self.network.id)
       
   241         if ensure_conf_dir:
       
   242             if not os.path.isdir(conf_dir):
       
   243                 os.makedirs(conf_dir, 0o755)
       
   244 
       
   245         return os.path.join(conf_dir, kind)
       
   246 
       
   247     def _get_value_from_conf_file(self, kind, converter=None):
       
   248         """A helper function to read a value from one of the state files."""
       
   249         file_name = self.get_conf_file_name(kind)
       
   250         msg = _('Error while reading %s')
       
   251 
       
   252         try:
       
   253             with open(file_name, 'r') as f:
       
   254                 try:
       
   255                     return converter and converter(f.read()) or f.read()
       
   256                 except ValueError:
       
   257                     msg = _('Unable to convert value in %s')
       
   258         except IOError:
       
   259             msg = _('Unable to access %s')
       
   260 
       
   261         LOG.debug(msg % file_name)
       
   262         return None
       
   263 
       
   264     @property
       
   265     def pid(self):
       
   266         """Last known pid for the DHCP process spawned for this network."""
       
   267         return self._get_value_from_conf_file('pid', int)
       
   268 
       
   269     @property
       
   270     def active(self):
       
   271         pid = self.pid
       
   272         if pid is None:
       
   273             return False
       
   274 
       
   275         cmd = ['/usr/bin/pargs', pid]
       
   276         try:
       
   277             return self.network.id in utils.execute(cmd)
       
   278         except RuntimeError:
       
   279             return False
       
   280 
       
   281     @property
       
   282     def interface_name(self):
       
   283         return self._get_value_from_conf_file('interface')
       
   284 
       
   285     @interface_name.setter
       
   286     def interface_name(self, value):
       
   287         interface_file_path = self.get_conf_file_name('interface',
       
   288                                                       ensure_conf_dir=True)
       
   289         utils.replace_file(interface_file_path, value)
       
   290 
       
   291     @abc.abstractmethod
       
   292     def spawn_process(self):
       
   293         pass
       
   294 
       
   295 
       
   296 class Dnsmasq(DhcpLocalProcess):
       
   297     # The ports that need to be opened when security policies are active
       
   298     # on the Neutron port used for DHCP.  These are provided as a convenience
       
   299     # for users of this class.
       
   300     PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)],
       
   301              IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)],
       
   302              }
       
   303 
       
   304     _TAG_PREFIX = 'tag%d'
       
   305 
       
   306     NEUTRON_NETWORK_ID_KEY = 'NEUTRON_NETWORK_ID'
       
   307     NEUTRON_RELAY_SOCKET_PATH_KEY = 'NEUTRON_RELAY_SOCKET_PATH'
       
   308     MINIMUM_VERSION = 2.63
       
   309     MINIMUM_IPV6_VERSION = 2.67
       
   310 
       
   311     @classmethod
       
   312     def check_version(cls):
       
   313         ver = 0
       
   314         try:
       
   315             cmd = ['/usr/lib/inet/dnsmasq', '--version']
       
   316             out = utils.execute(cmd)
       
   317             ver = re.findall("\d+.\d+", out)[0]
       
   318             is_valid_version = float(ver) >= cls.MINIMUM_VERSION
       
   319             # For Solaris, we rely on the packaging system to ensure a
       
   320             # matching/supported version of dnsmasq.
       
   321             if not is_valid_version:
       
   322                 LOG.warning(_('FAILED VERSION REQUIREMENT FOR DNSMASQ. '
       
   323                               'DHCP AGENT MAY NOT RUN CORRECTLY! '
       
   324                               'Please ensure that its version is %s '
       
   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)
       
   334         except (OSError, RuntimeError, IndexError, ValueError):
       
   335             LOG.warning(_('Unable to determine dnsmasq version. '
       
   336                           'Please ensure that its version is %s '
       
   337                           'or above!'), cls.MINIMUM_VERSION)
       
   338             raise SystemExit(1)
       
   339         return float(ver)
       
   340 
       
   341     @classmethod
       
   342     def existing_dhcp_networks(cls, conf, root_helper):
       
   343         """Return a list of existing networks ids that we have configs for."""
       
   344 
       
   345         confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs))
       
   346 
       
   347         return [
       
   348             c for c in os.listdir(confs_dir)
       
   349             if uuidutils.is_uuid_like(c)
       
   350         ]
       
   351 
       
   352     def spawn_process(self):
       
   353         """Spawns a Dnsmasq process for the network."""
       
   354         env = {
       
   355             self.NEUTRON_NETWORK_ID_KEY: self.network.id,
       
   356         }
       
   357 
       
   358         cmd = [
    47         cmd = [
   359             '/usr/lib/inet/dnsmasq',
    48             '/usr/lib/inet/dnsmasq',
   360             '--no-hosts',
    49             '--no-hosts',
   361             '--no-resolv',
    50             '--no-resolv',
   362             '--strict-order',
    51             '--strict-order',
   363             '--bind-interfaces',
    52             '--bind-interfaces',
   364             '--interface=%s' % self.interface_name,
    53             '--interface=%s' % self.interface_name,
   365             '--except-interface=lo0',
    54             '--except-interface=lo0',
   366             '--pid-file=%s' % self.get_conf_file_name(
    55             '--pid-file=%s' % pid_file,
   367                 'pid', ensure_conf_dir=True),
    56             '--dhcp-hostsfile=%s' % self.get_conf_file_name('host'),
   368             '--dhcp-hostsfile=%s' % self._output_hosts_file(),
    57             '--addn-hosts=%s' % self.get_conf_file_name('addn_hosts'),
   369             '--addn-hosts=%s' % self._output_addn_hosts_file(),
    58             '--dhcp-optsfile=%s' % self.get_conf_file_name('opts'),
   370             '--dhcp-optsfile=%s' % self._output_opts_file(),
       
   371             '--leasefile-ro',
    59             '--leasefile-ro',
       
    60             '--dhcp-authoritative'
   372         ]
    61         ]
   373 
    62 
   374         possible_leases = 0
    63         possible_leases = 0
   375         for i, subnet in enumerate(self.network.subnets):
    64         for i, subnet in enumerate(self.network.subnets):
   376             mode = None
    65             mode = None
   419                                ('set:', self._TAG_PREFIX % i,
   108                                ('set:', self._TAG_PREFIX % i,
   420                                 cidr.network, mode,
   109                                 cidr.network, mode,
   421                                 cidr.prefixlen, lease))
   110                                 cidr.prefixlen, lease))
   422                 possible_leases += cidr.size
   111                 possible_leases += cidr.size
   423 
   112 
       
   113         if cfg.CONF.advertise_mtu:
       
   114             mtu = self.network.mtu
       
   115             # Do not advertise unknown mtu
       
   116             if mtu > 0:
       
   117                 cmd.append('--dhcp-option-force=option:mtu,%d' % mtu)
       
   118 
   424         # Cap the limit because creating lots of subnets can inflate
   119         # Cap the limit because creating lots of subnets can inflate
   425         # this possible lease cap.
   120         # this possible lease cap.
   426         cmd.append('--dhcp-lease-max=%d' %
   121         cmd.append('--dhcp-lease-max=%d' %
   427                    min(possible_leases, self.conf.dnsmasq_lease_max))
   122                    min(possible_leases, self.conf.dnsmasq_lease_max))
   428 
   123 
   433                 for server in self.conf.dnsmasq_dns_servers)
   128                 for server in self.conf.dnsmasq_dns_servers)
   434 
   129 
   435         if self.conf.dhcp_domain:
   130         if self.conf.dhcp_domain:
   436             cmd.append('--domain=%s' % self.conf.dhcp_domain)
   131             cmd.append('--domain=%s' % self.conf.dhcp_domain)
   437 
   132 
   438         # TODO(gmoodalb): prepend the env vars before command
   133         if self.conf.dhcp_broadcast_reply:
   439         utils.execute(cmd, self.root_helper)
   134             cmd.append('--dhcp-broadcast')
       
   135 
       
   136         return cmd
   440 
   137 
   441     def _release_lease(self, mac_address, ip):
   138     def _release_lease(self, mac_address, ip):
   442         """Release a DHCP lease."""
   139         """Release a DHCP lease."""
   443         cmd = ['/usr/lib/inet/dhcp_release', self.interface_name,
   140         cmd = ['/usr/lib/inet/dhcp_release', self.interface_name,
   444                ip, mac_address]
   141                ip, mac_address]
   445         utils.execute(cmd, self.root_helper)
   142         utils.execute(cmd)
   446 
       
   447     def reload_allocations(self):
       
   448         """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
       
   449 
       
   450         # If all subnets turn off dhcp, kill the process.
       
   451         if not self._enable_dhcp():
       
   452             self.disable()
       
   453             LOG.debug(_('Killing dhcpmasq for network since all subnets have '
       
   454                         'turned off DHCP: %s'), self.network.id)
       
   455             return
       
   456 
       
   457         self._release_unused_leases()
       
   458         self._output_hosts_file()
       
   459         self._output_addn_hosts_file()
       
   460         self._output_opts_file()
       
   461         if self.active:
       
   462             cmd = ['kill', '-HUP', self.pid]
       
   463             utils.execute(cmd, self.root_helper)
       
   464         else:
       
   465             LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid)
       
   466         LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
       
   467         self.device_manager.update(self.network, self.interface_name)
       
   468 
       
   469     def _iter_hosts(self):
       
   470         """Iterate over hosts.
       
   471 
       
   472         For each host on the network we yield a tuple containing:
       
   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)
       
   482         for port in self.network.ports:
       
   483             for alloc in port.fixed_ips:
       
   484                 # Note(scollins) Only create entries that are
       
   485                 # associated with the subnet being managed by this
       
   486                 # dhcp agent
       
   487                 if alloc.subnet_id in v6_nets:
       
   488                     addr_mode = v6_nets[alloc.subnet_id].ipv6_address_mode
       
   489                     if addr_mode != constants.DHCPV6_STATEFUL:
       
   490                         continue
       
   491                 hostname = 'host-%s' % alloc.ip_address.replace(
       
   492                     '.', '-').replace(':', '-')
       
   493                 fqdn = hostname
       
   494                 if self.conf.dhcp_domain:
       
   495                     fqdn = '%s.%s' % (fqdn, self.conf.dhcp_domain)
       
   496                 yield (port, alloc, hostname, fqdn)
       
   497 
       
   498     def _output_hosts_file(self):
       
   499         """Writes a dnsmasq compatible dhcp hosts file.
       
   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
       
   581 
       
   582     def _output_opts_file(self):
       
   583         """Write a dnsmasq compatible options file."""
       
   584 
       
   585         if self.conf.enable_isolated_metadata:
       
   586             subnet_to_interface_ip = self._make_subnet_interface_ip_map()
       
   587 
       
   588         options = []
       
   589 
       
   590         isolated_subnets = self.get_isolated_subnets(self.network)
       
   591         dhcp_ips = collections.defaultdict(list)
       
   592         subnet_idx_map = {}
       
   593         for i, subnet in enumerate(self.network.subnets):
       
   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])):
       
   598                 continue
       
   599             if subnet.dns_nameservers:
       
   600                 options.append(
       
   601                     self._format_option(
       
   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)))
       
   614 
       
   615             gateway = subnet.gateway_ip
       
   616             host_routes = []
       
   617             for hr in subnet.host_routes:
       
   618                 if hr.destination == "0.0.0.0/0":
       
   619                     if not gateway:
       
   620                         gateway = hr.nexthop
       
   621                 else:
       
   622                     host_routes.append("%s,%s" % (hr.destination, hr.nexthop))
       
   623 
       
   624             # Add host routes for isolated network segments
       
   625 
       
   626             if (isolated_subnets[subnet.id] and
       
   627                     self.conf.enable_isolated_metadata and
       
   628                     subnet.ip_version == 4):
       
   629                 subnet_dhcp_ip = subnet_to_interface_ip[subnet.id]
       
   630                 host_routes.append(
       
   631                     '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)
       
   632                 )
       
   633 
       
   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 
       
   647                 if gateway:
       
   648                     options.append(self._format_option(subnet.ip_version,
       
   649                                                        i, 'router',
       
   650                                                        gateway))
       
   651                 else:
       
   652                     options.append(self._format_option(subnet.ip_version,
       
   653                                                        i, 'router'))
       
   654 
       
   655         for port in self.network.ports:
       
   656             if getattr(port, 'extra_dhcp_opts', False):
       
   657                 for ip_version in (4, 6):
       
   658                     if any(
       
   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))))
       
   692 
       
   693         name = self.get_conf_file_name('opts')
       
   694         utils.replace_file(name, '\n'.join(options))
       
   695         return name
       
   696 
   143 
   697     def _make_subnet_interface_ip_map(self):
   144     def _make_subnet_interface_ip_map(self):
   698         # TODO(gmoodalb): need to complete this when we support metadata
   145         # TODO(gmoodalb): need to complete this when we support metadata
       
   146         # in neutron-dhcp-agent as-well for isolated subnets
   699         pass
   147         pass
   700 
       
   701     def _format_option(self, ip_version, tag, option, *args):
       
   702         """Format DHCP option by option name or code."""
       
   703         option = str(option)
       
   704 
       
   705         if isinstance(tag, int):
       
   706             tag = self._TAG_PREFIX % tag
       
   707 
       
   708         if not option.isdigit():
       
   709             if ip_version == 4:
       
   710                 option = 'option:%s' % option
       
   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 
   148 
   742     @classmethod
   149     @classmethod
   743     def should_enable_metadata(cls, conf, network):
   150     def should_enable_metadata(cls, conf, network):
   744         """True if there exists a subnet for which a metadata proxy is needed
   151         # TODO(gmoodalb): need to complete this when we support metadata
   745         """
   152         # in neutron-dhcp-agent as-well for isolated subnets
   746         return False
   153         return False
   747 
   154 
   748     @classmethod
   155 
   749     def lease_update(cls):
   156 class DeviceManager(dhcp.DeviceManager):
   750         network_id = os.environ.get(cls.NEUTRON_NETWORK_ID_KEY)
   157 
   751         dhcp_relay_socket = os.environ.get(cls.NEUTRON_RELAY_SOCKET_PATH_KEY)
   158     def __init__(self, conf, plugin):
   752 
   159         super(DeviceManager, self).__init__(conf, plugin)
   753         action = sys.argv[1]
       
   754         if action not in ('add', 'del', 'old'):
       
   755             sys.exit()
       
   756 
       
   757         mac_address = sys.argv[2]
       
   758         ip_address = sys.argv[3]
       
   759 
       
   760         if action == 'del':
       
   761             lease_remaining = 0
       
   762         else:
       
   763             lease_remaining = int(os.environ.get('DNSMASQ_TIME_REMAINING', 0))
       
   764 
       
   765         data = dict(network_id=network_id, mac_address=mac_address,
       
   766                     ip_address=ip_address, lease_remaining=lease_remaining)
       
   767 
       
   768         if os.path.exists(dhcp_relay_socket):
       
   769             sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
       
   770             sock.connect(dhcp_relay_socket)
       
   771             sock.send(jsonutils.dumps(data))
       
   772             sock.close()
       
   773 
       
   774 
       
   775 class DeviceManager(object):
       
   776 
       
   777     def __init__(self, conf, root_helper, plugin):
       
   778         self.conf = conf
       
   779         self.root_helper = root_helper
       
   780         self.plugin = plugin
       
   781         if not conf.interface_driver:
       
   782             msg = _('An interface driver must be specified')
       
   783             LOG.error(msg)
       
   784             raise SystemExit(1)
       
   785         try:
       
   786             self.driver = importutils.import_object(
       
   787                 conf.interface_driver, conf)
       
   788         except Exception as e:
       
   789             msg = (_("Error importing interface driver '%(driver)s': "
       
   790                    "%(inner)s") % {'driver': conf.interface_driver,
       
   791                                    'inner': e})
       
   792             LOG.error(msg)
       
   793             raise SystemExit(1)
       
   794 
       
   795     def get_interface_name(self, network, port):
       
   796         """Return interface(device) name for use by the DHCP process."""
       
   797         return self.driver.get_device_name(port)
       
   798 
       
   799     def get_device_id(self, network):
       
   800         """Return a unique DHCP device ID for this host on the network."""
       
   801         # There could be more than one dhcp server per network, so create
       
   802         # a device id that combines host and network ids
       
   803 
       
   804         host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
       
   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
       
   816 
   160 
   817     def setup_dhcp_port(self, network):
   161     def setup_dhcp_port(self, network):
   818         """Create/update DHCP port for the host if needed and return port."""
   162         """Create/update DHCP port for the host if needed and return port."""
   819 
   163 
   820         device_id = self.get_device_id(network)
   164         device_id = self.get_device_id(network)
   858                 # break since we found port that matches device_id
   202                 # break since we found port that matches device_id
   859                 break
   203                 break
   860 
   204 
   861         # check for a reserved DHCP port
   205         # check for a reserved DHCP port
   862         if dhcp_port is None:
   206         if dhcp_port is None:
   863             LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
   207             LOG.debug('DHCP port %(device_id)s on network %(network_id)s'
   864                         ' does not yet exist. Checking for a reserved port.'),
   208                       ' does not yet exist. Checking for a reserved port.',
   865                       {'device_id': device_id, 'network_id': network.id})
   209                       {'device_id': device_id, 'network_id': network.id})
   866             for port in network.ports:
   210             for port in network.ports:
   867                 port_device_id = getattr(port, 'device_id', None)
   211                 port_device_id = getattr(port, 'device_id', None)
   868                 if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT:
   212                 if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT:
   869                     dhcp_port = self.plugin.update_dhcp_port(
   213                     dhcp_port = self.plugin.update_dhcp_port(
   872                     if dhcp_port:
   216                     if dhcp_port:
   873                         break
   217                         break
   874 
   218 
   875         # DHCP port has not yet been created.
   219         # DHCP port has not yet been created.
   876         if dhcp_port is None:
   220         if dhcp_port is None:
   877             LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
   221             LOG.debug('DHCP port %(device_id)s on network %(network_id)s'
   878                         ' does not yet exist.'), {'device_id': device_id,
   222                       ' does not yet exist.', {'device_id': device_id,
   879                                                   'network_id': network.id})
   223                                                'network_id': network.id})
   880             port_dict = dict(
   224             port_dict = dict(
   881                 name='',
   225                 name='',
   882                 admin_state_up=True,
   226                 admin_state_up=True,
   883                 device_id=device_id,
   227                 device_id=device_id,
   884                 network_id=network.id,
   228                 network_id=network.id,
   893         fixed_ips = [dict(subnet_id=fixed_ip.subnet_id,
   237         fixed_ips = [dict(subnet_id=fixed_ip.subnet_id,
   894                           ip_address=fixed_ip.ip_address,
   238                           ip_address=fixed_ip.ip_address,
   895                           subnet=subnets[fixed_ip.subnet_id])
   239                           subnet=subnets[fixed_ip.subnet_id])
   896                      for fixed_ip in dhcp_port.fixed_ips]
   240                      for fixed_ip in dhcp_port.fixed_ips]
   897 
   241 
   898         ips = [DictModel(item) if isinstance(item, dict) else item
   242         ips = [dhcp.DictModel(item) if isinstance(item, dict) else item
   899                for item in fixed_ips]
   243                for item in fixed_ips]
   900         dhcp_port.fixed_ips = ips
   244         dhcp_port.fixed_ips = ips
   901 
   245 
   902         return dhcp_port
   246         return dhcp_port
   903 
   247 
   905         """Create and initialize a device for network's DHCP on this host."""
   249         """Create and initialize a device for network's DHCP on this host."""
   906         port = self.setup_dhcp_port(network)
   250         port = self.setup_dhcp_port(network)
   907         interface_name = self.get_interface_name(network, port)
   251         interface_name = self.get_interface_name(network, port)
   908 
   252 
   909         if net_lib.Datalink.datalink_exists(interface_name):
   253         if net_lib.Datalink.datalink_exists(interface_name):
   910             LOG.debug(_('Reusing existing device: %s.'), interface_name)
   254             LOG.debug('Reusing existing device: %s.', interface_name)
   911         else:
   255         else:
   912             self.driver.plug(network.tenant_id, network.id,
   256             self.driver.plug(network.tenant_id, network.id,
   913                              port.id, interface_name)
   257                              port.id, interface_name)
   914         ip_cidrs = []
   258         ip_cidrs = []
       
   259         addrconf = False
   915         for fixed_ip in port.fixed_ips:
   260         for fixed_ip in port.fixed_ips:
   916             subnet = fixed_ip.subnet
   261             subnet = fixed_ip.subnet
   917             net = netaddr.IPNetwork(subnet.cidr)
   262             if not ipv6_utils.is_auto_address_subnet(subnet):
   918             ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
   263                 net = netaddr.IPNetwork(subnet.cidr)
   919             ip_cidrs.append(ip_cidr)
   264                 ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
   920 
   265                 ip_cidrs.append(ip_cidr)
   921         if (self.conf.enable_isolated_metadata and
   266             else:
   922                 self.conf.use_namespaces):
   267                 addrconf = True
   923             ip_cidrs.append(METADATA_DEFAULT_CIDR)
   268 
   924 
   269             self.driver.init_l3(interface_name, ip_cidrs, addrconf=addrconf)
   925         self.driver.init_l3(interface_name, ip_cidrs)
       
   926 
       
   927         if self.conf.use_namespaces:
       
   928             self._set_default_route(network, interface_name)
       
   929 
   270 
   930         return interface_name
   271         return interface_name
   931 
       
   932     def update(self, network, device_name):
       
   933         """Update device settings for the network's DHCP on this host."""
       
   934         pass
       
   935 
   272 
   936     def destroy(self, network, device_name):
   273     def destroy(self, network, device_name):
   937         """Destroy the device used for the network's DHCP on this host."""
   274         """Destroy the device used for the network's DHCP on this host."""
   938 
   275 
   939         self.driver.fini_l3(device_name)
   276         self.driver.fini_l3(device_name)