| #!/usr/bin/env python3 |
| # |
| # 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 os |
| import socket |
| import subprocess |
| import threading |
| from typing import List, Optional |
| |
| from antlion import context |
| from antlion.capabilities.ssh import SSHConfig |
| from antlion.controllers.adb_lib.error import AdbCommandError |
| from antlion.controllers.android_device import AndroidDevice |
| from antlion.controllers.fuchsia_lib.ssh import SSHProvider |
| from antlion.controllers.iperf_server import _AndroidDeviceBridge |
| from antlion.libs.proc import job |
| |
| MOBLY_CONTROLLER_CONFIG_NAME = "IPerfClient" |
| ACTS_CONTROLLER_REFERENCE_NAME = "iperf_clients" |
| |
| |
| class IPerfError(Exception): |
| """Raised on execution errors of iPerf.""" |
| |
| |
| def create(configs): |
| """Factory method for iperf clients. |
| |
| The function creates iperf clients based on at least one config. |
| If configs contain ssh settings or and AndroidDevice, remote iperf clients |
| will be started on those devices, otherwise, a the client will run on the |
| local machine. |
| |
| Args: |
| configs: config parameters for the iperf server |
| """ |
| results: List[IPerfClientBase] = [] |
| for c in configs: |
| if type(c) is dict and "AndroidDevice" in c: |
| results.append( |
| IPerfClientOverAdb( |
| c["AndroidDevice"], test_interface=c.get("test_interface") |
| ) |
| ) |
| elif type(c) is dict and "ssh_config" in c: |
| results.append( |
| IPerfClientOverSsh( |
| SSHProvider(SSHConfig.from_config(c["ssh_config"])), |
| test_interface=c.get("test_interface"), |
| ) |
| ) |
| else: |
| results.append(IPerfClient()) |
| return results |
| |
| |
| def get_info(iperf_clients): |
| """Placeholder for info about iperf clients |
| |
| Returns: |
| None |
| """ |
| return None |
| |
| |
| def destroy(_): |
| # No cleanup needed. |
| pass |
| |
| |
| class IPerfClientBase(object): |
| """The Base class for all IPerfClients. |
| |
| This base class is responsible for synchronizing the logging to prevent |
| multiple IPerfClients from writing results to the same file, as well |
| as providing the interface for IPerfClient objects. |
| """ |
| |
| # Keeps track of the number of IPerfClient logs to prevent file name |
| # collisions. |
| __log_file_counter = 0 |
| |
| __log_file_lock = threading.Lock() |
| |
| @staticmethod |
| def _get_full_file_path(tag=""): |
| """Returns the full file path for the IPerfClient log file. |
| |
| Note: If the directory for the file path does not exist, it will be |
| created. |
| |
| Args: |
| tag: The tag passed in to the server run. |
| """ |
| current_context = context.get_current_context() |
| full_out_dir = os.path.join( |
| current_context.get_full_output_path(), "iperf_client_files" |
| ) |
| |
| with IPerfClientBase.__log_file_lock: |
| os.makedirs(full_out_dir, exist_ok=True) |
| tags = ["IPerfClient", tag, IPerfClientBase.__log_file_counter] |
| out_file_name = "%s.log" % ( |
| ",".join([str(x) for x in tags if x != "" and x is not None]) |
| ) |
| IPerfClientBase.__log_file_counter += 1 |
| |
| return os.path.join(full_out_dir, out_file_name) |
| |
| def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): |
| """Starts iperf client, and waits for completion. |
| |
| Args: |
| ip: iperf server ip address. |
| iperf_args: A string representing arguments to start iperf |
| client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". |
| tag: A string to further identify iperf results file |
| timeout: the maximum amount of time the iperf client can run. |
| iperf_binary: Location of iperf3 binary. If none, it is assumed the |
| the binary is in the path. |
| |
| Returns: |
| full_out_path: iperf result path. |
| """ |
| raise NotImplementedError("start() must be implemented.") |
| |
| |
| class IPerfClient(IPerfClientBase): |
| """Class that handles iperf3 client operations.""" |
| |
| def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): |
| """Starts iperf client, and waits for completion. |
| |
| Args: |
| ip: iperf server ip address. |
| iperf_args: A string representing arguments to start iperf |
| client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". |
| tag: tag to further identify iperf results file |
| timeout: unused. |
| iperf_binary: Location of iperf3 binary. If none, it is assumed the |
| the binary is in the path. |
| |
| Returns: |
| full_out_path: iperf result path. |
| """ |
| if not iperf_binary: |
| logging.debug( |
| "No iperf3 binary specified. " "Assuming iperf3 is in the path." |
| ) |
| iperf_binary = "iperf3" |
| else: |
| logging.debug(f"Using iperf3 binary located at {iperf_binary}") |
| iperf_cmd = [str(iperf_binary), "-c", ip] + iperf_args.split(" ") |
| full_out_path = self._get_full_file_path(tag) |
| |
| with open(full_out_path, "w") as out_file: |
| subprocess.call(iperf_cmd, stdout=out_file) |
| |
| return full_out_path |
| |
| |
| class IPerfClientOverSsh(IPerfClientBase): |
| """Class that handles iperf3 client operations on remote machines.""" |
| |
| def __init__( |
| self, |
| ssh_provider: SSHProvider, |
| test_interface: Optional[str] = None, |
| ): |
| self._ssh_provider = ssh_provider |
| self.test_interface = test_interface |
| |
| def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): |
| """Starts iperf client, and waits for completion. |
| |
| Args: |
| ip: iperf server ip address. |
| iperf_args: A string representing arguments to start iperf |
| client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". |
| tag: tag to further identify iperf results file |
| timeout: the maximum amount of time to allow the iperf client to run |
| iperf_binary: Location of iperf3 binary. If none, it is assumed the |
| the binary is in the path. |
| |
| Returns: |
| full_out_path: iperf result path. |
| """ |
| if not iperf_binary: |
| logging.debug( |
| "No iperf3 binary specified. " "Assuming iperf3 is in the path." |
| ) |
| iperf_binary = "iperf3" |
| else: |
| logging.debug(f"Using iperf3 binary located at {iperf_binary}") |
| iperf_cmd = f"{iperf_binary} -c {ip} {iperf_args}" |
| full_out_path = self._get_full_file_path(tag) |
| |
| try: |
| iperf_process = self._ssh_provider.run(iperf_cmd, timeout_sec=timeout) |
| iperf_output = iperf_process.stdout |
| with open(full_out_path, "w") as out_file: |
| out_file.write(iperf_output) |
| except socket.timeout: |
| raise TimeoutError( |
| "Socket timeout. Timed out waiting for iperf " "client to finish." |
| ) |
| except Exception as err: |
| logging.exception(f"iperf run failed: {err}") |
| |
| return full_out_path |
| |
| |
| class IPerfClientOverAdb(IPerfClientBase): |
| """Class that handles iperf3 operations over ADB devices.""" |
| |
| def __init__(self, android_device_or_serial, test_interface=None): |
| """Creates a new IPerfClientOverAdb object. |
| |
| Args: |
| android_device_or_serial: Either an AndroidDevice object, or the |
| serial that corresponds to the AndroidDevice. Note that the |
| serial must be present in an AndroidDevice entry in the ACTS |
| config. |
| test_interface: The network interface that will be used to send |
| traffic to the iperf server. |
| """ |
| self._android_device_or_serial = android_device_or_serial |
| self.test_interface = test_interface |
| |
| @property |
| def _android_device(self): |
| if isinstance(self._android_device_or_serial, AndroidDevice): |
| return self._android_device_or_serial |
| else: |
| return _AndroidDeviceBridge.android_devices()[ |
| self._android_device_or_serial |
| ] |
| |
| def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): |
| """Starts iperf client, and waits for completion. |
| |
| Args: |
| ip: iperf server ip address. |
| iperf_args: A string representing arguments to start iperf |
| client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". |
| tag: tag to further identify iperf results file |
| timeout: the maximum amount of time to allow the iperf client to run |
| iperf_binary: Location of iperf3 binary. If none, it is assumed the |
| the binary is in the path. |
| |
| Returns: |
| The iperf result file path. |
| """ |
| clean_out = "" |
| try: |
| if not iperf_binary: |
| logging.debug( |
| "No iperf3 binary specified. " "Assuming iperf3 is in the path." |
| ) |
| iperf_binary = "iperf3" |
| else: |
| logging.debug(f"Using iperf3 binary located at {iperf_binary}") |
| iperf_cmd = f"{iperf_binary} -c {ip} {iperf_args}" |
| out = self._android_device.adb.shell(str(iperf_cmd), timeout=timeout) |
| clean_out = out.split("\n") |
| if "error" in clean_out[0].lower(): |
| raise IPerfError(clean_out) |
| except (job.TimeoutError, AdbCommandError): |
| logging.warning("TimeoutError: Iperf measurement failed.") |
| |
| full_out_path = self._get_full_file_path(tag) |
| with open(full_out_path, "w") as out_file: |
| out_file.write("\n".join(clean_out)) |
| |
| return full_out_path |