• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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