1# Copyright 2020 - 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 logging 16import shlex 17import tempfile 18import time 19 20from acts.controllers.utils_lib.commands import shell 21from acts.libs.proc import job 22 23 24class Error(Exception): 25 """An error caused by radvd.""" 26 27 28class Radvd(object): 29 """Manages the radvd program. 30 31 https://en.wikipedia.org/wiki/Radvd 32 This implements the Router Advertisement Daemon of IPv6 router addresses 33 and IPv6 routing prefixes using the Neighbor Discovery Protocol. 34 35 Attributes: 36 config: The radvd configuration that is being used. 37 """ 38 def __init__(self, runner, interface, working_dir=None, radvd_binary=None): 39 """ 40 Args: 41 runner: Object that has run_async and run methods for executing 42 shell commands (e.g. connection.SshConnection) 43 interface: string, The name of the interface to use (eg. wlan0). 44 working_dir: The directory to work out of. 45 radvd_binary: Location of the radvd binary 46 """ 47 if not radvd_binary: 48 logging.debug('No radvd binary specified. ' 49 'Assuming radvd is in the path.') 50 radvd_binary = 'radvd' 51 else: 52 logging.debug('Using radvd binary located at %s' % radvd_binary) 53 if working_dir is None and runner == job.run: 54 working_dir = tempfile.gettempdir() 55 else: 56 working_dir = '/tmp' 57 self._radvd_binary = radvd_binary 58 self._runner = runner 59 self._interface = interface 60 self._working_dir = working_dir 61 self.config = None 62 self._shell = shell.ShellCommand(runner, working_dir) 63 self._log_file = '%s/radvd-%s.log' % (working_dir, self._interface) 64 self._config_file = '%s/radvd-%s.conf' % (working_dir, self._interface) 65 self._pid_file = '%s/radvd-%s.pid' % (working_dir, self._interface) 66 self._ps_identifier = '%s.*%s' % (self._radvd_binary, 67 self._config_file) 68 69 def start(self, config, timeout=60): 70 """Starts radvd 71 72 Starts the radvd daemon and runs it in the background. 73 74 Args: 75 config: Configs to start the radvd with. 76 timeout: Time to wait for radvd to come up. 77 78 Returns: 79 True if the daemon could be started. Note that the daemon can still 80 start and not work. Invalid configurations can take a long amount 81 of time to be produced, and because the daemon runs indefinitely 82 it's impossible to wait on. If you need to check if configs are ok 83 then periodic checks to is_running and logs should be used. 84 """ 85 if self.is_alive(): 86 self.stop() 87 88 self.config = config 89 90 self._shell.delete_file(self._log_file) 91 self._shell.delete_file(self._config_file) 92 self._write_configs(self.config) 93 94 radvd_command = '%s -C %s -p %s -m logfile -d 5 -l %s' % ( 95 self._radvd_binary, shlex.quote(self._config_file), 96 shlex.quote(self._pid_file), self._log_file) 97 job_str = '%s > "%s" 2>&1' % (radvd_command, self._log_file) 98 self._runner.run_async(job_str) 99 100 try: 101 self._wait_for_process(timeout=timeout) 102 except Error: 103 self.stop() 104 raise 105 106 def stop(self): 107 """Kills the daemon if it is running.""" 108 self._shell.kill(self._ps_identifier) 109 110 def is_alive(self): 111 """ 112 Returns: 113 True if the daemon is running. 114 """ 115 return self._shell.is_alive(self._ps_identifier) 116 117 def pull_logs(self): 118 """Pulls the log files from where radvd is running. 119 120 Returns: 121 A string of the radvd logs. 122 """ 123 # TODO: Auto pulling of logs when stop is called. 124 return self._shell.read_file(self._log_file) 125 126 def _wait_for_process(self, timeout=60): 127 """Waits for the process to come up. 128 129 Waits until the radvd process is found running, or there is 130 a timeout. If the program never comes up then the log file 131 will be scanned for errors. 132 133 Raises: See _scan_for_errors 134 """ 135 start_time = time.time() 136 while time.time() - start_time < timeout and not self.is_alive(): 137 time.sleep(0.1) 138 self._scan_for_errors(False) 139 self._scan_for_errors(True) 140 141 def _scan_for_errors(self, should_be_up): 142 """Scans the radvd log for any errors. 143 144 Args: 145 should_be_up: If true then radvd program is expected to be alive. 146 If it is found not alive while this is true an error 147 is thrown. 148 149 Raises: 150 Error: Raised when a radvd error is found. 151 """ 152 # Store this so that all other errors have priority. 153 is_dead = not self.is_alive() 154 155 exited_prematurely = self._shell.search_file('Exiting', self._log_file) 156 if exited_prematurely: 157 raise Error('Radvd exited prematurely.', self) 158 if should_be_up and is_dead: 159 raise Error('Radvd failed to start', self) 160 161 def _write_configs(self, config): 162 """Writes the configs to the radvd config file. 163 164 Args: 165 config: a RadvdConfig object. 166 """ 167 self._shell.delete_file(self._config_file) 168 conf = config.package_configs() 169 lines = ['interface %s {' % self._interface] 170 for (interface_option_key, 171 interface_option) in conf['interface_options'].items(): 172 lines.append('\t%s %s;' % 173 (str(interface_option_key), str(interface_option))) 174 lines.append('\tprefix %s' % conf['prefix']) 175 lines.append('\t{') 176 for prefix_option in conf['prefix_options'].items(): 177 lines.append('\t\t%s;' % ' '.join(map(str, prefix_option))) 178 lines.append('\t};') 179 if conf['clients']: 180 lines.append('\tclients') 181 lines.append('\t{') 182 for client in conf['clients']: 183 lines.append('\t\t%s;' % client) 184 lines.append('\t};') 185 if conf['route']: 186 lines.append('\troute %s {' % conf['route']) 187 for route_option in conf['route_options'].items(): 188 lines.append('\t\t%s;' % ' '.join(map(str, route_option))) 189 lines.append('\t};') 190 if conf['rdnss']: 191 lines.append('\tRDNSS %s {' % 192 ' '.join([str(elem) for elem in conf['rdnss']])) 193 for rdnss_option in conf['rdnss_options'].items(): 194 lines.append('\t\t%s;' % ' '.join(map(str, rdnss_option))) 195 lines.append('\t};') 196 lines.append('};') 197 output_config = '\n'.join(lines) 198 logging.info('Writing %s' % self._config_file) 199 logging.debug('******************Start*******************') 200 logging.debug('\n%s' % output_config) 201 logging.debug('*******************End********************') 202 203 self._shell.write_file(self._config_file, output_config) 204