Home | History | Annotate | Download | only in ap_lib
      1 #   Copyright 2016 - The Android Open Source Project
      2 #
      3 #   Licensed under the Apache License, Version 2.0 (the "License");
      4 #   you may not use this file except in compliance with the License.
      5 #   You may obtain 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,
     11 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 #   See the License for the specific language governing permissions and
     13 #   limitations under the License.
     14 
     15 import time
     16 from acts.controllers.utils_lib.commands import shell
     17 
     18 _ROUTER_DNS = '8.8.8.8, 4.4.4.4'
     19 
     20 
     21 class Error(Exception):
     22     """An error caused by the dhcp server."""
     23 
     24 
     25 class NoInterfaceError(Exception):
     26     """Error thrown when the dhcp server has no interfaces on any subnet."""
     27 
     28 
     29 class DhcpServer(object):
     30     """Manages the dhcp server program.
     31 
     32     Only one of these can run in an environment at a time.
     33 
     34     Attributes:
     35         config: The dhcp server configuration that is being used.
     36     """
     37 
     38     PROGRAM_FILE = 'dhcpd'
     39 
     40     def __init__(self, runner, interface, working_dir='/tmp'):
     41         """
     42         Args:
     43             runner: Object that has a run_async and run methods for running
     44                     shell commands.
     45             interface: string, The name of the interface to use.
     46             working_dir: The directory to work out of.
     47         """
     48         self._runner = runner
     49         self._working_dir = working_dir
     50         self._shell = shell.ShellCommand(runner, working_dir)
     51         self._log_file = 'dhcpd_%s.log' % interface
     52         self._config_file = 'dhcpd_%s.conf' % interface
     53         self._lease_file = 'dhcpd_%s.leases' % interface
     54         self._identifier = '%s.*%s' % (self.PROGRAM_FILE, self._config_file)
     55 
     56     def start(self, config, timeout=60):
     57         """Starts the dhcp server.
     58 
     59         Starts the dhcp server daemon and runs it in the background.
     60 
     61         Args:
     62             config: dhcp_config.DhcpConfig, Configs to start the dhcp server
     63                     with.
     64 
     65         Returns:
     66             True if the daemon could be started. Note that the daemon can still
     67             start and not work. Invalid configurations can take a long amount
     68             of time to be produced, and because the daemon runs indefinitely
     69             it's infeasible to wait on. If you need to check if configs are ok
     70             then periodic checks to is_running and logs should be used.
     71         """
     72         if self.is_alive():
     73             self.stop()
     74 
     75         self._write_configs(config)
     76         self._shell.delete_file(self._log_file)
     77         self._shell.touch_file(self._lease_file)
     78 
     79         dhcpd_command = '%s -cf "%s" -lf %s -f""' % (self.PROGRAM_FILE,
     80                                                      self._config_file,
     81                                                      self._lease_file)
     82         base_command = 'cd "%s"; %s' % (self._working_dir, dhcpd_command)
     83         job_str = '%s > "%s" 2>&1' % (base_command, self._log_file)
     84         self._runner.run_async(job_str)
     85 
     86         try:
     87             self._wait_for_process(timeout=timeout)
     88             self._wait_for_server(timeout=timeout)
     89         except:
     90             self.stop()
     91             raise
     92 
     93     def stop(self):
     94         """Kills the daemon if it is running."""
     95         self._shell.kill(self._identifier)
     96 
     97     def is_alive(self):
     98         """
     99         Returns:
    100             True if the daemon is running.
    101         """
    102         return self._shell.is_alive(self._identifier)
    103 
    104     def get_logs(self):
    105         """Pulls the log files from where dhcp server is running.
    106 
    107         Returns:
    108             A string of the dhcp server logs.
    109         """
    110         return self._shell.read_file(self._log_file)
    111 
    112     def _wait_for_process(self, timeout=60):
    113         """Waits for the process to come up.
    114 
    115         Waits until the dhcp server process is found running, or there is
    116         a timeout. If the program never comes up then the log file
    117         will be scanned for errors.
    118 
    119         Raises: See _scan_for_errors
    120         """
    121         start_time = time.time()
    122         while time.time() - start_time < timeout and not self.is_alive():
    123             self._scan_for_errors(False)
    124             time.sleep(0.1)
    125 
    126         self._scan_for_errors(True)
    127 
    128     def _wait_for_server(self, timeout=60):
    129         """Waits for dhcp server to report that the server is up.
    130 
    131         Waits until dhcp server says the server has been brought up or an
    132         error occurs.
    133 
    134         Raises: see _scan_for_errors
    135         """
    136         start_time = time.time()
    137         while time.time() - start_time < timeout:
    138             success = self._shell.search_file(
    139                 'Wrote [0-9]* leases to leases file', self._log_file)
    140             if success:
    141                 return
    142 
    143             self._scan_for_errors(True)
    144 
    145     def _scan_for_errors(self, should_be_up):
    146         """Scans the dhcp server log for any errors.
    147 
    148         Args:
    149             should_be_up: If true then dhcp server is expected to be alive.
    150                           If it is found not alive while this is true an error
    151                           is thrown.
    152 
    153         Raises:
    154             Error: Raised when a dhcp server error is found.
    155         """
    156         # If this is checked last we can run into a race condition where while
    157         # scanning the log the process has not died, but after scanning it
    158         # has. If this were checked last in that condition then the wrong
    159         # error will be thrown. To prevent this we gather the alive state first
    160         # so that if it is dead it will definitely give the right error before
    161         # just giving a generic one.
    162         is_dead = not self.is_alive()
    163 
    164         no_interface = self._shell.search_file(
    165             'Not configured to listen on any interfaces', self._log_file)
    166         if no_interface:
    167             raise NoInterfaceError(
    168                 'Dhcp does not contain a subnet for any of the networks the'
    169                 ' current interfaces are on.')
    170 
    171         if should_be_up and is_dead:
    172             raise Error('Dhcp server failed to start.', self)
    173 
    174     def _write_configs(self, config):
    175         """Writes the configs to the dhcp server config file."""
    176 
    177         self._shell.delete_file(self._config_file)
    178 
    179         lines = []
    180 
    181         if config.default_lease_time:
    182             lines.append('default-lease-time %d;' % config.default_lease_time)
    183         if config.max_lease_time:
    184             lines.append('max-lease-time %s;' % config.max_lease_time)
    185 
    186         for subnet in config.subnets:
    187             address = subnet.network.network_address
    188             mask = subnet.network.netmask
    189             router = subnet.router
    190             start = subnet.start
    191             end = subnet.end
    192             lease_time = subnet.lease_time
    193 
    194             lines.append('subnet %s netmask %s {' % (address, mask))
    195             lines.append('\toption subnet-mask %s;' % mask)
    196             lines.append('\toption routers %s;' % router)
    197             lines.append('\toption domain-name-servers %s;' % _ROUTER_DNS)
    198             lines.append('\trange %s %s;' % (start, end))
    199             if lease_time:
    200                 lines.append('\tdefault-lease-time %d;' % lease_time)
    201                 lines.append('\tmax-lease-time %d;' % lease_time)
    202             lines.append('}')
    203 
    204         for mapping in config.static_mappings:
    205             identifier = mapping.identifier
    206             fixed_address = mapping.ipv4_address
    207             host_fake_name = 'host%s' % identifier.replace(':', '')
    208             lease_time = mapping.lease_time
    209 
    210             lines.append('host %s {' % host_fake_name)
    211             lines.append('\thardware ethernet %s;' % identifier)
    212             lines.append('\tfixed-address %s;' % fixed_address)
    213             if lease_time:
    214                 lines.append('\tdefault-lease-time %d;' % lease_time)
    215                 lines.append('\tmax-lease-time %d;' % lease_time)
    216             lines.append('}')
    217 
    218         config_str = '\n'.join(lines)
    219 
    220         self._shell.write_file(self._config_file, config_str)
    221