1#!/usr/bin/env python3
2#
3#   Copyright 2016 - 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 json
18import logging
19import math
20import os
21import shlex
22import subprocess
23import threading
24import time
25
26from acts import context
27from acts import utils
28from acts.controllers.android_device import AndroidDevice
29from acts.controllers.utils_lib.ssh import connection
30from acts.controllers.utils_lib.ssh import settings
31from acts.event import event_bus
32from acts.event.decorators import subscribe_static
33from acts.event.event import TestClassBeginEvent
34from acts.event.event import TestClassEndEvent
35from acts.libs.proc import job
36
37MOBLY_CONTROLLER_CONFIG_NAME = 'IPerfServer'
38ACTS_CONTROLLER_REFERENCE_NAME = 'iperf_servers'
39KILOBITS = 1024
40MEGABITS = KILOBITS * 1024
41GIGABITS = MEGABITS * 1024
42BITS_IN_BYTE = 8
43
44
45def create(configs):
46    """ Factory method for iperf servers.
47
48    The function creates iperf servers based on at least one config.
49    If configs only specify a port number, a regular local IPerfServer object
50    will be created. If configs contains ssh settings or and AndroidDevice,
51    remote iperf servers will be started on those devices
52
53    Args:
54        configs: config parameters for the iperf server
55    """
56    results = []
57    for c in configs:
58        if type(c) in (str, int) and str(c).isdigit():
59            results.append(IPerfServer(int(c)))
60        elif type(c) is dict and 'AndroidDevice' in c and 'port' in c:
61            results.append(IPerfServerOverAdb(c['AndroidDevice'], c['port']))
62        elif type(c) is dict and 'ssh_config' in c and 'port' in c:
63            results.append(
64                IPerfServerOverSsh(c['ssh_config'],
65                                   c['port'],
66                                   test_interface=c.get('test_interface'),
67                                   use_killall=c.get('use_killall')))
68        else:
69            raise ValueError(
70                'Config entry %s in %s is not a valid IPerfServer '
71                'config.' % (repr(c), configs))
72    return results
73
74
75def get_info(iperf_servers):
76    """Placeholder for info about iperf servers
77
78    Returns:
79        None
80    """
81    return None
82
83
84def destroy(iperf_server_list):
85    for iperf_server in iperf_server_list:
86        try:
87            iperf_server.stop()
88        except Exception:
89            logging.exception('Unable to properly clean up %s.' % iperf_server)
90
91
92class IPerfResult(object):
93    def __init__(self, result_path, reporting_speed_units='Mbytes'):
94        """Loads iperf result from file.
95
96        Loads iperf result from JSON formatted server log. File can be accessed
97        before or after server is stopped. Note that only the first JSON object
98        will be loaded and this funtion is not intended to be used with files
99        containing multiple iperf client runs.
100        """
101        # if result_path isn't a path, treat it as JSON
102        self.reporting_speed_units = reporting_speed_units
103        if not os.path.exists(result_path):
104            self.result = json.loads(result_path)
105        else:
106            try:
107                with open(result_path, 'r') as f:
108                    iperf_output = f.readlines()
109                    if '}\n' in iperf_output:
110                        iperf_output = iperf_output[:iperf_output.index('}\n'
111                                                                        ) + 1]
112                    iperf_string = ''.join(iperf_output)
113                    iperf_string = iperf_string.replace('nan', '0')
114                    self.result = json.loads(iperf_string)
115            except ValueError:
116                with open(result_path, 'r') as f:
117                    # Possibly a result from interrupted iperf run,
118                    # skip first line and try again.
119                    lines = f.readlines()[1:]
120                    self.result = json.loads(''.join(lines))
121
122    def _has_data(self):
123        """Checks if the iperf result has valid throughput data.
124
125        Returns:
126            True if the result contains throughput data. False otherwise.
127        """
128        return ('end' in self.result) and ('sum_received' in self.result['end']
129                                           or 'sum' in self.result['end'])
130
131    def _get_reporting_speed(self, network_speed_in_bits_per_second):
132        """Sets the units for the network speed reporting based on how the
133        object was initiated.  Defaults to Megabytes per second.  Currently
134        supported, bits per second (bits), kilobits per second (kbits), megabits
135        per second (mbits), gigabits per second (gbits), bytes per second
136        (bytes), kilobits per second (kbytes), megabits per second (mbytes),
137        gigabytes per second (gbytes).
138
139        Args:
140            network_speed_in_bits_per_second: The network speed from iperf in
141                bits per second.
142
143        Returns:
144            The value of the throughput in the appropriate units.
145        """
146        speed_divisor = 1
147        if self.reporting_speed_units[1:].lower() == 'bytes':
148            speed_divisor = speed_divisor * BITS_IN_BYTE
149        if self.reporting_speed_units[0:1].lower() == 'k':
150            speed_divisor = speed_divisor * KILOBITS
151        if self.reporting_speed_units[0:1].lower() == 'm':
152            speed_divisor = speed_divisor * MEGABITS
153        if self.reporting_speed_units[0:1].lower() == 'g':
154            speed_divisor = speed_divisor * GIGABITS
155        return network_speed_in_bits_per_second / speed_divisor
156
157    def get_json(self):
158        """Returns the raw json output from iPerf."""
159        return self.result
160
161    @property
162    def error(self):
163        return self.result.get('error', None)
164
165    @property
166    def avg_rate(self):
167        """Average UDP rate in MB/s over the entire run.
168
169        This is the average UDP rate observed at the terminal the iperf result
170        is pulled from. According to iperf3 documentation this is calculated
171        based on bytes sent and thus is not a good representation of the
172        quality of the link. If the result is not from a success run, this
173        property is None.
174        """
175        if not self._has_data() or 'sum' not in self.result['end']:
176            return None
177        bps = self.result['end']['sum']['bits_per_second']
178        return self._get_reporting_speed(bps)
179
180    @property
181    def avg_receive_rate(self):
182        """Average receiving rate in MB/s over the entire run.
183
184        This data may not exist if iperf was interrupted. If the result is not
185        from a success run, this property is None.
186        """
187        if not self._has_data() or 'sum_received' not in self.result['end']:
188            return None
189        bps = self.result['end']['sum_received']['bits_per_second']
190        return self._get_reporting_speed(bps)
191
192    @property
193    def avg_send_rate(self):
194        """Average sending rate in MB/s over the entire run.
195
196        This data may not exist if iperf was interrupted. If the result is not
197        from a success run, this property is None.
198        """
199        if not self._has_data() or 'sum_sent' not in self.result['end']:
200            return None
201        bps = self.result['end']['sum_sent']['bits_per_second']
202        return self._get_reporting_speed(bps)
203
204    @property
205    def instantaneous_rates(self):
206        """Instantaneous received rate in MB/s over entire run.
207
208        This data may not exist if iperf was interrupted. If the result is not
209        from a success run, this property is None.
210        """
211        if not self._has_data():
212            return None
213        intervals = [
214            self._get_reporting_speed(interval['sum']['bits_per_second'])
215            for interval in self.result['intervals']
216        ]
217        return intervals
218
219    @property
220    def std_deviation(self):
221        """Standard deviation of rates in MB/s over entire run.
222
223        This data may not exist if iperf was interrupted. If the result is not
224        from a success run, this property is None.
225        """
226        return self.get_std_deviation(0)
227
228    def get_std_deviation(self, iperf_ignored_interval):
229        """Standard deviation of rates in MB/s over entire run.
230
231        This data may not exist if iperf was interrupted. If the result is not
232        from a success run, this property is None. A configurable number of
233        beginning (and the single last) intervals are ignored in the
234        calculation as they are inaccurate (e.g. the last is from a very small
235        interval)
236
237        Args:
238            iperf_ignored_interval: number of iperf interval to ignored in
239            calculating standard deviation
240
241        Returns:
242            The standard deviation.
243        """
244        if not self._has_data():
245            return None
246        instantaneous_rates = self.instantaneous_rates[
247            iperf_ignored_interval:-1]
248        avg_rate = math.fsum(instantaneous_rates) / len(instantaneous_rates)
249        sqd_deviations = ([(rate - avg_rate)**2
250                           for rate in instantaneous_rates])
251        std_dev = math.sqrt(
252            math.fsum(sqd_deviations) / (len(sqd_deviations) - 1))
253        return std_dev
254
255
256class IPerfServerBase(object):
257    # Keeps track of the number of IPerfServer logs to prevent file name
258    # collisions.
259    __log_file_counter = 0
260
261    __log_file_lock = threading.Lock()
262
263    def __init__(self, port):
264        self._port = port
265        # TODO(markdr): We shouldn't be storing the log files in an array like
266        # this. Nobody should be reading this property either. Instead, the
267        # IPerfResult should be returned in stop() with all the necessary info.
268        # See aosp/1012824 for a WIP implementation.
269        self.log_files = []
270
271    @property
272    def port(self):
273        raise NotImplementedError('port must be specified.')
274
275    @property
276    def started(self):
277        raise NotImplementedError('started must be specified.')
278
279    def start(self, extra_args='', tag=''):
280        """Starts an iperf3 server.
281
282        Args:
283            extra_args: A string representing extra arguments to start iperf
284                server with.
285            tag: Appended to log file name to identify logs from different
286                iperf runs.
287        """
288        raise NotImplementedError('start() must be specified.')
289
290    def stop(self):
291        """Stops the iperf server.
292
293        Returns:
294            The name of the log file generated from the terminated session.
295        """
296        raise NotImplementedError('stop() must be specified.')
297
298    def get_interface_ip_addresses(self, interface):
299        """Gets all of the ip addresses, ipv4 and ipv6, associated with a
300           particular interface name.
301
302        Args:
303            interface: The interface name on the device, ie eth0
304
305        Returns:
306            A list of dictionaries of the the various IP addresses:
307                ipv4_private_local_addresses: Any 192.168, 172.16, or 10
308                    addresses
309                ipv4_public_addresses: Any IPv4 public addresses
310                ipv6_link_local_addresses: Any fe80:: addresses
311                ipv6_private_local_addresses: Any fd00:: addresses
312                ipv6_public_addresses: Any publicly routable addresses
313        """
314        raise NotImplementedError('get_interface_ip_addresses'
315                                  ' must be specified.')
316
317    def _get_full_file_path(self, tag=None):
318        """Returns the full file path for the IPerfServer log file.
319
320        Note: If the directory for the file path does not exist, it will be
321        created.
322
323        Args:
324            tag: The tag passed in to the server run.
325        """
326        out_dir = self.log_path
327
328        with IPerfServerBase.__log_file_lock:
329            tags = [tag, IPerfServerBase.__log_file_counter]
330            out_file_name = 'IPerfServer,%s.log' % (','.join(
331                [str(x) for x in tags if x != '' and x is not None]))
332            IPerfServerBase.__log_file_counter += 1
333
334        file_path = os.path.join(out_dir, out_file_name)
335        self.log_files.append(file_path)
336        return file_path
337
338    @property
339    def log_path(self):
340        current_context = context.get_current_context()
341        full_out_dir = os.path.join(current_context.get_full_output_path(),
342                                    'IPerfServer%s' % self.port)
343
344        # Ensure the directory exists.
345        os.makedirs(full_out_dir, exist_ok=True)
346
347        return full_out_dir
348
349
350def _get_port_from_ss_output(ss_output, pid):
351    pid = str(pid)
352    lines = ss_output.split('\n')
353    for line in lines:
354        if pid in line:
355            # Expected format:
356            # tcp LISTEN  0 5 *:<PORT>  *:* users:(("cmd",pid=<PID>,fd=3))
357            return line.split()[4].split(':')[-1]
358    else:
359        raise ProcessLookupError('Could not find started iperf3 process.')
360
361
362class IPerfServer(IPerfServerBase):
363    """Class that handles iperf server commands on localhost."""
364    def __init__(self, port=5201):
365        super().__init__(port)
366        self._hinted_port = port
367        self._current_log_file = None
368        self._iperf_process = None
369        self._last_opened_file = None
370
371    @property
372    def port(self):
373        return self._port
374
375    @property
376    def started(self):
377        return self._iperf_process is not None
378
379    def start(self, extra_args='', tag=''):
380        """Starts iperf server on local machine.
381
382        Args:
383            extra_args: A string representing extra arguments to start iperf
384                server with.
385            tag: Appended to log file name to identify logs from different
386                iperf runs.
387        """
388        if self._iperf_process is not None:
389            return
390
391        self._current_log_file = self._get_full_file_path(tag)
392
393        # Run an iperf3 server on the hinted port with JSON output.
394        command = ['iperf3', '-s', '-p', str(self._hinted_port), '-J']
395
396        command.extend(shlex.split(extra_args))
397
398        if self._last_opened_file:
399            self._last_opened_file.close()
400        self._last_opened_file = open(self._current_log_file, 'w')
401        self._iperf_process = subprocess.Popen(command,
402                                               stdout=self._last_opened_file,
403                                               stderr=subprocess.DEVNULL)
404        for attempts_left in reversed(range(3)):
405            try:
406                self._port = int(
407                    _get_port_from_ss_output(
408                        job.run('ss -l -p -n | grep iperf').stdout,
409                        self._iperf_process.pid))
410                break
411            except ProcessLookupError:
412                if attempts_left == 0:
413                    raise
414                logging.debug('iperf3 process not started yet.')
415                time.sleep(.01)
416
417    def stop(self):
418        """Stops the iperf server.
419
420        Returns:
421            The name of the log file generated from the terminated session.
422        """
423        if self._iperf_process is None:
424            return
425
426        if self._last_opened_file:
427            self._last_opened_file.close()
428            self._last_opened_file = None
429
430        self._iperf_process.terminate()
431        self._iperf_process = None
432
433        return self._current_log_file
434
435    def get_interface_ip_addresses(self, interface):
436        """Gets all of the ip addresses, ipv4 and ipv6, associated with a
437           particular interface name.
438
439        Args:
440            interface: The interface name on the device, ie eth0
441
442        Returns:
443            A list of dictionaries of the the various IP addresses:
444                ipv4_private_local_addresses: Any 192.168, 172.16, or 10
445                    addresses
446                ipv4_public_addresses: Any IPv4 public addresses
447                ipv6_link_local_addresses: Any fe80:: addresses
448                ipv6_private_local_addresses: Any fd00:: addresses
449                ipv6_public_addresses: Any publicly routable addresses
450        """
451        return utils.get_interface_ip_addresses(job, interface)
452
453    def __del__(self):
454        self.stop()
455
456
457class IPerfServerOverSsh(IPerfServerBase):
458    """Class that handles iperf3 operations on remote machines."""
459    def __init__(self,
460                 ssh_config,
461                 port,
462                 test_interface=None,
463                 use_killall=False):
464        super().__init__(port)
465        self.ssh_settings = settings.from_config(ssh_config)
466        self._ssh_session = None
467        self.start_ssh()
468
469        self._iperf_pid = None
470        self._current_tag = None
471        self.hostname = self.ssh_settings.hostname
472        self._use_killall = str(use_killall).lower() == 'true'
473        try:
474            # A test interface can only be found if an ip address is specified.
475            # A fully qualified hostname will return None for the
476            # test_interface.
477            self.test_interface = self._get_test_interface_based_on_ip(
478                test_interface)
479        except Exception:
480            self.test_interface = None
481
482    @property
483    def port(self):
484        return self._port
485
486    @property
487    def started(self):
488        return self._iperf_pid is not None
489
490    def _get_remote_log_path(self):
491        return '/tmp/iperf_server_port%s.log' % self.port
492
493    def _get_test_interface_based_on_ip(self, test_interface):
494        """Gets the test interface for a particular IP if the test interface
495            passed in test_interface is None
496
497        Args:
498            test_interface: Either a interface name, ie eth0, or None
499
500        Returns:
501            The name of the test interface.
502        """
503        if test_interface:
504            return test_interface
505        return utils.get_interface_based_on_ip(self._ssh_session,
506                                               self.hostname)
507
508    def get_interface_ip_addresses(self, interface):
509        """Gets all of the ip addresses, ipv4 and ipv6, associated with a
510           particular interface name.
511
512        Args:
513            interface: The interface name on the device, ie eth0
514
515        Returns:
516            A list of dictionaries of the the various IP addresses:
517                ipv4_private_local_addresses: Any 192.168, 172.16, or 10
518                    addresses
519                ipv4_public_addresses: Any IPv4 public addresses
520                ipv6_link_local_addresses: Any fe80:: addresses
521                ipv6_private_local_addresses: Any fd00:: addresses
522                ipv6_public_addresses: Any publicly routable addresses
523        """
524        if not self._ssh_session:
525            self.start_ssh()
526        return utils.get_interface_ip_addresses(self._ssh_session, interface)
527
528    def renew_test_interface_ip_address(self):
529        """Renews the test interface's IP address.  Necessary for changing
530           DHCP scopes during a test.
531        """
532        if not self._ssh_session:
533            self.start_ssh()
534        utils.renew_linux_ip_address(self._ssh_session, self.test_interface)
535
536    def start(self, extra_args='', tag='', iperf_binary=None):
537        """Starts iperf server on specified machine and port.
538
539        Args:
540            extra_args: A string representing extra arguments to start iperf
541                server with.
542            tag: Appended to log file name to identify logs from different
543                iperf runs.
544            iperf_binary: Location of iperf3 binary. If none, it is assumed the
545                the binary is in the path.
546        """
547        if self.started:
548            return
549
550        if not self._ssh_session:
551            self.start_ssh()
552        if not iperf_binary:
553            logging.debug('No iperf3 binary specified.  '
554                          'Assuming iperf3 is in the path.')
555            iperf_binary = 'iperf3'
556        else:
557            logging.debug('Using iperf3 binary located at %s' % iperf_binary)
558        iperf_command = '{} -s -J -p {}'.format(iperf_binary, self.port)
559
560        cmd = '{cmd} {extra_flags} > {log_file}'.format(
561            cmd=iperf_command,
562            extra_flags=extra_args,
563            log_file=self._get_remote_log_path())
564
565        job_result = self._ssh_session.run_async(cmd)
566        self._iperf_pid = job_result.stdout
567        self._current_tag = tag
568
569    def stop(self):
570        """Stops the iperf server.
571
572        Returns:
573            The name of the log file generated from the terminated session.
574        """
575        if not self.started:
576            return
577
578        if self._use_killall:
579            self._ssh_session.run('killall iperf3', ignore_status=True)
580        else:
581            self._ssh_session.run_async('kill -9 {}'.format(
582                str(self._iperf_pid)))
583
584        iperf_result = self._ssh_session.run('cat {}'.format(
585            self._get_remote_log_path()))
586
587        log_file = self._get_full_file_path(self._current_tag)
588        with open(log_file, 'w') as f:
589            f.write(iperf_result.stdout)
590
591        self._ssh_session.run_async('rm {}'.format(
592            self._get_remote_log_path()))
593        self._iperf_pid = None
594        return log_file
595
596    def start_ssh(self):
597        """Starts an ssh session to the iperf server."""
598        if not self._ssh_session:
599            self._ssh_session = connection.SshConnection(self.ssh_settings)
600
601    def close_ssh(self):
602        """Closes the ssh session to the iperf server, if one exists, preventing
603        connection reset errors when rebooting server device.
604        """
605        if self.started:
606            self.stop()
607        if self._ssh_session:
608            self._ssh_session.close()
609            self._ssh_session = None
610
611
612# TODO(markdr): Remove this after automagic controller creation has been
613# removed.
614class _AndroidDeviceBridge(object):
615    """A helper class for connecting serial numbers to AndroidDevices."""
616
617    _test_class = None
618
619    @staticmethod
620    @subscribe_static(TestClassBeginEvent)
621    def on_test_begin(event):
622        _AndroidDeviceBridge._test_class = event.test_class
623
624    @staticmethod
625    @subscribe_static(TestClassEndEvent)
626    def on_test_end(_):
627        _AndroidDeviceBridge._test_class = None
628
629    @staticmethod
630    def android_devices():
631        """A dict of serial -> AndroidDevice, where AndroidDevice is a device
632        found in the current TestClass's controllers.
633        """
634        if not _AndroidDeviceBridge._test_class:
635            return {}
636        return {
637            device.serial: device
638            for device in _AndroidDeviceBridge._test_class.android_devices
639        }
640
641
642event_bus.register_subscription(
643    _AndroidDeviceBridge.on_test_begin.subscription)
644event_bus.register_subscription(_AndroidDeviceBridge.on_test_end.subscription)
645
646
647class IPerfServerOverAdb(IPerfServerBase):
648    """Class that handles iperf3 operations over ADB devices."""
649    def __init__(self, android_device_or_serial, port):
650        """Creates a new IPerfServerOverAdb object.
651
652        Args:
653            android_device_or_serial: Either an AndroidDevice object, or the
654                serial that corresponds to the AndroidDevice. Note that the
655                serial must be present in an AndroidDevice entry in the ACTS
656                config.
657            port: The port number to open the iperf server on.
658        """
659        super().__init__(port)
660        self._android_device_or_serial = android_device_or_serial
661
662        self._iperf_process = None
663        self._current_tag = ''
664
665    @property
666    def port(self):
667        return self._port
668
669    @property
670    def started(self):
671        return self._iperf_process is not None
672
673    @property
674    def _android_device(self):
675        if isinstance(self._android_device_or_serial, AndroidDevice):
676            return self._android_device_or_serial
677        else:
678            return _AndroidDeviceBridge.android_devices()[
679                self._android_device_or_serial]
680
681    def _get_device_log_path(self):
682        return '~/data/iperf_server_port%s.log' % self.port
683
684    def start(self, extra_args='', tag='', iperf_binary=None):
685        """Starts iperf server on an ADB device.
686
687        Args:
688            extra_args: A string representing extra arguments to start iperf
689                server with.
690            tag: Appended to log file name to identify logs from different
691                iperf runs.
692            iperf_binary: Location of iperf3 binary. If none, it is assumed the
693                the binary is in the path.
694        """
695        if self._iperf_process is not None:
696            return
697
698        if not iperf_binary:
699            logging.debug('No iperf3 binary specified.  '
700                          'Assuming iperf3 is in the path.')
701            iperf_binary = 'iperf3'
702        else:
703            logging.debug('Using iperf3 binary located at %s' % iperf_binary)
704        iperf_command = '{} -s -J -p {}'.format(iperf_binary, self.port)
705
706        self._iperf_process = self._android_device.adb.shell_nb(
707            '{cmd} {extra_flags} > {log_file}'.format(
708                cmd=iperf_command,
709                extra_flags=extra_args,
710                log_file=self._get_device_log_path()))
711
712        self._iperf_process_adb_pid = ''
713        while len(self._iperf_process_adb_pid) == 0:
714            self._iperf_process_adb_pid = self._android_device.adb.shell(
715                'pgrep iperf3 -n')
716
717        self._current_tag = tag
718
719    def stop(self):
720        """Stops the iperf server.
721
722        Returns:
723            The name of the log file generated from the terminated session.
724        """
725        if self._iperf_process is None:
726            return
727
728        job.run('kill -9 {}'.format(self._iperf_process.pid))
729
730        # TODO(markdr): update with definitive kill method
731        while True:
732            iperf_process_list = self._android_device.adb.shell('pgrep iperf3')
733            if iperf_process_list.find(self._iperf_process_adb_pid) == -1:
734                break
735            else:
736                self._android_device.adb.shell("kill -9 {}".format(
737                    self._iperf_process_adb_pid))
738
739        iperf_result = self._android_device.adb.shell('cat {}'.format(
740            self._get_device_log_path()))
741
742        log_file = self._get_full_file_path(self._current_tag)
743        with open(log_file, 'w') as f:
744            f.write(iperf_result)
745
746        self._android_device.adb.shell('rm {}'.format(
747            self._get_device_log_path()))
748
749        self._iperf_process = None
750        return log_file
751
752    def get_interface_ip_addresses(self, interface):
753        """Gets all of the ip addresses, ipv4 and ipv6, associated with a
754           particular interface name.
755
756        Args:
757            interface: The interface name on the device, ie eth0
758
759        Returns:
760            A list of dictionaries of the the various IP addresses:
761                ipv4_private_local_addresses: Any 192.168, 172.16, or 10
762                    addresses
763                ipv4_public_addresses: Any IPv4 public addresses
764                ipv6_link_local_addresses: Any fe80:: addresses
765                ipv6_private_local_addresses: Any fd00:: addresses
766                ipv6_public_addresses: Any publicly routable addresses
767        """
768        return utils.get_interface_ip_addresses(self._android_device_or_serial,
769                                                interface)
770