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