components/openstack/neutron/files/agent/solaris/dhcp.py
branchs11-update
changeset 3028 5e73a3a3f66a
child 1872 0b81e3d9f3ae
equal deleted inserted replaced
3027:3bcf7d43558b 3028:5e73a3a3f66a
       
     1 # Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
       
     2 #
       
     3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
       
     4 #    not use this file except in compliance with the License. You may obtain
       
     5 #    a copy of the License at
       
     6 #
       
     7 #         http://www.apache.org/licenses/LICENSE-2.0
       
     8 #
       
     9 #    Unless required by applicable law or agreed to in writing, software
       
    10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
       
    11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
       
    12 #    License for the specific language governing permissions and limitations
       
    13 #    under the License.
       
    14 #
       
    15 # @author: Girish Moodalbail, Oracle, Inc.
       
    16 
       
    17 import abc
       
    18 import os
       
    19 import re
       
    20 import shutil
       
    21 import StringIO
       
    22 
       
    23 import netaddr
       
    24 
       
    25 from oslo.config import cfg
       
    26 from quantum.agent.linux import utils
       
    27 from quantum.openstack.common import log as logging
       
    28 from quantum.openstack.common import uuidutils
       
    29 
       
    30 LOG = logging.getLogger(__name__)
       
    31 
       
    32 OPTS = [
       
    33     cfg.StrOpt('dhcp_confs',
       
    34                default='$state_path/dhcp',
       
    35                help=_('Location to store DHCP server config files')),
       
    36     cfg.IntOpt('dhcp_lease_time',
       
    37                default=120,
       
    38                help=_('Lifetime of a DHCP lease in seconds')),
       
    39     cfg.StrOpt('dhcp_domain',
       
    40                default='openstacklocal',
       
    41                help=_('Domain to use for building the hostnames')),
       
    42     cfg.StrOpt('dnsmasq_config_file',
       
    43                default='',
       
    44                help=_('Override the default dnsmasq settings with this file')),
       
    45     cfg.StrOpt('dnsmasq_dns_server',
       
    46                help=_('Use another DNS server before any in '
       
    47                       '/etc/resolv.conf.')),
       
    48 ]
       
    49 
       
    50 IPV4 = 4
       
    51 IPV6 = 6
       
    52 UDP = 'udp'
       
    53 TCP = 'tcp'
       
    54 DNS_PORT = 53
       
    55 DHCPV4_PORT = 67
       
    56 DHCPV6_PORT = 467
       
    57 
       
    58 
       
    59 class DhcpBase(object):
       
    60     __metaclass__ = abc.ABCMeta
       
    61 
       
    62     def __init__(self, conf, network, root_helper='sudo',
       
    63                  device_delegate=None, namespace=None, version=None):
       
    64         self.conf = conf
       
    65         self.network = network
       
    66         self.root_helper = root_helper
       
    67         self.device_delegate = device_delegate
       
    68         self.namespace = namespace
       
    69         self.version = version
       
    70 
       
    71     @abc.abstractmethod
       
    72     def enable(self):
       
    73         """Enables DHCP for this network."""
       
    74 
       
    75     @abc.abstractmethod
       
    76     def disable(self, retain_port=False):
       
    77         """Disable dhcp for this network."""
       
    78 
       
    79     def restart(self):
       
    80         """Restart the dhcp service for the network."""
       
    81         self.disable(retain_port=True)
       
    82         self.enable()
       
    83 
       
    84     @abc.abstractproperty
       
    85     def active(self):
       
    86         """Boolean representing the running state of the DHCP server."""
       
    87 
       
    88     @abc.abstractmethod
       
    89     def reload_allocations(self):
       
    90         """Force the DHCP server to reload the assignment database."""
       
    91 
       
    92     @classmethod
       
    93     def existing_dhcp_networks(cls, conf, root_helper):
       
    94         """Return a list of existing networks ids (ones we have configs for)"""
       
    95 
       
    96         raise NotImplementedError
       
    97 
       
    98     @classmethod
       
    99     def check_version(cls):
       
   100         """Execute version checks on DHCP server."""
       
   101 
       
   102         raise NotImplementedError
       
   103 
       
   104 
       
   105 class DhcpLocalProcess(DhcpBase):
       
   106     PORTS = []
       
   107 
       
   108     def _enable_dhcp(self):
       
   109         """check if there is a subnet within the network with dhcp enabled."""
       
   110         return any(s for s in self.network.subnets if s.enable_dhcp)
       
   111 
       
   112     def enable(self):
       
   113         """Enables DHCP for this network by spawning a local process."""
       
   114         interface_name = self.device_delegate.setup(self.network,
       
   115                                                     reuse_existing=True)
       
   116         if self.active:
       
   117             self.restart()
       
   118         elif self._enable_dhcp():
       
   119             self.interface_name = interface_name
       
   120             self.spawn_process()
       
   121 
       
   122     def disable(self, retain_port=False):
       
   123         """Disable DHCP for this network by killing the local process."""
       
   124         pid = self.pid
       
   125 
       
   126         if self.active:
       
   127             cmd = ['kill', '-9', pid]
       
   128             utils.execute(cmd)
       
   129 
       
   130             if not retain_port:
       
   131                 self.device_delegate.destroy(self.network, self.interface_name)
       
   132 
       
   133         elif pid:
       
   134             LOG.debug(_('DHCP for %(net_id)s pid %(pid)d is stale, ignoring '
       
   135                         'command'), {'net_id': self.network.id, 'pid': pid})
       
   136         else:
       
   137             LOG.debug(_('No DHCP started for %s'), self.network.id)
       
   138 
       
   139         self._remove_config_files()
       
   140 
       
   141     def _remove_config_files(self):
       
   142         confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
       
   143         conf_dir = os.path.join(confs_dir, self.network.id)
       
   144         shutil.rmtree(conf_dir, ignore_errors=True)
       
   145 
       
   146     def get_conf_file_name(self, kind, ensure_conf_dir=False):
       
   147         """Returns the file name for a given kind of config file."""
       
   148         confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
       
   149         conf_dir = os.path.join(confs_dir, self.network.id)
       
   150         if ensure_conf_dir:
       
   151             if not os.path.isdir(conf_dir):
       
   152                 os.makedirs(conf_dir, 0755)
       
   153 
       
   154         return os.path.join(conf_dir, kind)
       
   155 
       
   156     def _get_value_from_conf_file(self, kind, converter=None):
       
   157         """A helper function to read a value from one of the state files."""
       
   158         file_name = self.get_conf_file_name(kind)
       
   159         msg = _('Error while reading %s')
       
   160 
       
   161         try:
       
   162             with open(file_name, 'r') as f:
       
   163                 try:
       
   164                     return converter and converter(f.read()) or f.read()
       
   165                 except ValueError, e:
       
   166                     msg = _('Unable to convert value in %s')
       
   167         except IOError, e:
       
   168             msg = _('Unable to access %s')
       
   169 
       
   170         LOG.debug(msg % file_name)
       
   171         return None
       
   172 
       
   173     @property
       
   174     def pid(self):
       
   175         """Last known pid for the DHCP process spawned for this network."""
       
   176         return self._get_value_from_conf_file('pid', int)
       
   177 
       
   178     @property
       
   179     def active(self):
       
   180         pid = self.pid
       
   181         if pid is None:
       
   182             return False
       
   183 
       
   184         cmd = ['pargs', pid]
       
   185         try:
       
   186             return self.network.id in utils.execute(cmd)
       
   187         except RuntimeError:
       
   188             return False
       
   189 
       
   190     @property
       
   191     def interface_name(self):
       
   192         return self._get_value_from_conf_file('interface')
       
   193 
       
   194     @interface_name.setter
       
   195     def interface_name(self, value):
       
   196         interface_file_path = self.get_conf_file_name('interface',
       
   197                                                       ensure_conf_dir=True)
       
   198         utils.replace_file(interface_file_path, value)
       
   199 
       
   200     @abc.abstractmethod
       
   201     def spawn_process(self):
       
   202         pass
       
   203 
       
   204 
       
   205 class Dnsmasq(DhcpLocalProcess):
       
   206     # The ports that need to be opened when security policies are active
       
   207     # on the Quantum port used for DHCP.  These are provided as a convenience
       
   208     # for users of this class.
       
   209     PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)],
       
   210              IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)]}
       
   211 
       
   212     _TAG_PREFIX = 'tag%d'
       
   213 
       
   214     QUANTUM_NETWORK_ID_KEY = 'QUANTUM_NETWORK_ID'
       
   215     QUANTUM_RELAY_SOCKET_PATH_KEY = 'QUANTUM_RELAY_SOCKET_PATH'
       
   216 
       
   217     @classmethod
       
   218     def check_version(cls):
       
   219         # For Solaris, we rely on the packaging system to ensure a
       
   220         # matching/supported version of dnsmasq
       
   221         pass
       
   222 
       
   223     @classmethod
       
   224     def existing_dhcp_networks(cls, conf, root_helper):
       
   225         """Return a list of existing networks ids (ones we have configs for)"""
       
   226 
       
   227         confs_dir = os.path.abspath(os.path.normpath(conf.dhcp_confs))
       
   228 
       
   229         class FakeNetwork:
       
   230             def __init__(self, net_id):
       
   231                 self.id = net_id
       
   232 
       
   233         return [
       
   234             c for c in os.listdir(confs_dir)
       
   235             if (uuidutils.is_uuid_like(c) and
       
   236                 cls(conf, FakeNetwork(c), root_helper).active)
       
   237         ]
       
   238 
       
   239     def spawn_process(self):
       
   240         """Spawns a Dnsmasq process for the network."""
       
   241         env = {
       
   242             self.QUANTUM_NETWORK_ID_KEY: self.network.id
       
   243         }
       
   244 
       
   245         cmd = [
       
   246             '/usr/lib/inet/dnsmasq',
       
   247             '--no-hosts',
       
   248             '--no-resolv',
       
   249             '--strict-order',
       
   250             '--bind-interfaces',
       
   251             '--interface=%s' % self.interface_name,
       
   252             '--except-interface=lo0',
       
   253             '--pid-file=%s' % self.get_conf_file_name(
       
   254                 'pid', ensure_conf_dir=True),
       
   255             #TODO(gmoodalb): calculate value from cidr (defaults to 150)
       
   256             #'--dhcp-lease-max=%s' % ?,
       
   257             '--dhcp-hostsfile=%s' % self._output_hosts_file(),
       
   258             '--dhcp-optsfile=%s' % self._output_opts_file(),
       
   259             #'--dhcp-script=%s' % self._lease_relay_script_path(),
       
   260             '--leasefile-ro',
       
   261         ]
       
   262 
       
   263         for i, subnet in enumerate(self.network.subnets):
       
   264             # if a subnet is specified to have dhcp disabled
       
   265             if not subnet.enable_dhcp:
       
   266                 continue
       
   267             if subnet.ip_version == 4:
       
   268                 mode = 'static'
       
   269             else:
       
   270                 #TODO(gmoodalb): how do we indicate other options
       
   271                 #ra-only, slaac, ra-nameservers, and ra-stateless.
       
   272                 mode = 'static'
       
   273             cmd.append('--dhcp-range=set:%s,%s,%s,%ss' %
       
   274                        (self._TAG_PREFIX % i,
       
   275                         netaddr.IPNetwork(subnet.cidr).network,
       
   276                         mode, self.conf.dhcp_lease_time))
       
   277 
       
   278         cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file)
       
   279         if self.conf.dnsmasq_dns_server:
       
   280             cmd.append('--server=%s' % self.conf.dnsmasq_dns_server)
       
   281 
       
   282         if self.conf.dhcp_domain:
       
   283             cmd.append('--domain=%s' % self.conf.dhcp_domain)
       
   284         utils.execute(cmd)
       
   285 
       
   286     def reload_allocations(self):
       
   287         """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
       
   288 
       
   289         # If all subnets turn off dhcp, kill the process.
       
   290         if not self._enable_dhcp():
       
   291             self.disable()
       
   292             LOG.debug(_('Killing dhcpmasq for network since all subnets have '
       
   293                         'turned off DHCP: %s'), self.network.id)
       
   294             return
       
   295 
       
   296         self._output_hosts_file()
       
   297         self._output_opts_file()
       
   298 
       
   299         if self.active:
       
   300             cmd = ['kill', '-HUP', self.pid]
       
   301             utils.execute(cmd)
       
   302         else:
       
   303             LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), self.pid)
       
   304         LOG.debug(_('Reloading allocations for network: %s'), self.network.id)
       
   305 
       
   306     def _output_hosts_file(self):
       
   307         """Writes a dnsmasq compatible hosts file."""
       
   308         r = re.compile('[:.]')
       
   309         buf = StringIO.StringIO()
       
   310 
       
   311         for port in self.network.ports:
       
   312             for alloc in port.fixed_ips:
       
   313                 name = 'host-%s.%s' % (r.sub('-', alloc.ip_address),
       
   314                                        self.conf.dhcp_domain)
       
   315                 buf.write('%s,%s,%s\n' %
       
   316                           (port.mac_address, name, alloc.ip_address))
       
   317 
       
   318         name = self.get_conf_file_name('host')
       
   319         utils.replace_file(name, buf.getvalue())
       
   320         return name
       
   321 
       
   322     def _output_opts_file(self):
       
   323         """Write a dnsmasq compatible options file."""
       
   324 
       
   325         options = []
       
   326         for i, subnet in enumerate(self.network.subnets):
       
   327             if not subnet.enable_dhcp:
       
   328                 continue
       
   329             if subnet.dns_nameservers:
       
   330                 options.append(
       
   331                     self._format_option(i, 'dns-server',
       
   332                                         ','.join(subnet.dns_nameservers)))
       
   333 
       
   334             host_routes = ["%s,%s" % (hr.destination, hr.nexthop)
       
   335                            for hr in subnet.host_routes]
       
   336 
       
   337             if host_routes:
       
   338                 options.append(
       
   339                     self._format_option(i, 'classless-static-route',
       
   340                                         ','.join(host_routes)))
       
   341 
       
   342             if subnet.ip_version == 4:
       
   343                 if subnet.gateway_ip:
       
   344                     options.append(self._format_option(i, 'router',
       
   345                                                        subnet.gateway_ip))
       
   346                 else:
       
   347                     options.append(self._format_option(i, 'router'))
       
   348 
       
   349         name = self.get_conf_file_name('opts')
       
   350         utils.replace_file(name, '\n'.join(options))
       
   351         return name
       
   352 
       
   353     def _format_option(self, index, option_name, *args):
       
   354         return ','.join(('tag:' + self._TAG_PREFIX % index,
       
   355                          'option:%s' % option_name) + args)
       
   356 
       
   357     @classmethod
       
   358     def lease_update(cls):
       
   359         pass