| #!/usr/bin/env python3 |
| # |
| # Copyright 2018 - The Android Open Source Project |
| # |
| # 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 subprocess |
| import socket |
| import threading |
| |
| from acts import context |
| from acts import utils |
| from acts.controllers.adb_lib.error import AdbCommandError |
| from acts.controllers.android_device import AndroidDevice |
| from acts.controllers.iperf_server import _AndroidDeviceBridge |
| from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection |
| from acts.controllers.fuchsia_lib.utils_lib import ssh_is_connected |
| from acts.controllers.fuchsia_lib.utils_lib import SshResults |
| from acts.controllers.utils_lib.ssh import connection |
| from acts.controllers.utils_lib.ssh import settings |
| from acts.event import event_bus |
| from acts.event.decorators import subscribe_static |
| from acts.event.event import TestClassBeginEvent |
| from acts.event.event import TestClassEndEvent |
| from acts.libs.proc import job |
| from paramiko.buffered_pipe import PipeTimeout |
| from paramiko.ssh_exception import SSHException |
| 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 = [] |
| 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(c['ssh_config'], |
| use_paramiko=c.get('use_paramiko'), |
| 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('Using iperf3 binary located at %s' % 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_config, use_paramiko=False, test_interface=None): |
| self._ssh_settings = settings.from_config(ssh_config) |
| if not (utils.is_valid_ipv4_address(self._ssh_settings.hostname) |
| or utils.is_valid_ipv6_address(self._ssh_settings.hostname)): |
| mdns_ip = utils.get_fuchsia_mdns_ipv6_address( |
| self._ssh_settings.hostname) |
| if mdns_ip: |
| self._ssh_settings.hostname = mdns_ip |
| # use_paramiko may be passed in as a string (from JSON), so this line |
| # guarantees it is a converted to a bool. |
| self._use_paramiko = str(use_paramiko).lower() == 'true' |
| self._ssh_session = None |
| self.start_ssh() |
| |
| self.hostname = self._ssh_settings.hostname |
| 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('Using iperf3 binary located at %s' % iperf_binary) |
| iperf_cmd = '{} -c {} {}'.format(iperf_binary, ip, iperf_args) |
| full_out_path = self._get_full_file_path(tag) |
| |
| try: |
| if not self._ssh_session: |
| self.start_ssh() |
| if self._use_paramiko: |
| if not ssh_is_connected(self._ssh_session): |
| logging.info('Lost SSH connection to %s. Reconnecting.' % |
| self._ssh_settings.hostname) |
| self._ssh_session.close() |
| self._ssh_session = create_ssh_connection( |
| ip_address=self._ssh_settings.hostname, |
| ssh_username=self._ssh_settings.username, |
| ssh_config=self._ssh_settings.ssh_config) |
| cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = ( |
| self._ssh_session.exec_command(iperf_cmd, timeout=timeout)) |
| iperf_process = SshResults(cmd_result_stdin, cmd_result_stdout, |
| cmd_result_stderr, |
| cmd_result_stdout.channel) |
| else: |
| iperf_process = self._ssh_session.run(iperf_cmd, |
| timeout=timeout) |
| iperf_output = iperf_process.stdout |
| with open(full_out_path, 'w') as out_file: |
| out_file.write(iperf_output) |
| except PipeTimeout: |
| raise TimeoutError('Paramiko PipeTimeout. Timed out waiting for ' |
| 'iperf client to finish.') |
| except socket.timeout: |
| raise TimeoutError('Socket timeout. Timed out waiting for iperf ' |
| 'client to finish.') |
| except SSHException as err: |
| raise ConnectionError('SSH connection failed: {}'.format(err)) |
| except Exception as err: |
| logging.exception('iperf run failed: {}'.format(err)) |
| |
| return full_out_path |
| |
| def start_ssh(self): |
| """Starts an ssh session to the iperf client.""" |
| if not self._ssh_session: |
| if self._use_paramiko: |
| self._ssh_session = create_ssh_connection( |
| ip_address=self._ssh_settings.hostname, |
| ssh_username=self._ssh_settings.username, |
| ssh_config=self._ssh_settings.ssh_config) |
| else: |
| self._ssh_session = connection.SshConnection( |
| self._ssh_settings) |
| |
| def close_ssh(self): |
| """Closes the ssh session to the iperf client, if one exists, preventing |
| connection reset errors when rebooting client device. |
| """ |
| if self._ssh_session: |
| self._ssh_session.close() |
| self._ssh_session = None |
| |
| |
| 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('Using iperf3 binary located at %s' % |
| iperf_binary) |
| iperf_cmd = '{} -c {} {}'.format(iperf_binary, 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 |