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 base64
18import concurrent.futures
19import copy
20import datetime
21import functools
22import ipaddress
23import json
24import logging
25import os
26import platform
27import random
28import re
29import signal
30import string
31import socket
32import subprocess
33import time
34import threading
35import traceback
36import zipfile
37from concurrent.futures import ThreadPoolExecutor
38
39from acts import signals
40from acts.controllers.adb_lib.error import AdbError
41from acts.libs.proc import job
42
43# File name length is limited to 255 chars on some OS, so we need to make sure
44# the file names we output fits within the limit.
45MAX_FILENAME_LEN = 255
46
47
48class ActsUtilsError(Exception):
49    """Generic error raised for exceptions in ACTS utils."""
50
51
52class NexusModelNames:
53    # TODO(angli): This will be fixed later by angli.
54    ONE = 'sprout'
55    N5 = 'hammerhead'
56    N5v2 = 'bullhead'
57    N6 = 'shamu'
58    N6v2 = 'angler'
59    N6v3 = 'marlin'
60    N5v3 = 'sailfish'
61
62
63class DozeModeStatus:
64    ACTIVE = "ACTIVE"
65    IDLE = "IDLE"
66
67
68ascii_letters_and_digits = string.ascii_letters + string.digits
69valid_filename_chars = "-_." + ascii_letters_and_digits
70
71models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg",
72          "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu",
73          "ryu", "marlin", "sailfish")
74
75manufacture_name_to_model = {
76    "flo": "razor",
77    "flo_lte": "razorg",
78    "flounder": "volantis",
79    "flounder_lte": "volantisg",
80    "dragon": "ryu"
81}
82
83GMT_to_olson = {
84    "GMT-9": "America/Anchorage",
85    "GMT-8": "US/Pacific",
86    "GMT-7": "US/Mountain",
87    "GMT-6": "US/Central",
88    "GMT-5": "US/Eastern",
89    "GMT-4": "America/Barbados",
90    "GMT-3": "America/Buenos_Aires",
91    "GMT-2": "Atlantic/South_Georgia",
92    "GMT-1": "Atlantic/Azores",
93    "GMT+0": "Africa/Casablanca",
94    "GMT+1": "Europe/Amsterdam",
95    "GMT+2": "Europe/Athens",
96    "GMT+3": "Europe/Moscow",
97    "GMT+4": "Asia/Baku",
98    "GMT+5": "Asia/Oral",
99    "GMT+6": "Asia/Almaty",
100    "GMT+7": "Asia/Bangkok",
101    "GMT+8": "Asia/Hong_Kong",
102    "GMT+9": "Asia/Tokyo",
103    "GMT+10": "Pacific/Guam",
104    "GMT+11": "Pacific/Noumea",
105    "GMT+12": "Pacific/Fiji",
106    "GMT+13": "Pacific/Tongatapu",
107    "GMT-11": "Pacific/Midway",
108    "GMT-10": "Pacific/Honolulu"
109}
110
111
112def abs_path(path):
113    """Resolve the '.' and '~' in a path to get the absolute path.
114
115    Args:
116        path: The path to expand.
117
118    Returns:
119        The absolute path of the input path.
120    """
121    return os.path.abspath(os.path.expanduser(path))
122
123
124def get_current_epoch_time():
125    """Current epoch time in milliseconds.
126
127    Returns:
128        An integer representing the current epoch time in milliseconds.
129    """
130    return int(round(time.time() * 1000))
131
132
133def get_current_human_time():
134    """Returns the current time in human readable format.
135
136    Returns:
137        The current time stamp in Month-Day-Year Hour:Min:Sec format.
138    """
139    return time.strftime("%m-%d-%Y %H:%M:%S ")
140
141
142def epoch_to_human_time(epoch_time):
143    """Converts an epoch timestamp to human readable time.
144
145    This essentially converts an output of get_current_epoch_time to an output
146    of get_current_human_time
147
148    Args:
149        epoch_time: An integer representing an epoch timestamp in milliseconds.
150
151    Returns:
152        A time string representing the input time.
153        None if input param is invalid.
154    """
155    if isinstance(epoch_time, int):
156        try:
157            d = datetime.datetime.fromtimestamp(epoch_time / 1000)
158            return d.strftime("%m-%d-%Y %H:%M:%S ")
159        except ValueError:
160            return None
161
162
163def get_timezone_olson_id():
164    """Return the Olson ID of the local (non-DST) timezone.
165
166    Returns:
167        A string representing one of the Olson IDs of the local (non-DST)
168        timezone.
169    """
170    tzoffset = int(time.timezone / 3600)
171    gmt = None
172    if tzoffset <= 0:
173        gmt = "GMT+{}".format(-tzoffset)
174    else:
175        gmt = "GMT-{}".format(tzoffset)
176    return GMT_to_olson[gmt]
177
178
179def get_next_device(test_bed_controllers, used_devices):
180    """Gets the next device in a list of testbed controllers
181
182    Args:
183        test_bed_controllers: A list of testbed controllers of a particular
184            type, for example a list ACTS Android devices.
185        used_devices: A list of devices that have been used.  This can be a
186            mix of devices, for example a fuchsia device and an Android device.
187    Returns:
188        The next device in the test_bed_controllers list or None if there are
189        no items that are not in the used devices list.
190    """
191    if test_bed_controllers:
192        device_list = test_bed_controllers
193    else:
194        raise ValueError('test_bed_controllers is empty.')
195    for used_device in used_devices:
196        if used_device in device_list:
197            device_list.remove(used_device)
198    if device_list:
199        return device_list[0]
200    else:
201        return None
202
203
204def find_files(paths, file_predicate):
205    """Locate files whose names and extensions match the given predicate in
206    the specified directories.
207
208    Args:
209        paths: A list of directory paths where to find the files.
210        file_predicate: A function that returns True if the file name and
211          extension are desired.
212
213    Returns:
214        A list of files that match the predicate.
215    """
216    file_list = []
217    if not isinstance(paths, list):
218        paths = [paths]
219    for path in paths:
220        p = abs_path(path)
221        for dirPath, subdirList, fileList in os.walk(p):
222            for fname in fileList:
223                name, ext = os.path.splitext(fname)
224                if file_predicate(name, ext):
225                    file_list.append((dirPath, name, ext))
226    return file_list
227
228
229def load_config(file_full_path, log_errors=True):
230    """Loads a JSON config file.
231
232    Returns:
233        A JSON object.
234    """
235    with open(file_full_path, 'r') as f:
236        try:
237            return json.load(f)
238        except Exception as e:
239            if log_errors:
240                logging.error("Exception error to load %s: %s", f, e)
241            raise
242
243
244def load_file_to_base64_str(f_path):
245    """Loads the content of a file into a base64 string.
246
247    Args:
248        f_path: full path to the file including the file name.
249
250    Returns:
251        A base64 string representing the content of the file in utf-8 encoding.
252    """
253    path = abs_path(f_path)
254    with open(path, 'rb') as f:
255        f_bytes = f.read()
256        base64_str = base64.b64encode(f_bytes).decode("utf-8")
257        return base64_str
258
259
260def dump_string_to_file(content, file_path, mode='w'):
261    """ Dump content of a string to
262
263    Args:
264        content: content to be dumped to file
265        file_path: full path to the file including the file name.
266        mode: file open mode, 'w' (truncating file) by default
267    :return:
268    """
269    full_path = abs_path(file_path)
270    with open(full_path, mode) as f:
271        f.write(content)
272
273
274def list_of_dict_to_dict_of_dict(list_of_dicts, dict_key):
275    """Transforms a list of dicts to a dict of dicts.
276
277    For instance:
278    >>> list_of_dict_to_dict_of_dict([{'a': '1', 'b':'2'},
279    >>>                               {'a': '3', 'b':'4'}],
280    >>>                              'b')
281
282    returns:
283
284    >>> {'2': {'a': '1', 'b':'2'},
285    >>>  '4': {'a': '3', 'b':'4'}}
286
287    Args:
288        list_of_dicts: A list of dictionaries.
289        dict_key: The key in the inner dict to be used as the key for the
290                  outer dict.
291    Returns:
292        A dict of dicts.
293    """
294    return {d[dict_key]: d for d in list_of_dicts}
295
296
297def dict_purge_key_if_value_is_none(dictionary):
298    """Removes all pairs with value None from dictionary."""
299    for k, v in dict(dictionary).items():
300        if v is None:
301            del dictionary[k]
302    return dictionary
303
304
305def find_field(item_list, cond, comparator, target_field):
306    """Finds the value of a field in a dict object that satisfies certain
307    conditions.
308
309    Args:
310        item_list: A list of dict objects.
311        cond: A param that defines the condition.
312        comparator: A function that checks if an dict satisfies the condition.
313        target_field: Name of the field whose value to be returned if an item
314            satisfies the condition.
315
316    Returns:
317        Target value or None if no item satisfies the condition.
318    """
319    for item in item_list:
320        if comparator(item, cond) and target_field in item:
321            return item[target_field]
322    return None
323
324
325def rand_ascii_str(length):
326    """Generates a random string of specified length, composed of ascii letters
327    and digits.
328
329    Args:
330        length: The number of characters in the string.
331
332    Returns:
333        The random string generated.
334    """
335    letters = [random.choice(ascii_letters_and_digits) for i in range(length)]
336    return ''.join(letters)
337
338
339def rand_hex_str(length):
340    """Generates a random string of specified length, composed of hex digits
341
342    Args:
343        length: The number of characters in the string.
344
345    Returns:
346        The random string generated.
347    """
348    letters = [random.choice(string.hexdigits) for i in range(length)]
349    return ''.join(letters)
350
351
352# Thead/Process related functions.
353def concurrent_exec(func, param_list):
354    """Executes a function with different parameters pseudo-concurrently.
355
356    This is basically a map function. Each element (should be an iterable) in
357    the param_list is unpacked and passed into the function. Due to Python's
358    GIL, there's no true concurrency. This is suited for IO-bound tasks.
359
360    Args:
361        func: The function that parforms a task.
362        param_list: A list of iterables, each being a set of params to be
363            passed into the function.
364
365    Returns:
366        A list of return values from each function execution. If an execution
367        caused an exception, the exception object will be the corresponding
368        result.
369    """
370    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
371        # Start the load operations and mark each future with its params
372        future_to_params = {executor.submit(func, *p): p for p in param_list}
373        return_vals = []
374        for future in concurrent.futures.as_completed(future_to_params):
375            params = future_to_params[future]
376            try:
377                return_vals.append(future.result())
378            except Exception as exc:
379                print("{} generated an exception: {}".format(
380                    params, traceback.format_exc()))
381                return_vals.append(exc)
382        return return_vals
383
384
385def exe_cmd(*cmds):
386    """Executes commands in a new shell.
387
388    Args:
389        cmds: A sequence of commands and arguments.
390
391    Returns:
392        The output of the command run.
393
394    Raises:
395        OSError is raised if an error occurred during the command execution.
396    """
397    cmd = ' '.join(cmds)
398    proc = subprocess.Popen(cmd,
399                            stdout=subprocess.PIPE,
400                            stderr=subprocess.PIPE,
401                            shell=True)
402    (out, err) = proc.communicate()
403    if not err:
404        return out
405    raise OSError(err)
406
407
408def require_sl4a(android_devices):
409    """Makes sure sl4a connection is established on the given AndroidDevice
410    objects.
411
412    Args:
413        android_devices: A list of AndroidDevice objects.
414
415    Raises:
416        AssertionError is raised if any given android device does not have SL4A
417        connection established.
418    """
419    for ad in android_devices:
420        msg = "SL4A connection not established properly on %s." % ad.serial
421        assert ad.droid, msg
422
423
424def _assert_subprocess_running(proc):
425    """Checks if a subprocess has terminated on its own.
426
427    Args:
428        proc: A subprocess returned by subprocess.Popen.
429
430    Raises:
431        ActsUtilsError is raised if the subprocess has stopped.
432    """
433    ret = proc.poll()
434    if ret is not None:
435        out, err = proc.communicate()
436        raise ActsUtilsError("Process %d has terminated. ret: %d, stderr: %s,"
437                             " stdout: %s" % (proc.pid, ret, err, out))
438
439
440def start_standing_subprocess(cmd, check_health_delay=0, shell=True):
441    """Starts a long-running subprocess.
442
443    This is not a blocking call and the subprocess started by it should be
444    explicitly terminated with stop_standing_subprocess.
445
446    For short-running commands, you should use exe_cmd, which blocks.
447
448    You can specify a health check after the subprocess is started to make sure
449    it did not stop prematurely.
450
451    Args:
452        cmd: string, the command to start the subprocess with.
453        check_health_delay: float, the number of seconds to wait after the
454                            subprocess starts to check its health. Default is 0,
455                            which means no check.
456
457    Returns:
458        The subprocess that got started.
459    """
460    proc = subprocess.Popen(cmd,
461                            stdout=subprocess.PIPE,
462                            stderr=subprocess.PIPE,
463                            shell=shell,
464                            preexec_fn=os.setpgrp)
465    logging.debug("Start standing subprocess with cmd: %s", cmd)
466    if check_health_delay > 0:
467        time.sleep(check_health_delay)
468        _assert_subprocess_running(proc)
469    return proc
470
471
472def stop_standing_subprocess(proc, kill_signal=signal.SIGTERM):
473    """Stops a subprocess started by start_standing_subprocess.
474
475    Before killing the process, we check if the process is running, if it has
476    terminated, ActsUtilsError is raised.
477
478    Catches and ignores the PermissionError which only happens on Macs.
479
480    Args:
481        proc: Subprocess to terminate.
482    """
483    pid = proc.pid
484    logging.debug("Stop standing subprocess %d", pid)
485    _assert_subprocess_running(proc)
486    try:
487        os.killpg(pid, kill_signal)
488    except PermissionError:
489        pass
490
491
492def wait_for_standing_subprocess(proc, timeout=None):
493    """Waits for a subprocess started by start_standing_subprocess to finish
494    or times out.
495
496    Propagates the exception raised by the subprocess.wait(.) function.
497    The subprocess.TimeoutExpired exception is raised if the process timed-out
498    rather then terminating.
499
500    If no exception is raised: the subprocess terminated on its own. No need
501    to call stop_standing_subprocess() to kill it.
502
503    If an exception is raised: the subprocess is still alive - it did not
504    terminate. Either call stop_standing_subprocess() to kill it, or call
505    wait_for_standing_subprocess() to keep waiting for it to terminate on its
506    own.
507
508    Args:
509        p: Subprocess to wait for.
510        timeout: An integer number of seconds to wait before timing out.
511    """
512    proc.wait(timeout)
513
514
515def sync_device_time(ad):
516    """Sync the time of an android device with the current system time.
517
518    Both epoch time and the timezone will be synced.
519
520    Args:
521        ad: The android device to sync time on.
522    """
523    ad.adb.shell("settings put global auto_time 0", ignore_status=True)
524    ad.adb.shell("settings put global auto_time_zone 0", ignore_status=True)
525    droid = ad.droid
526    droid.setTimeZone(get_timezone_olson_id())
527    droid.setTime(get_current_epoch_time())
528
529
530# Timeout decorator block
531class TimeoutError(Exception):
532    """Exception for timeout decorator related errors.
533    """
534    pass
535
536
537def _timeout_handler(signum, frame):
538    """Handler function used by signal to terminate a timed out function.
539    """
540    raise TimeoutError()
541
542
543def timeout(sec):
544    """A decorator used to add time out check to a function.
545
546    This only works in main thread due to its dependency on signal module.
547    Do NOT use it if the decorated funtion does not run in the Main thread.
548
549    Args:
550        sec: Number of seconds to wait before the function times out.
551            No timeout if set to 0
552
553    Returns:
554        What the decorated function returns.
555
556    Raises:
557        TimeoutError is raised when time out happens.
558    """
559
560    def decorator(func):
561        @functools.wraps(func)
562        def wrapper(*args, **kwargs):
563            if sec:
564                signal.signal(signal.SIGALRM, _timeout_handler)
565                signal.alarm(sec)
566            try:
567                return func(*args, **kwargs)
568            except TimeoutError:
569                raise TimeoutError(("Function {} timed out after {} "
570                                    "seconds.").format(func.__name__, sec))
571            finally:
572                signal.alarm(0)
573
574        return wrapper
575
576    return decorator
577
578
579def trim_model_name(model):
580    """Trim any prefix and postfix and return the android designation of the
581    model name.
582
583    e.g. "m_shamu" will be trimmed to "shamu".
584
585    Args:
586        model: model name to be trimmed.
587
588    Returns
589        Trimmed model name if one of the known model names is found.
590        None otherwise.
591    """
592    # Directly look up first.
593    if model in models:
594        return model
595    if model in manufacture_name_to_model:
596        return manufacture_name_to_model[model]
597    # If not found, try trimming off prefix/postfix and look up again.
598    tokens = re.split("_|-", model)
599    for t in tokens:
600        if t in models:
601            return t
602        if t in manufacture_name_to_model:
603            return manufacture_name_to_model[t]
604    return None
605
606
607def force_airplane_mode(ad, new_state, timeout_value=60):
608    """Force the device to set airplane mode on or off by adb shell command.
609
610    Args:
611        ad: android device object.
612        new_state: Turn on airplane mode if True.
613            Turn off airplane mode if False.
614        timeout_value: max wait time for 'adb wait-for-device'
615
616    Returns:
617        True if success.
618        False if timeout.
619    """
620
621    # Using timeout decorator.
622    # Wait for device with timeout. If after <timeout_value> seconds, adb
623    # is still waiting for device, throw TimeoutError exception.
624    @timeout(timeout_value)
625    def wait_for_device_with_timeout(ad):
626        ad.adb.wait_for_device()
627
628    try:
629        wait_for_device_with_timeout(ad)
630        ad.adb.shell("settings put global airplane_mode_on {}".format(
631            1 if new_state else 0))
632        ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE")
633    except TimeoutError:
634        # adb wait for device timeout
635        return False
636    return True
637
638
639def get_battery_level(ad):
640    """Gets battery level from device
641
642    Returns:
643        battery_level: int indicating battery level
644    """
645    output = ad.adb.shell("dumpsys battery")
646    match = re.search(r"level: (?P<battery_level>\S+)", output)
647    battery_level = int(match.group("battery_level"))
648    return battery_level
649
650
651def get_device_usb_charging_status(ad):
652    """ Returns the usb charging status of the device.
653
654    Args:
655        ad: android device object
656
657    Returns:
658        True if charging
659        False if not charging
660     """
661    adb_shell_result = ad.adb.shell("dumpsys deviceidle get charging")
662    ad.log.info("Device Charging State: {}".format(adb_shell_result))
663    return adb_shell_result == 'true'
664
665
666def disable_usb_charging(ad):
667    """ Unplug device from usb charging.
668
669    Args:
670        ad: android device object
671
672    Returns:
673        True if device is unplugged
674        False otherwise
675    """
676    ad.adb.shell("dumpsys battery unplug")
677    if not get_device_usb_charging_status(ad):
678        return True
679    else:
680        ad.log.info("Could not disable USB charging")
681        return False
682
683
684def enable_usb_charging(ad):
685    """ Plug device to usb charging.
686
687    Args:
688        ad: android device object
689
690    Returns:
691        True if device is Plugged
692        False otherwise
693    """
694    ad.adb.shell("dumpsys battery reset")
695    if get_device_usb_charging_status(ad):
696        return True
697    else:
698        ad.log.info("Could not enable USB charging")
699        return False
700
701
702def enable_doze(ad):
703    """Force the device into doze mode.
704
705    Args:
706        ad: android device object.
707
708    Returns:
709        True if device is in doze mode.
710        False otherwise.
711    """
712    ad.adb.shell("dumpsys battery unplug")
713    ad.adb.shell("dumpsys deviceidle enable")
714    ad.adb.shell("dumpsys deviceidle force-idle")
715    ad.droid.goToSleepNow()
716    time.sleep(5)
717    adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep")
718    if not adb_shell_result.startswith(DozeModeStatus.IDLE):
719        info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result))
720        print(info)
721        return False
722    return True
723
724
725def disable_doze(ad):
726    """Force the device not in doze mode.
727
728    Args:
729        ad: android device object.
730
731    Returns:
732        True if device is not in doze mode.
733        False otherwise.
734    """
735    ad.adb.shell("dumpsys deviceidle disable")
736    ad.adb.shell("dumpsys battery reset")
737    adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep")
738    if not adb_shell_result.startswith(DozeModeStatus.ACTIVE):
739        info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result))
740        print(info)
741        return False
742    return True
743
744
745def enable_doze_light(ad):
746    """Force the device into doze light mode.
747
748    Args:
749        ad: android device object.
750
751    Returns:
752        True if device is in doze light mode.
753        False otherwise.
754    """
755    ad.adb.shell("dumpsys battery unplug")
756    ad.droid.goToSleepNow()
757    time.sleep(5)
758    ad.adb.shell("cmd deviceidle enable light")
759    ad.adb.shell("cmd deviceidle step light")
760    adb_shell_result = ad.adb.shell("dumpsys deviceidle get light")
761    if not adb_shell_result.startswith(DozeModeStatus.IDLE):
762        info = ("dumpsys deviceidle get light: {}".format(adb_shell_result))
763        print(info)
764        return False
765    return True
766
767
768def disable_doze_light(ad):
769    """Force the device not in doze light mode.
770
771    Args:
772        ad: android device object.
773
774    Returns:
775        True if device is not in doze light mode.
776        False otherwise.
777    """
778    ad.adb.shell("dumpsys battery reset")
779    ad.adb.shell("cmd deviceidle disable light")
780    adb_shell_result = ad.adb.shell("dumpsys deviceidle get light")
781    if not adb_shell_result.startswith(DozeModeStatus.ACTIVE):
782        info = ("dumpsys deviceidle get light: {}".format(adb_shell_result))
783        print(info)
784        return False
785    return True
786
787
788def set_ambient_display(ad, new_state):
789    """Set "Ambient Display" in Settings->Display
790
791    Args:
792        ad: android device object.
793        new_state: new state for "Ambient Display". True or False.
794    """
795    ad.adb.shell(
796        "settings put secure doze_enabled {}".format(1 if new_state else 0))
797
798
799def set_adaptive_brightness(ad, new_state):
800    """Set "Adaptive Brightness" in Settings->Display
801
802    Args:
803        ad: android device object.
804        new_state: new state for "Adaptive Brightness". True or False.
805    """
806    ad.adb.shell("settings put system screen_brightness_mode {}".format(
807        1 if new_state else 0))
808
809
810def set_auto_rotate(ad, new_state):
811    """Set "Auto-rotate" in QuickSetting
812
813    Args:
814        ad: android device object.
815        new_state: new state for "Auto-rotate". True or False.
816    """
817    ad.adb.shell("settings put system accelerometer_rotation {}".format(
818        1 if new_state else 0))
819
820
821def set_location_service(ad, new_state):
822    """Set Location service on/off in Settings->Location
823
824    Args:
825        ad: android device object.
826        new_state: new state for "Location service".
827            If new_state is False, turn off location service.
828            If new_state if True, set location service to "High accuracy".
829    """
830    ad.adb.shell("content insert --uri "
831                 " content://com.google.settings/partner --bind "
832                 "name:s:network_location_opt_in --bind value:s:1")
833    ad.adb.shell("content insert --uri "
834                 " content://com.google.settings/partner --bind "
835                 "name:s:use_location_for_services --bind value:s:1")
836    if new_state:
837        ad.adb.shell("settings put secure location_mode 3")
838    else:
839        ad.adb.shell("settings put secure location_mode 0")
840
841
842def set_mobile_data_always_on(ad, new_state):
843    """Set Mobile_Data_Always_On feature bit
844
845    Args:
846        ad: android device object.
847        new_state: new state for "mobile_data_always_on"
848            if new_state is False, set mobile_data_always_on disabled.
849            if new_state if True, set mobile_data_always_on enabled.
850    """
851    ad.adb.shell("settings put global mobile_data_always_on {}".format(
852        1 if new_state else 0))
853
854
855def bypass_setup_wizard(ad):
856    """Bypass the setup wizard on an input Android device
857
858    Args:
859        ad: android device object.
860
861    Returns:
862        True if Android device successfully bypassed the setup wizard.
863        False if failed.
864    """
865    try:
866        ad.adb.shell("am start -n \"com.google.android.setupwizard/"
867                     ".SetupWizardExitActivity\"")
868        logging.debug("No error during default bypass call.")
869    except AdbError as adb_error:
870        if adb_error.stdout == "ADB_CMD_OUTPUT:0":
871            if adb_error.stderr and \
872                    not adb_error.stderr.startswith("Error type 3\n"):
873                logging.error("ADB_CMD_OUTPUT:0, but error is %s " %
874                              adb_error.stderr)
875                raise adb_error
876            logging.debug("Bypass wizard call received harmless error 3: "
877                          "No setup to bypass.")
878        elif adb_error.stdout == "ADB_CMD_OUTPUT:255":
879            # Run it again as root.
880            ad.adb.root_adb()
881            logging.debug("Need root access to bypass setup wizard.")
882            try:
883                ad.adb.shell("am start -n \"com.google.android.setupwizard/"
884                             ".SetupWizardExitActivity\"")
885                logging.debug("No error during rooted bypass call.")
886            except AdbError as adb_error:
887                if adb_error.stdout == "ADB_CMD_OUTPUT:0":
888                    if adb_error.stderr and \
889                            not adb_error.stderr.startswith("Error type 3\n"):
890                        logging.error("Rooted ADB_CMD_OUTPUT:0, but error is "
891                                      "%s " % adb_error.stderr)
892                        raise adb_error
893                    logging.debug(
894                        "Rooted bypass wizard call received harmless "
895                        "error 3: No setup to bypass.")
896
897    # magical sleep to wait for the gservices override broadcast to complete
898    time.sleep(3)
899
900    provisioned_state = int(
901        ad.adb.shell("settings get global device_provisioned"))
902    if provisioned_state != 1:
903        logging.error("Failed to bypass setup wizard.")
904        return False
905    logging.debug("Setup wizard successfully bypassed.")
906    return True
907
908
909def parse_ping_ouput(ad, count, out, loss_tolerance=20):
910    """Ping Parsing util.
911
912    Args:
913        ad: Android Device Object.
914        count: Number of ICMP packets sent
915        out: shell output text of ping operation
916        loss_tolerance: Threshold after which flag test as false
917    Returns:
918        False: if packet loss is more than loss_tolerance%
919        True: if all good
920    """
921    result = re.search(
922        r"(\d+) packets transmitted, (\d+) received, (\d+)% packet loss", out)
923    if not result:
924        ad.log.info("Ping failed with %s", out)
925        return False
926
927    packet_loss = int(result.group(3))
928    packet_xmit = int(result.group(1))
929    packet_rcvd = int(result.group(2))
930    min_packet_xmit_rcvd = (100 - loss_tolerance) * 0.01
931    if (packet_loss > loss_tolerance
932            or packet_xmit < count * min_packet_xmit_rcvd
933            or packet_rcvd < count * min_packet_xmit_rcvd):
934        ad.log.error("%s, ping failed with loss more than tolerance %s%%",
935                     result.group(0), loss_tolerance)
936        return False
937    ad.log.info("Ping succeed with %s", result.group(0))
938    return True
939
940
941def adb_shell_ping(ad,
942                   count=120,
943                   dest_ip="www.google.com",
944                   timeout=200,
945                   loss_tolerance=20):
946    """Ping utility using adb shell.
947
948    Args:
949        ad: Android Device Object.
950        count: Number of ICMP packets to send
951        dest_ip: hostname or IP address
952                 default www.google.com
953        timeout: timeout for icmp pings to complete.
954    """
955    if is_valid_ipv6_address(dest_ip):
956        ping_cmd = "ping6 -W 1"
957    else:
958        ping_cmd = "ping -W 1"
959    if count:
960        ping_cmd += " -c %d" % count
961    if dest_ip:
962        ping_cmd += " %s" % dest_ip
963    try:
964        ad.log.info("Starting ping test to %s using adb command %s", dest_ip,
965                    ping_cmd)
966        out = ad.adb.shell(ping_cmd, timeout=timeout, ignore_status=True)
967        if not parse_ping_ouput(ad, count, out, loss_tolerance):
968            return False
969        return True
970    except Exception as e:
971        ad.log.warning("Ping Test to %s failed with exception %s", dest_ip, e)
972        return False
973
974
975def unzip_maintain_permissions(zip_path, extract_location):
976    """Unzip a .zip file while maintaining permissions.
977
978    Args:
979        zip_path: The path to the zipped file.
980        extract_location: the directory to extract to.
981    """
982    with zipfile.ZipFile(zip_path, 'r') as zip_file:
983        for info in zip_file.infolist():
984            _extract_file(zip_file, info, extract_location)
985
986
987def _extract_file(zip_file, zip_info, extract_location):
988    """Extracts a single entry from a ZipFile while maintaining permissions.
989
990    Args:
991        zip_file: A zipfile.ZipFile.
992        zip_info: A ZipInfo object from zip_file.
993        extract_location: The directory to extract to.
994    """
995    out_path = zip_file.extract(zip_info.filename, path=extract_location)
996    perm = zip_info.external_attr >> 16
997    os.chmod(out_path, perm)
998
999
1000def get_directory_size(path):
1001    """Computes the total size of the files in a directory, including subdirectories.
1002
1003    Args:
1004        path: The path of the directory.
1005    Returns:
1006        The size of the provided directory.
1007    """
1008    total = 0
1009    for dirpath, dirnames, filenames in os.walk(path):
1010        for filename in filenames:
1011            total += os.path.getsize(os.path.join(dirpath, filename))
1012    return total
1013
1014
1015def get_command_uptime(command_regex):
1016    """Returns the uptime for a given command.
1017
1018    Args:
1019        command_regex: A regex that matches the command line given. Must be
1020            pgrep compatible.
1021    """
1022    pid = job.run('pgrep -f %s' % command_regex).stdout
1023    runtime = ''
1024    if pid:
1025        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
1026    return runtime
1027
1028
1029def get_process_uptime(process):
1030    """Returns the runtime in [[dd-]hh:]mm:ss, or '' if not running."""
1031    pid = job.run('pidof %s' % process, ignore_status=True).stdout
1032    runtime = ''
1033    if pid:
1034        runtime = job.run('ps -o etime= -p "%s"' % pid).stdout
1035    return runtime
1036
1037
1038def get_device_process_uptime(adb, process):
1039    """Returns the uptime of a device process."""
1040    pid = adb.shell('pidof %s' % process, ignore_status=True)
1041    runtime = ''
1042    if pid:
1043        runtime = adb.shell('ps -o etime= -p "%s"' % pid)
1044    return runtime
1045
1046
1047def wait_until(func, timeout_s, condition=True, sleep_s=1.0):
1048    """Executes a function repeatedly until condition is met.
1049
1050    Args:
1051      func: The function pointer to execute.
1052      timeout_s: Amount of time (in seconds) to wait before raising an
1053                 exception.
1054      condition: The ending condition of the WaitUntil loop.
1055      sleep_s: The amount of time (in seconds) to sleep between each function
1056               execution.
1057
1058    Returns:
1059      The time in seconds before detecting a successful condition.
1060
1061    Raises:
1062      TimeoutError: If the condition was never met and timeout is hit.
1063    """
1064    start_time = time.time()
1065    end_time = start_time + timeout_s
1066    count = 0
1067    while True:
1068        count += 1
1069        if func() == condition:
1070            return time.time() - start_time
1071        if time.time() > end_time:
1072            break
1073        time.sleep(sleep_s)
1074    raise TimeoutError('Failed to complete function %s in %d seconds having '
1075                       'attempted %d times.' % (str(func), timeout_s, count))
1076
1077
1078# Adapted from
1079# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python
1080# Available under the Creative Commons Attribution-ShareAlike License
1081def levenshtein(string1, string2):
1082    """Returns the Levenshtein distance of two strings.
1083    Uses Dynamic Programming approach, only keeping track of
1084    two rows of the DP table at a time.
1085
1086    Args:
1087      string1: String to compare to string2
1088      string2: String to compare to string1
1089
1090    Returns:
1091      distance: the Levenshtein distance between string1 and string2
1092    """
1093
1094    if len(string1) < len(string2):
1095        return levenshtein(string2, string1)
1096
1097    if len(string2) == 0:
1098        return len(string1)
1099
1100    previous_row = range(len(string2) + 1)
1101    for i, char1 in enumerate(string1):
1102        current_row = [i + 1]
1103        for j, char2 in enumerate(string2):
1104            insertions = previous_row[j + 1] + 1
1105            deletions = current_row[j] + 1
1106            substitutions = previous_row[j] + (char1 != char2)
1107            current_row.append(min(insertions, deletions, substitutions))
1108        previous_row = current_row
1109
1110    return previous_row[-1]
1111
1112
1113def string_similarity(s1, s2):
1114    """Returns a similarity measurement based on Levenshtein distance.
1115
1116    Args:
1117      s1: the string to compare to s2
1118      s2: the string to compare to s1
1119
1120    Returns:
1121      result: the similarity metric
1122    """
1123    lev = levenshtein(s1, s2)
1124    try:
1125        lev_ratio = float(lev) / max(len(s1), len(s2))
1126        result = (1.0 - lev_ratio) * 100
1127    except ZeroDivisionError:
1128        result = 100 if not s2 else 0
1129    return float(result)
1130
1131
1132def run_concurrent_actions_no_raise(*calls):
1133    """Concurrently runs all callables passed in using multithreading.
1134
1135    Example:
1136
1137    >>> def test_function_1(arg1, arg2):
1138    >>>     return arg1, arg2
1139    >>>
1140    >>> def test_function_2(arg1, kwarg='kwarg'):
1141    >>>     raise arg1(kwarg)
1142    >>>
1143    >>> run_concurrent_actions_no_raise(
1144    >>>     lambda: test_function_1('arg1', 'arg2'),
1145    >>>     lambda: test_function_2(IndexError, kwarg='kwarg'),
1146    >>> )
1147    >>> # Output:
1148    >>> [('arg1', 'arg2'), IndexError('kwarg')]
1149
1150    Args:
1151        *calls: A *args list of argumentless callable objects to be called. Note
1152            that if a function has arguments it can be turned into an
1153            argumentless function via the lambda keyword or functools.partial.
1154
1155    Returns:
1156        An array of the returned values or exceptions received from calls,
1157        respective of the order given.
1158    """
1159    with ThreadPoolExecutor(max_workers=len(calls)) as executor:
1160        futures = [executor.submit(call) for call in calls]
1161
1162    results = []
1163    for future in futures:
1164        try:
1165            results.append(future.result())
1166        except Exception as e:
1167            results.append(e)
1168    return results
1169
1170
1171def run_concurrent_actions(*calls):
1172    """Runs all callables passed in concurrently using multithreading.
1173
1174    Examples:
1175
1176    >>> def test_function_1(arg1, arg2):
1177    >>>     print(arg1, arg2)
1178    >>>
1179    >>> def test_function_2(arg1, kwarg='kwarg'):
1180    >>>     raise arg1(kwarg)
1181    >>>
1182    >>> run_concurrent_actions(
1183    >>>     lambda: test_function_1('arg1', 'arg2'),
1184    >>>     lambda: test_function_2(IndexError, kwarg='kwarg'),
1185    >>> )
1186    >>> 'The above line raises IndexError("kwarg")'
1187
1188    Args:
1189        *calls: A *args list of argumentless callable objects to be called. Note
1190            that if a function has arguments it can be turned into an
1191            argumentless function via the lambda keyword or functools.partial.
1192
1193    Returns:
1194        An array of the returned values respective of the order of the calls
1195        argument.
1196
1197    Raises:
1198        If an exception is raised in any of the calls, the first exception
1199        caught will be raised.
1200    """
1201    first_exception = None
1202
1203    class WrappedException(Exception):
1204        """Raised when a passed-in callable raises an exception."""
1205
1206    def call_wrapper(call):
1207        nonlocal first_exception
1208
1209        try:
1210            return call()
1211        except Exception as e:
1212            logging.exception(e)
1213            # Note that there is a potential race condition between two
1214            # exceptions setting first_exception. Even if a locking mechanism
1215            # was added to prevent this from happening, it is still possible
1216            # that we capture the second exception as the first exception, as
1217            # the active thread can swap to the thread that raises the second
1218            # exception. There is no way to solve this with the tools we have
1219            # here, so we do not bother. The effects this issue has on the
1220            # system as a whole are negligible.
1221            if first_exception is None:
1222                first_exception = e
1223            raise WrappedException(e)
1224
1225    with ThreadPoolExecutor(max_workers=len(calls)) as executor:
1226        futures = [executor.submit(call_wrapper, call) for call in calls]
1227
1228    results = []
1229    for future in futures:
1230        try:
1231            results.append(future.result())
1232        except WrappedException:
1233            # We do not need to raise here, since first_exception will already
1234            # be set to the first exception raised by these callables.
1235            break
1236
1237    if first_exception:
1238        raise first_exception
1239
1240    return results
1241
1242
1243def test_concurrent_actions(*calls, failure_exceptions=(Exception, )):
1244    """Concurrently runs all passed in calls using multithreading.
1245
1246    If any callable raises an Exception found within failure_exceptions, the
1247    test case is marked as a failure.
1248
1249    Example:
1250    >>> def test_function_1(arg1, arg2):
1251    >>>     print(arg1, arg2)
1252    >>>
1253    >>> def test_function_2(kwarg='kwarg'):
1254    >>>     raise IndexError(kwarg)
1255    >>>
1256    >>> test_concurrent_actions(
1257    >>>     lambda: test_function_1('arg1', 'arg2'),
1258    >>>     lambda: test_function_2(kwarg='kwarg'),
1259    >>>     failure_exceptions=IndexError
1260    >>> )
1261    >>> 'raises signals.TestFailure due to IndexError being raised.'
1262
1263    Args:
1264        *calls: A *args list of argumentless callable objects to be called. Note
1265            that if a function has arguments it can be turned into an
1266            argumentless function via the lambda keyword or functools.partial.
1267        failure_exceptions: A tuple of all possible Exceptions that will mark
1268            the test as a FAILURE. Any exception that is not in this list will
1269            mark the tests as UNKNOWN.
1270
1271    Returns:
1272        An array of the returned values respective of the order of the calls
1273        argument.
1274
1275    Raises:
1276        signals.TestFailure if any call raises an Exception.
1277    """
1278    try:
1279        return run_concurrent_actions(*calls)
1280    except signals.TestFailure:
1281        # Do not modify incoming test failures
1282        raise
1283    except failure_exceptions as e:
1284        raise signals.TestFailure(e)
1285
1286
1287class SuppressLogOutput(object):
1288    """Context manager used to suppress all logging output for the specified
1289    logger and level(s).
1290    """
1291
1292    def __init__(self, logger=logging.getLogger(), log_levels=None):
1293        """Create a SuppressLogOutput context manager
1294
1295        Args:
1296            logger: The logger object to suppress
1297            log_levels: Levels of log handlers to disable.
1298        """
1299
1300        self._logger = logger
1301        self._log_levels = log_levels or [
1302            logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
1303            logging.CRITICAL
1304        ]
1305        if isinstance(self._log_levels, int):
1306            self._log_levels = [self._log_levels]
1307        self._handlers = copy.copy(self._logger.handlers)
1308
1309    def __enter__(self):
1310        for handler in self._handlers:
1311            if handler.level in self._log_levels:
1312                self._logger.removeHandler(handler)
1313        return self
1314
1315    def __exit__(self, *_):
1316        for handler in self._handlers:
1317            self._logger.addHandler(handler)
1318
1319
1320class BlockingTimer(object):
1321    """Context manager used to block until a specified amount of time has
1322     elapsed.
1323     """
1324
1325    def __init__(self, secs):
1326        """Initializes a BlockingTimer
1327
1328        Args:
1329            secs: Number of seconds to wait before exiting
1330        """
1331        self._thread = threading.Timer(secs, lambda: None)
1332
1333    def __enter__(self):
1334        self._thread.start()
1335        return self
1336
1337    def __exit__(self, *_):
1338        self._thread.join()
1339
1340
1341def is_valid_ipv4_address(address):
1342    try:
1343        socket.inet_pton(socket.AF_INET, address)
1344    except AttributeError:  # no inet_pton here, sorry
1345        try:
1346            socket.inet_aton(address)
1347        except socket.error:
1348            return False
1349        return address.count('.') == 3
1350    except socket.error:  # not a valid address
1351        return False
1352
1353    return True
1354
1355
1356def is_valid_ipv6_address(address):
1357    if '%' in address:
1358        address = address.split('%')[0]
1359    try:
1360        socket.inet_pton(socket.AF_INET6, address)
1361    except socket.error:  # not a valid address
1362        return False
1363    return True
1364
1365
1366def merge_dicts(*dict_args):
1367    """ Merges args list of dictionaries into a single dictionary.
1368
1369    Args:
1370        dict_args: an args list of dictionaries to be merged. If multiple
1371            dictionaries share a key, the last in the list will appear in the
1372            final result.
1373    """
1374    result = {}
1375    for dictionary in dict_args:
1376        result.update(dictionary)
1377    return result
1378
1379
1380def ascii_string(uc_string):
1381    """Converts unicode string to ascii"""
1382    return str(uc_string).encode('ASCII')
1383
1384
1385def get_interface_ip_addresses(comm_channel, interface):
1386    """Gets all of the ip addresses, ipv4 and ipv6, associated with a
1387       particular interface name.
1388
1389    Args:
1390        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
1391            etc.  Must have the run function implemented.
1392        interface: The interface name on the device, ie eth0
1393
1394    Returns:
1395        A list of dictionaries of the the various IP addresses:
1396            ipv4_private_local_addresses: Any 192.168, 172.16, 10, or 169.254
1397                addresses
1398            ipv4_public_addresses: Any IPv4 public addresses
1399            ipv6_link_local_addresses: Any fe80:: addresses
1400            ipv6_private_local_addresses: Any fd00:: addresses
1401            ipv6_public_addresses: Any publicly routable addresses
1402    """
1403    # Local imports are used here to prevent cyclic dependency.
1404    from acts.controllers.android_device import AndroidDevice
1405    from acts.controllers.fuchsia_device import FuchsiaDevice
1406    from acts.controllers.utils_lib.ssh.connection import SshConnection
1407    ipv4_private_local_addresses = []
1408    ipv4_public_addresses = []
1409    ipv6_link_local_addresses = []
1410    ipv6_private_local_addresses = []
1411    ipv6_public_addresses = []
1412    is_local = comm_channel == job
1413    if type(comm_channel) is AndroidDevice:
1414        all_interfaces_and_addresses = comm_channel.adb.shell(
1415            'ip -o addr | awk \'!/^[0-9]*: ?lo|link\/ether/ {gsub("/", " "); '
1416            'print $2" "$4}\'')
1417        ifconfig_output = comm_channel.adb.shell('ifconfig %s' % interface)
1418    elif (type(comm_channel) is SshConnection or is_local):
1419        all_interfaces_and_addresses = comm_channel.run(
1420            'ip -o addr | awk \'!/^[0-9]*: ?lo|link\/ether/ {gsub("/", " "); '
1421            'print $2" "$4}\'').stdout
1422        ifconfig_output = comm_channel.run('ifconfig %s' % interface).stdout
1423    elif type(comm_channel) is FuchsiaDevice:
1424        all_interfaces_and_addresses = []
1425        comm_channel.netstack_lib.init()
1426        interfaces = comm_channel.netstack_lib.netstackListInterfaces()
1427        if interfaces.get('error') is not None:
1428            raise ActsUtilsError('Failed with {}'.format(
1429                interfaces.get('error')))
1430        for item in interfaces.get('result'):
1431            for ipv4_address in item['ipv4_addresses']:
1432                ipv4_address = '.'.join(map(str, ipv4_address))
1433                all_interfaces_and_addresses.append(
1434                    '%s %s' % (item['name'], ipv4_address))
1435            for ipv6_address in item['ipv6_addresses']:
1436                converted_ipv6_address = []
1437                for octet in ipv6_address:
1438                    converted_ipv6_address.append(format(octet, 'x').zfill(2))
1439                ipv6_address = ''.join(converted_ipv6_address)
1440                ipv6_address = (':'.join(
1441                    ipv6_address[i:i + 4]
1442                    for i in range(0, len(ipv6_address), 4)))
1443                all_interfaces_and_addresses.append(
1444                    '%s %s' %
1445                    (item['name'], str(ipaddress.ip_address(ipv6_address))))
1446        all_interfaces_and_addresses = '\n'.join(all_interfaces_and_addresses)
1447        ifconfig_output = all_interfaces_and_addresses
1448    else:
1449        raise ValueError('Unsupported method to send command to device.')
1450
1451    for interface_line in all_interfaces_and_addresses.split('\n'):
1452        if interface != interface_line.split()[0]:
1453            continue
1454        on_device_ip = ipaddress.ip_address(interface_line.split()[1])
1455        if on_device_ip.version == 4:
1456            if on_device_ip.is_private:
1457                if str(on_device_ip) in ifconfig_output:
1458                    ipv4_private_local_addresses.append(str(on_device_ip))
1459            elif on_device_ip.is_global or (
1460                    # Carrier private doesn't have a property, so we check if
1461                    # all other values are left unset.
1462                    not on_device_ip.is_reserved
1463                    and not on_device_ip.is_unspecified
1464                    and not on_device_ip.is_link_local
1465                    and not on_device_ip.is_loopback
1466                    and not on_device_ip.is_multicast):
1467                if str(on_device_ip) in ifconfig_output:
1468                    ipv4_public_addresses.append(str(on_device_ip))
1469        elif on_device_ip.version == 6:
1470            if on_device_ip.is_link_local:
1471                if str(on_device_ip) in ifconfig_output:
1472                    ipv6_link_local_addresses.append(str(on_device_ip))
1473            elif on_device_ip.is_private:
1474                if str(on_device_ip) in ifconfig_output:
1475                    ipv6_private_local_addresses.append(str(on_device_ip))
1476            elif on_device_ip.is_global:
1477                if str(on_device_ip) in ifconfig_output:
1478                    ipv6_public_addresses.append(str(on_device_ip))
1479    return {
1480        'ipv4_private': ipv4_private_local_addresses,
1481        'ipv4_public': ipv4_public_addresses,
1482        'ipv6_link_local': ipv6_link_local_addresses,
1483        'ipv6_private_local': ipv6_private_local_addresses,
1484        'ipv6_public': ipv6_public_addresses
1485    }
1486
1487
1488def get_interface_based_on_ip(comm_channel, desired_ip_address):
1489    """Gets the interface for a particular IP
1490
1491    Args:
1492        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
1493            etc.  Must have the run function implemented.
1494        desired_ip_address: The IP address that is being looked for on a device.
1495
1496    Returns:
1497        The name of the test interface.
1498    """
1499
1500    desired_ip_address = desired_ip_address.split('%', 1)[0]
1501    all_ips_and_interfaces = comm_channel.run(
1502        '(ip -o -4 addr show; ip -o -6 addr show) | '
1503        'awk \'{print $2" "$4}\'').stdout
1504    for ip_address_and_interface in all_ips_and_interfaces.split('\n'):
1505        if desired_ip_address in ip_address_and_interface:
1506            return ip_address_and_interface.split()[1][:-1]
1507    return None
1508
1509
1510def renew_linux_ip_address(comm_channel, interface):
1511    comm_channel.run('sudo ifconfig %s down' % interface)
1512    comm_channel.run('sudo ifconfig %s up' % interface)
1513    comm_channel.run('sudo killall dhcpcd 2>/dev/null; echo""')
1514    is_dhcpcd_dead = False
1515    dhcpcd_counter = 0
1516    dhcpcd_checker_max_times = 3
1517    while not is_dhcpcd_dead:
1518        if dhcpcd_counter == dhcpcd_checker_max_times:
1519            raise TimeoutError('Unable to stop dhcpcd')
1520        time.sleep(1)
1521        if 'dhcpcd' in comm_channel.run('ps axu').stdout:
1522            dhcpcd_counter += 1
1523        else:
1524            is_dhcpcd_dead = True
1525    comm_channel.run('sudo dhcpcd -q -b')
1526    comm_channel.run('sudo dhclient -r %s' % interface)
1527    comm_channel.run('sudo dhclient %s' % interface)
1528
1529
1530def is_pingable(ip):
1531    """Returns whether an ip is pingable or not.
1532
1533    Args:
1534        ip: string, ip address to ping
1535    Returns:
1536        True if ping was successful, else False
1537    """
1538    if is_valid_ipv4_address(ip):
1539        ping_binary = 'ping'
1540    elif is_valid_ipv6_address(ip):
1541        ping_binary = 'ping6'
1542    else:
1543        raise ValueError('Invalid ip addr: %s' % ip)
1544
1545    os_type = platform.system()
1546    if os_type == 'Darwin':
1547        if is_valid_ipv6_address(ip):
1548            # ping6 on MacOS doesn't support timeout
1549            timeout_flag = []
1550        else:
1551            timeout_flag = ['-t', '1']
1552    elif os_type == 'Linux':
1553        timeout_flag = ['-W', '1']
1554    else:
1555        raise ValueError('Invalid OS.  Only Linux and MacOS are supported.')
1556
1557    ping_cmd = [ping_binary, *timeout_flag, '-c', '1', ip]
1558
1559    result = job.run(ping_cmd, timeout=10, ignore_status=True)
1560    return result.exit_status == 0
1561
1562
1563def ip_in_subnet(ip, subnet):
1564    """Validate that ip is in a given subnet.
1565
1566    Args:
1567        ip: string, ip address to verify (eg. '192.168.42.158')
1568        subnet: string, subnet to check (eg. '192.168.42.0/24')
1569
1570    Returns:
1571        True, if ip in subnet, else False
1572    """
1573    return ipaddress.ip_address(ip) in ipaddress.ip_network(subnet)
1574
1575
1576def mac_address_str_to_list(mac_addr_str):
1577    """Converts mac address string to list of decimal octets.
1578
1579    Args:
1580        mac_addr_string: string, mac address
1581            e.g. '12:34:56:78:9a:bc'
1582
1583    Returns
1584        list, representing mac address octets in decimal
1585            e.g. [18, 52, 86, 120, 154, 188]
1586    """
1587    return [int(octet, 16) for octet in mac_addr_str.split(':')]
1588
1589
1590def mac_address_list_to_str(mac_addr_list):
1591    """Converts list of decimal octets represeting mac address to string.
1592
1593    Args:
1594        mac_addr_list: list, representing mac address octets in decimal
1595            e.g. [18, 52, 86, 120, 154, 188]
1596
1597    Returns:
1598        string, mac address
1599            e.g. '12:34:56:78:9a:bc'
1600    """
1601    hex_list = []
1602    for octet in mac_addr_list:
1603        hex_octet = hex(octet)[2:]
1604        if octet < 16:
1605            hex_list.append('0%s' % hex_octet)
1606        else:
1607            hex_list.append(hex_octet)
1608
1609    return ':'.join(hex_list)
1610