1#!/usr/bin/env python3 2# 3# Copyright 2018 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import logging 18import os 19import subprocess 20import socket 21import threading 22 23from acts import context 24from acts import utils 25from acts.controllers.adb_lib.error import AdbCommandError 26from acts.controllers.android_device import AndroidDevice 27from acts.controllers.iperf_server import _AndroidDeviceBridge 28from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection 29from acts.controllers.fuchsia_lib.utils_lib import ssh_is_connected 30from acts.controllers.fuchsia_lib.utils_lib import SshResults 31from acts.controllers.utils_lib.ssh import connection 32from acts.controllers.utils_lib.ssh import settings 33from acts.event import event_bus 34from acts.event.decorators import subscribe_static 35from acts.event.event import TestClassBeginEvent 36from acts.event.event import TestClassEndEvent 37from acts.libs.proc import job 38from paramiko.buffered_pipe import PipeTimeout 39from paramiko.ssh_exception import SSHException 40MOBLY_CONTROLLER_CONFIG_NAME = 'IPerfClient' 41ACTS_CONTROLLER_REFERENCE_NAME = 'iperf_clients' 42 43 44class IPerfError(Exception): 45 """Raised on execution errors of iPerf.""" 46 47 48def create(configs): 49 """Factory method for iperf clients. 50 51 The function creates iperf clients based on at least one config. 52 If configs contain ssh settings or and AndroidDevice, remote iperf clients 53 will be started on those devices, otherwise, a the client will run on the 54 local machine. 55 56 Args: 57 configs: config parameters for the iperf server 58 """ 59 results = [] 60 for c in configs: 61 if type(c) is dict and 'AndroidDevice' in c: 62 results.append( 63 IPerfClientOverAdb(c['AndroidDevice'], 64 test_interface=c.get('test_interface'))) 65 elif type(c) is dict and 'ssh_config' in c: 66 results.append( 67 IPerfClientOverSsh(c['ssh_config'], 68 use_paramiko=c.get('use_paramiko'), 69 test_interface=c.get('test_interface'))) 70 else: 71 results.append(IPerfClient()) 72 return results 73 74 75def get_info(iperf_clients): 76 """Placeholder for info about iperf clients 77 78 Returns: 79 None 80 """ 81 return None 82 83 84def destroy(_): 85 # No cleanup needed. 86 pass 87 88 89class IPerfClientBase(object): 90 """The Base class for all IPerfClients. 91 92 This base class is responsible for synchronizing the logging to prevent 93 multiple IPerfClients from writing results to the same file, as well 94 as providing the interface for IPerfClient objects. 95 """ 96 # Keeps track of the number of IPerfClient logs to prevent file name 97 # collisions. 98 __log_file_counter = 0 99 100 __log_file_lock = threading.Lock() 101 102 @staticmethod 103 def _get_full_file_path(tag=''): 104 """Returns the full file path for the IPerfClient log file. 105 106 Note: If the directory for the file path does not exist, it will be 107 created. 108 109 Args: 110 tag: The tag passed in to the server run. 111 """ 112 current_context = context.get_current_context() 113 full_out_dir = os.path.join(current_context.get_full_output_path(), 114 'iperf_client_files') 115 116 with IPerfClientBase.__log_file_lock: 117 os.makedirs(full_out_dir, exist_ok=True) 118 tags = ['IPerfClient', tag, IPerfClientBase.__log_file_counter] 119 out_file_name = '%s.log' % (','.join( 120 [str(x) for x in tags if x != '' and x is not None])) 121 IPerfClientBase.__log_file_counter += 1 122 123 return os.path.join(full_out_dir, out_file_name) 124 125 def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): 126 """Starts iperf client, and waits for completion. 127 128 Args: 129 ip: iperf server ip address. 130 iperf_args: A string representing arguments to start iperf 131 client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". 132 tag: A string to further identify iperf results file 133 timeout: the maximum amount of time the iperf client can run. 134 iperf_binary: Location of iperf3 binary. If none, it is assumed the 135 the binary is in the path. 136 137 Returns: 138 full_out_path: iperf result path. 139 """ 140 raise NotImplementedError('start() must be implemented.') 141 142 143class IPerfClient(IPerfClientBase): 144 """Class that handles iperf3 client operations.""" 145 def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): 146 """Starts iperf client, and waits for completion. 147 148 Args: 149 ip: iperf server ip address. 150 iperf_args: A string representing arguments to start iperf 151 client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". 152 tag: tag to further identify iperf results file 153 timeout: unused. 154 iperf_binary: Location of iperf3 binary. If none, it is assumed the 155 the binary is in the path. 156 157 Returns: 158 full_out_path: iperf result path. 159 """ 160 if not iperf_binary: 161 logging.debug('No iperf3 binary specified. ' 162 'Assuming iperf3 is in the path.') 163 iperf_binary = 'iperf3' 164 else: 165 logging.debug('Using iperf3 binary located at %s' % iperf_binary) 166 iperf_cmd = [str(iperf_binary), '-c', ip] + iperf_args.split(' ') 167 full_out_path = self._get_full_file_path(tag) 168 169 with open(full_out_path, 'w') as out_file: 170 subprocess.call(iperf_cmd, stdout=out_file) 171 172 return full_out_path 173 174 175class IPerfClientOverSsh(IPerfClientBase): 176 """Class that handles iperf3 client operations on remote machines.""" 177 def __init__(self, ssh_config, use_paramiko=False, test_interface=None): 178 self._ssh_settings = settings.from_config(ssh_config) 179 if not (utils.is_valid_ipv4_address(self._ssh_settings.hostname) 180 or utils.is_valid_ipv6_address(self._ssh_settings.hostname)): 181 mdns_ip = utils.get_fuchsia_mdns_ipv6_address( 182 self._ssh_settings.hostname) 183 if mdns_ip: 184 self._ssh_settings.hostname = mdns_ip 185 # use_paramiko may be passed in as a string (from JSON), so this line 186 # guarantees it is a converted to a bool. 187 self._use_paramiko = str(use_paramiko).lower() == 'true' 188 self._ssh_session = None 189 self.start_ssh() 190 191 self.hostname = self._ssh_settings.hostname 192 self.test_interface = test_interface 193 194 def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): 195 """Starts iperf client, and waits for completion. 196 197 Args: 198 ip: iperf server ip address. 199 iperf_args: A string representing arguments to start iperf 200 client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". 201 tag: tag to further identify iperf results file 202 timeout: the maximum amount of time to allow the iperf client to run 203 iperf_binary: Location of iperf3 binary. If none, it is assumed the 204 the binary is in the path. 205 206 Returns: 207 full_out_path: iperf result path. 208 """ 209 if not iperf_binary: 210 logging.debug('No iperf3 binary specified. ' 211 'Assuming iperf3 is in the path.') 212 iperf_binary = 'iperf3' 213 else: 214 logging.debug('Using iperf3 binary located at %s' % iperf_binary) 215 iperf_cmd = '{} -c {} {}'.format(iperf_binary, ip, iperf_args) 216 full_out_path = self._get_full_file_path(tag) 217 218 try: 219 if not self._ssh_session: 220 self.start_ssh() 221 if self._use_paramiko: 222 if not ssh_is_connected(self._ssh_session): 223 logging.info('Lost SSH connection to %s. Reconnecting.' % 224 self._ssh_settings.hostname) 225 self._ssh_session.close() 226 self._ssh_session = create_ssh_connection( 227 ip_address=self._ssh_settings.hostname, 228 ssh_username=self._ssh_settings.username, 229 ssh_config=self._ssh_settings.ssh_config) 230 cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = ( 231 self._ssh_session.exec_command(iperf_cmd, timeout=timeout)) 232 iperf_process = SshResults(cmd_result_stdin, cmd_result_stdout, 233 cmd_result_stderr, 234 cmd_result_stdout.channel) 235 else: 236 iperf_process = self._ssh_session.run(iperf_cmd, 237 timeout=timeout) 238 iperf_output = iperf_process.stdout 239 with open(full_out_path, 'w') as out_file: 240 out_file.write(iperf_output) 241 except PipeTimeout: 242 raise TimeoutError('Paramiko PipeTimeout. Timed out waiting for ' 243 'iperf client to finish.') 244 except socket.timeout: 245 raise TimeoutError('Socket timeout. Timed out waiting for iperf ' 246 'client to finish.') 247 except SSHException as err: 248 raise ConnectionError('SSH connection failed: {}'.format(err)) 249 except Exception as err: 250 logging.exception('iperf run failed: {}'.format(err)) 251 252 return full_out_path 253 254 def start_ssh(self): 255 """Starts an ssh session to the iperf client.""" 256 if not self._ssh_session: 257 if self._use_paramiko: 258 self._ssh_session = create_ssh_connection( 259 ip_address=self._ssh_settings.hostname, 260 ssh_username=self._ssh_settings.username, 261 ssh_config=self._ssh_settings.ssh_config) 262 else: 263 self._ssh_session = connection.SshConnection( 264 self._ssh_settings) 265 266 def close_ssh(self): 267 """Closes the ssh session to the iperf client, if one exists, preventing 268 connection reset errors when rebooting client device. 269 """ 270 if self._ssh_session: 271 self._ssh_session.close() 272 self._ssh_session = None 273 274 275class IPerfClientOverAdb(IPerfClientBase): 276 """Class that handles iperf3 operations over ADB devices.""" 277 def __init__(self, android_device_or_serial, test_interface=None): 278 """Creates a new IPerfClientOverAdb object. 279 280 Args: 281 android_device_or_serial: Either an AndroidDevice object, or the 282 serial that corresponds to the AndroidDevice. Note that the 283 serial must be present in an AndroidDevice entry in the ACTS 284 config. 285 test_interface: The network interface that will be used to send 286 traffic to the iperf server. 287 """ 288 self._android_device_or_serial = android_device_or_serial 289 self.test_interface = test_interface 290 291 @property 292 def _android_device(self): 293 if isinstance(self._android_device_or_serial, AndroidDevice): 294 return self._android_device_or_serial 295 else: 296 return _AndroidDeviceBridge.android_devices()[ 297 self._android_device_or_serial] 298 299 def start(self, ip, iperf_args, tag, timeout=3600, iperf_binary=None): 300 """Starts iperf client, and waits for completion. 301 302 Args: 303 ip: iperf server ip address. 304 iperf_args: A string representing arguments to start iperf 305 client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J". 306 tag: tag to further identify iperf results file 307 timeout: the maximum amount of time to allow the iperf client to run 308 iperf_binary: Location of iperf3 binary. If none, it is assumed the 309 the binary is in the path. 310 311 Returns: 312 The iperf result file path. 313 """ 314 clean_out = '' 315 try: 316 if not iperf_binary: 317 logging.debug('No iperf3 binary specified. ' 318 'Assuming iperf3 is in the path.') 319 iperf_binary = 'iperf3' 320 else: 321 logging.debug('Using iperf3 binary located at %s' % 322 iperf_binary) 323 iperf_cmd = '{} -c {} {}'.format(iperf_binary, ip, iperf_args) 324 out = self._android_device.adb.shell(str(iperf_cmd), 325 timeout=timeout) 326 clean_out = out.split('\n') 327 if 'error' in clean_out[0].lower(): 328 raise IPerfError(clean_out) 329 except (job.TimeoutError, AdbCommandError): 330 logging.warning('TimeoutError: Iperf measurement failed.') 331 332 full_out_path = self._get_full_file_path(tag) 333 with open(full_out_path, 'w') as out_file: 334 out_file.write('\n'.join(clean_out)) 335 336 return full_out_path 337