| # Copyright 2022 The Fuchsia Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import logging |
| import shlex |
| import tempfile |
| import time |
| |
| from typing import Any, Optional |
| |
| from antlion.controllers.ap_lib.radvd_config import RadvdConfig |
| from antlion.controllers.utils_lib.commands import shell |
| from antlion.libs.proc import job |
| |
| |
| class Error(Exception): |
| """An error caused by radvd.""" |
| |
| |
| class Radvd(object): |
| """Manages the radvd program. |
| |
| https://en.wikipedia.org/wiki/Radvd |
| This implements the Router Advertisement Daemon of IPv6 router addresses |
| and IPv6 routing prefixes using the Neighbor Discovery Protocol. |
| |
| Attributes: |
| config: The radvd configuration that is being used. |
| """ |
| |
| def __init__( |
| self, |
| runner: Any, |
| interface: str, |
| working_dir: Optional[str] = None, |
| radvd_binary: Optional[str] = None, |
| ) -> None: |
| """ |
| Args: |
| runner: Object that has run_async and run methods for executing |
| shell commands (e.g. connection.SshConnection) |
| interface: Name of the interface to use (eg. wlan0). |
| working_dir: Directory to work out of. |
| radvd_binary: Location of the radvd binary |
| """ |
| if not radvd_binary: |
| logging.debug( |
| "No radvd binary specified. " "Assuming radvd is in the path." |
| ) |
| radvd_binary = "radvd" |
| else: |
| logging.debug(f"Using radvd binary located at {radvd_binary}") |
| if working_dir is None and runner == job.run: |
| working_dir = tempfile.gettempdir() |
| else: |
| working_dir = "/tmp" |
| self._radvd_binary = radvd_binary |
| self._runner = runner |
| self._interface = interface |
| self._working_dir = working_dir |
| self.config: Optional[RadvdConfig] = None |
| self._shell = shell.ShellCommand(runner, working_dir) |
| self._log_file = f"{working_dir}/radvd-{self._interface}.log" |
| self._config_file = f"{working_dir}/radvd-{self._interface}.conf" |
| self._pid_file = f"{working_dir}/radvd-{self._interface}.pid" |
| self._ps_identifier = f"{self._radvd_binary}.*{self._config_file}" |
| |
| def start(self, config: RadvdConfig, timeout: int = 60) -> None: |
| """Starts radvd |
| |
| Starts the radvd daemon and runs it in the background. |
| |
| Args: |
| config: Configs to start the radvd with. |
| timeout: Time to wait for radvd to come up. |
| |
| Returns: |
| True if the daemon could be started. Note that the daemon can still |
| start and not work. Invalid configurations can take a long amount |
| of time to be produced, and because the daemon runs indefinitely |
| it's impossible to wait on. If you need to check if configs are ok |
| then periodic checks to is_running and logs should be used. |
| """ |
| if self.is_alive(): |
| self.stop() |
| |
| self.config = config |
| |
| self._shell.delete_file(self._log_file) |
| self._shell.delete_file(self._config_file) |
| self._write_configs(self.config) |
| |
| command = ( |
| f"{self._radvd_binary} -C {shlex.quote(self._config_file)} " |
| f"-p {shlex.quote(self._pid_file)} -m logfile -d 5 " |
| f'-l {self._log_file} > "{self._log_file}" 2>&1' |
| ) |
| self._runner.run_async(command) |
| |
| try: |
| self._wait_for_process(timeout=timeout) |
| except Error: |
| self.stop() |
| raise |
| |
| def stop(self): |
| """Kills the daemon if it is running.""" |
| self._shell.kill(self._ps_identifier) |
| |
| def is_alive(self): |
| """ |
| Returns: |
| True if the daemon is running. |
| """ |
| return self._shell.is_alive(self._ps_identifier) |
| |
| def pull_logs(self) -> str: |
| """Pulls the log files from where radvd is running. |
| |
| Returns: |
| A string of the radvd logs. |
| """ |
| # TODO: Auto pulling of logs when stop is called. |
| return self._shell.read_file(self._log_file) |
| |
| def _wait_for_process(self, timeout: int = 60) -> None: |
| """Waits for the process to come up. |
| |
| Waits until the radvd process is found running, or there is |
| a timeout. If the program never comes up then the log file |
| will be scanned for errors. |
| |
| Raises: See _scan_for_errors |
| """ |
| start_time = time.time() |
| while time.time() - start_time < timeout and not self.is_alive(): |
| time.sleep(0.1) |
| self._scan_for_errors(False) |
| self._scan_for_errors(True) |
| |
| def _scan_for_errors(self, should_be_up: bool) -> None: |
| """Scans the radvd log for any errors. |
| |
| Args: |
| should_be_up: If true then radvd program is expected to be alive. |
| If it is found not alive while this is true an error |
| is thrown. |
| |
| Raises: |
| Error: Raised when a radvd error is found. |
| """ |
| # Store this so that all other errors have priority. |
| is_dead = not self.is_alive() |
| |
| exited_prematurely = self._shell.search_file("Exiting", self._log_file) |
| if exited_prematurely: |
| raise Error("Radvd exited prematurely.", self) |
| if should_be_up and is_dead: |
| raise Error("Radvd failed to start", self) |
| |
| def _write_configs(self, config: RadvdConfig) -> None: |
| """Writes the configs to the radvd config file. |
| |
| Args: |
| config: a RadvdConfig object. |
| """ |
| self._shell.delete_file(self._config_file) |
| conf = config.package_configs() |
| lines = ["interface %s {" % self._interface] |
| for interface_option_key, interface_option in conf["interface_options"].items(): |
| lines.append( |
| "\t%s %s;" % (str(interface_option_key), str(interface_option)) |
| ) |
| lines.append("\tprefix %s" % conf["prefix"]) |
| lines.append("\t{") |
| for prefix_option in conf["prefix_options"].items(): |
| lines.append("\t\t%s;" % " ".join(map(str, prefix_option))) |
| lines.append("\t};") |
| if conf["clients"]: |
| lines.append("\tclients") |
| lines.append("\t{") |
| for client in conf["clients"]: |
| lines.append("\t\t%s;" % client) |
| lines.append("\t};") |
| if conf["route"]: |
| lines.append("\troute %s {" % conf["route"]) |
| for route_option in conf["route_options"].items(): |
| lines.append("\t\t%s;" % " ".join(map(str, route_option))) |
| lines.append("\t};") |
| if conf["rdnss"]: |
| lines.append( |
| "\tRDNSS %s {" % " ".join([str(elem) for elem in conf["rdnss"]]) |
| ) |
| for rdnss_option in conf["rdnss_options"].items(): |
| lines.append("\t\t%s;" % " ".join(map(str, rdnss_option))) |
| lines.append("\t};") |
| lines.append("};") |
| output_config = "\n".join(lines) |
| logging.info("Writing %s" % self._config_file) |
| logging.debug("******************Start*******************") |
| logging.debug("\n%s" % output_config) |
| logging.debug("*******************End********************") |
| |
| self._shell.write_file(self._config_file, output_config) |