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 15import time 16from acts.controllers.utils_lib.commands import shell 17 18_ROUTER_DNS = '8.8.8.8, 4.4.4.4' 19 20 21class Error(Exception): 22 """An error caused by the dhcp server.""" 23 24 25class NoInterfaceError(Exception): 26 """Error thrown when the dhcp server has no interfaces on any subnet.""" 27 28 29class 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