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