1# Lint as: python2, python3
2# Copyright 2017 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7Convenience functions for use by tests or whomever.
8"""
9
10# pylint: disable=missing-docstring
11
12from __future__ import absolute_import
13from __future__ import division
14from __future__ import print_function
15
16import base64
17import collections
18import errno
19import glob
20import json
21import logging
22import math
23import multiprocessing
24import os
25import platform
26import re
27import shutil
28import signal
29import string
30import subprocess
31import tempfile
32import time
33import uuid
34
35from autotest_lib.client.common_lib import error
36from autotest_lib.client.common_lib import magic
37from autotest_lib.client.common_lib import utils
38from autotest_lib.client.common_lib.cros import cros_config
39
40from autotest_lib.client.common_lib.utils import *
41import six
42from six.moves import map
43from six.moves import range
44from six.moves import zip
45
46
47def grep(pattern, file):
48    """
49    This is mainly to fix the return code inversion from grep
50    Also handles compressed files.
51
52    returns 1 if the pattern is present in the file, 0 if not.
53    """
54    command = 'grep "%s" > /dev/null' % pattern
55    ret = cat_file_to_cmd(file, command, ignore_status=True)
56    return not ret
57
58
59def cat_file_to_cmd(file, command, ignore_status=0, return_output=False):
60    """
61    equivalent to 'cat file | command' but knows to use
62    zcat or bzcat if appropriate
63    """
64    if not os.path.isfile(file):
65        raise NameError('invalid file %s to cat to command %s'
66                % (file, command))
67
68    if return_output:
69        run_cmd = utils.system_output
70    else:
71        run_cmd = utils.system
72
73    if magic.guess_type(file) == 'application/x-bzip2':
74        cat = 'bzcat'
75    elif magic.guess_type(file) == 'application/x-gzip':
76        cat = 'zcat'
77    else:
78        cat = 'cat'
79    return run_cmd('%s %s | %s' % (cat, file, command),
80                   ignore_status=ignore_status)
81
82
83def extract_tarball_to_dir(tarball, dir):
84    """
85    Extract a tarball to a specified directory name instead of whatever
86    the top level of a tarball is - useful for versioned directory names, etc
87    """
88    if os.path.exists(dir):
89        if os.path.isdir(dir):
90            shutil.rmtree(dir)
91        else:
92            os.remove(dir)
93    pwd = os.getcwd()
94    os.chdir(os.path.dirname(os.path.abspath(dir)))
95    newdir = extract_tarball(tarball)
96    os.rename(newdir, dir)
97    os.chdir(pwd)
98
99
100def extract_tarball(tarball):
101    """Returns the directory extracted by the tarball."""
102    extracted = cat_file_to_cmd(tarball, 'tar xvf - 2>/dev/null',
103                                    return_output=True).splitlines()
104
105    dir = None
106
107    for line in extracted:
108        if line.startswith('./'):
109            line = line[2:]
110        if not line or line == '.':
111            continue
112        topdir = line.split('/')[0]
113        if os.path.isdir(topdir):
114            if dir:
115                assert(dir == topdir), 'tarball must be a a single directory'
116            else:
117                dir = topdir
118    if dir:
119        return dir
120    else:
121        raise NameError('extracting tarball produced no dir')
122
123
124def force_copy(src, dest):
125    """Replace dest with a new copy of src, even if it exists"""
126    if os.path.isfile(dest):
127        os.remove(dest)
128    if os.path.isdir(dest):
129        dest = os.path.join(dest, os.path.basename(src))
130    shutil.copyfile(src, dest)
131    return dest
132
133
134def file_contains_pattern(file, pattern):
135    """Return true if file contains the specified egrep pattern"""
136    if not os.path.isfile(file):
137        raise NameError('file %s does not exist' % file)
138    return not utils.system('egrep -q "' + pattern + '" ' + file,
139                            ignore_status=True)
140
141
142def list_grep(list, pattern):
143    """True if any item in list matches the specified pattern."""
144    compiled = re.compile(pattern)
145    for line in list:
146        match = compiled.search(line)
147        if (match):
148            return 1
149    return 0
150
151
152def get_os_vendor():
153    """Try to guess what's the os vendor
154    """
155    if os.path.isfile('/etc/SuSE-release'):
156        return 'SUSE'
157
158    issue = '/etc/issue'
159
160    if not os.path.isfile(issue):
161        return 'Unknown'
162
163    if file_contains_pattern(issue, 'Red Hat'):
164        return 'Red Hat'
165    elif file_contains_pattern(issue, 'Fedora'):
166        return 'Fedora Core'
167    elif file_contains_pattern(issue, 'SUSE'):
168        return 'SUSE'
169    elif file_contains_pattern(issue, 'Ubuntu'):
170        return 'Ubuntu'
171    elif file_contains_pattern(issue, 'Debian'):
172        return 'Debian'
173    else:
174        return 'Unknown'
175
176
177def get_cc():
178    try:
179        return os.environ['CC']
180    except KeyError:
181        return 'gcc'
182
183
184def get_vmlinux():
185    """Return the full path to vmlinux
186
187    Ahem. This is crap. Pray harder. Bad Martin.
188    """
189    vmlinux = '/boot/vmlinux-%s' % utils.system_output('uname -r')
190    if os.path.isfile(vmlinux):
191        return vmlinux
192    vmlinux = '/lib/modules/%s/build/vmlinux' % utils.system_output('uname -r')
193    if os.path.isfile(vmlinux):
194        return vmlinux
195    return None
196
197
198def get_systemmap():
199    """Return the full path to System.map
200
201    Ahem. This is crap. Pray harder. Bad Martin.
202    """
203    map = '/boot/System.map-%s' % utils.system_output('uname -r')
204    if os.path.isfile(map):
205        return map
206    map = '/lib/modules/%s/build/System.map' % utils.system_output('uname -r')
207    if os.path.isfile(map):
208        return map
209    return None
210
211
212def get_modules_dir():
213    """Return the modules dir for the running kernel version"""
214    kernel_version = utils.system_output('uname -r')
215    return '/lib/modules/%s/kernel' % kernel_version
216
217
218_CPUINFO_RE = re.compile(r'^(?P<key>[^\t]*)\t*: ?(?P<value>.*)$')
219
220
221def get_cpuinfo():
222    """Read /proc/cpuinfo and convert to a list of dicts."""
223    cpuinfo = []
224    with open('/proc/cpuinfo', 'r') as f:
225        cpu = {}
226        for line in f:
227            line = line.strip()
228            if not line:
229                cpuinfo.append(cpu)
230                cpu = {}
231                continue
232            match = _CPUINFO_RE.match(line)
233            cpu[match.group('key')] = match.group('value')
234        if cpu:
235            # cpuinfo usually ends in a blank line, so this shouldn't happen.
236            cpuinfo.append(cpu)
237    return cpuinfo
238
239
240def get_cpu_arch():
241    """Work out which CPU architecture we're running on"""
242
243    # Using 'uname -m' should be a very portable way to do this since the
244    # format is pretty standard.
245    machine_name = utils.system_output('uname -m').strip()
246
247    # Apparently ARM64 and ARM have both historically returned the string 'arm'
248    # here so continue the tradition.  Use startswith() because:
249    # - On most of our arm devices we'll actually see the string armv7l.
250    # - In theory the machine name could include a suffix for endianness.
251    if machine_name.startswith('aarch64') or machine_name.startswith('arm'):
252        return 'arm'
253
254    # Historically we _have_ treated x86_64 and i386 separately.
255    if machine_name in ('x86_64', 'i386'):
256        return machine_name
257
258    raise error.TestError('unsupported machine type %s' % machine_name)
259
260
261def get_arm_soc_family_from_devicetree():
262    """
263    Work out which ARM SoC we're running on based on the 'compatible' property
264    of the base node of devicetree, if it exists.
265    """
266    devicetree_compatible = '/sys/firmware/devicetree/base/compatible'
267    if not os.path.isfile(devicetree_compatible):
268        return None
269    f = open(devicetree_compatible, 'r')
270    compatible = f.read().split(chr(0))
271    f.close()
272    if list_grep(compatible, '^rockchip,'):
273        return 'rockchip'
274    elif list_grep(compatible, '^mediatek,'):
275        return 'mediatek'
276    elif list_grep(compatible, '^qcom,'):
277        return 'qualcomm'
278    return None
279
280
281def get_arm_soc_family():
282    """Work out which ARM SoC we're running on"""
283    family = get_arm_soc_family_from_devicetree()
284    if family is not None:
285        return family
286
287    f = open('/proc/cpuinfo', 'r')
288    cpuinfo = f.readlines()
289    f.close()
290    if list_grep(cpuinfo, 'EXYNOS5'):
291        return 'exynos5'
292    elif list_grep(cpuinfo, 'Tegra'):
293        return 'tegra'
294    elif list_grep(cpuinfo, 'Rockchip'):
295        return 'rockchip'
296    return 'arm'
297
298
299def get_cpu_soc_family():
300    """Like get_cpu_arch, but for ARM, returns the SoC family name"""
301    f = open('/proc/cpuinfo', 'r')
302    cpuinfo = f.readlines()
303    f.close()
304    family = get_cpu_arch()
305    if family == 'arm':
306        family = get_arm_soc_family()
307    if list_grep(cpuinfo, '^vendor_id.*:.*AMD'):
308        family = 'amd'
309    return family
310
311
312INTEL_UARCH_TABLE = {
313    '06_4C': 'Airmont',
314    '06_1C': 'Atom',
315    '06_26': 'Atom',
316    '06_27': 'Atom',
317    '06_35': 'Atom',
318    '06_36': 'Atom',
319    '06_3D': 'Broadwell',
320    '06_47': 'Broadwell',
321    '06_4F': 'Broadwell',
322    '06_56': 'Broadwell',
323    '06_A5': 'Comet Lake',
324    '06_A6': 'Comet Lake',
325    '06_0D': 'Dothan',
326    '06_5C': 'Goldmont',
327    '06_7A': 'Goldmont',
328    '06_3C': 'Haswell',
329    '06_45': 'Haswell',
330    '06_46': 'Haswell',
331    '06_3F': 'Haswell-E',
332    '06_7D': 'Ice Lake',
333    '06_7E': 'Ice Lake',
334    '06_3A': 'Ivy Bridge',
335    '06_3E': 'Ivy Bridge-E',
336    '06_8E': 'Kaby Lake',
337    '06_9E': 'Kaby Lake',
338    '06_0F': 'Merom',
339    '06_16': 'Merom',
340    '06_17': 'Nehalem',
341    '06_1A': 'Nehalem',
342    '06_1D': 'Nehalem',
343    '06_1E': 'Nehalem',
344    '06_1F': 'Nehalem',
345    '06_2E': 'Nehalem',
346    '0F_03': 'Prescott',
347    '0F_04': 'Prescott',
348    '0F_06': 'Presler',
349    '06_2A': 'Sandy Bridge',
350    '06_2D': 'Sandy Bridge',
351    '06_37': 'Silvermont',
352    '06_4A': 'Silvermont',
353    '06_4D': 'Silvermont',
354    '06_5A': 'Silvermont',
355    '06_5D': 'Silvermont',
356    '06_4E': 'Skylake',
357    '06_5E': 'Skylake',
358    '06_55': 'Skylake',
359    '06_8C': 'Tiger Lake',
360    '06_8D': 'Tiger Lake',
361    '06_86': 'Tremont',
362    '06_96': 'Tremont',
363    '06_9C': 'Tremont',
364    '06_25': 'Westmere',
365    '06_2C': 'Westmere',
366    '06_2F': 'Westmere',
367}
368
369
370def get_intel_cpu_uarch(numeric=False):
371    """Return the Intel microarchitecture we're running on, or None.
372
373    Returns None if this is not an Intel CPU. Returns the family and model as
374    underscore-separated hex (per Intel manual convention) if the uarch is not
375    known, or if numeric is True.
376    """
377    if not get_current_kernel_arch().startswith('x86'):
378        return None
379    cpuinfo = get_cpuinfo()[0]
380    if cpuinfo['vendor_id'] != 'GenuineIntel':
381        return None
382    family_model = '%02X_%02X' % (int(cpuinfo['cpu family']),
383                                  int(cpuinfo['model']))
384    if numeric:
385        return family_model
386    return INTEL_UARCH_TABLE.get(family_model, family_model)
387
388
389INTEL_SILVERMONT_BCLK_TABLE = [83333, 100000, 133333, 116667, 80000];
390
391
392def get_intel_bclk_khz():
393    """Return Intel CPU base clock.
394
395    This only worked with SandyBridge (released in 2011) or newer. Older CPU has
396    133 MHz bclk. See turbostat code for implementation that also works with
397    older CPU. https://git.io/vpyKT
398    """
399    if get_intel_cpu_uarch() == 'Silvermont':
400        MSR_FSB_FREQ = 0xcd
401        return INTEL_SILVERMONT_BCLK_TABLE[utils.rdmsr(MSR_FSB_FREQ) & 0xf]
402    return 100000
403
404
405def get_current_kernel_arch():
406    """Get the machine architecture, now just a wrap of 'uname -m'."""
407    return os.popen('uname -m').read().rstrip()
408
409
410def count_cpus():
411    """number of CPUs in the local machine according to /proc/cpuinfo"""
412    try:
413       return multiprocessing.cpu_count()
414    except Exception:
415       logging.exception('can not get cpu count from'
416                        ' multiprocessing.cpu_count()')
417    cpuinfo = get_cpuinfo()
418    # Returns at least one cpu. Check comment #1 in crosbug.com/p/9582.
419    return len(cpuinfo) or 1
420
421
422def cpu_online_map():
423    """
424    Check out the available cpu online map
425    """
426    cpuinfo = get_cpuinfo()
427    cpus = []
428    for cpu in cpuinfo:
429        cpus.append(cpu['processor'])  # grab cpu number
430    return cpus
431
432
433# Returns total memory in kb
434def read_from_meminfo(key):
435    meminfo = utils.system_output('grep %s /proc/meminfo' % key)
436    return int(re.search(r'\d+', meminfo).group(0))
437
438
439def memtotal():
440    return read_from_meminfo('MemTotal')
441
442
443def freememtotal():
444    return read_from_meminfo('MemFree')
445
446def usable_memtotal():
447    # Reserved 5% for OS use
448    return int(read_from_meminfo('MemFree') * 0.95)
449
450def swaptotal():
451    return read_from_meminfo('SwapTotal')
452
453def rounded_memtotal():
454    # Get total of all physical mem, in kbytes
455    usable_kbytes = memtotal()
456    # usable_kbytes is system's usable DRAM in kbytes,
457    #   as reported by memtotal() from device /proc/meminfo memtotal
458    #   after Linux deducts 1.5% to 5.1% for system table overhead
459    # Undo the unknown actual deduction by rounding up
460    #   to next small multiple of a big power-of-two
461    #   eg  12GB - 5.1% gets rounded back up to 12GB
462    mindeduct = 0.015  # 1.5 percent
463    maxdeduct = 0.055  # 5.5 percent
464    # deduction range 1.5% .. 5.5% supports physical mem sizes
465    #    6GB .. 12GB in steps of .5GB
466    #   12GB .. 24GB in steps of 1 GB
467    #   24GB .. 48GB in steps of 2 GB ...
468    # Finer granularity in physical mem sizes would require
469    #   tighter spread between min and max possible deductions
470
471    # increase mem size by at least min deduction, without rounding
472    min_kbytes = int(usable_kbytes / (1.0 - mindeduct))
473    # increase mem size further by 2**n rounding, by 0..roundKb or more
474    round_kbytes = int(usable_kbytes / (1.0 - maxdeduct)) - min_kbytes
475    # find least binary roundup 2**n that covers worst-cast roundKb
476    mod2n = 1 << int(math.ceil(math.log(round_kbytes, 2)))
477    # have round_kbytes <= mod2n < round_kbytes*2
478    # round min_kbytes up to next multiple of mod2n
479    phys_kbytes = min_kbytes + mod2n - 1
480    phys_kbytes = phys_kbytes - (phys_kbytes % mod2n)  # clear low bits
481    return phys_kbytes
482
483
484_MEMINFO_RE = re.compile('^(\w+)(\(\w+\))?:\s+(\d+)')
485
486
487def get_meminfo():
488    """Returns a namedtuple of pairs from /proc/meminfo.
489
490    Example /proc/meminfo snippets:
491        MemTotal:        2048000 kB
492        Active(anon):     409600 kB
493    Example usage:
494        meminfo = utils.get_meminfo()
495        print meminfo.Active_anon
496    """
497    info = {}
498    with _open_file('/proc/meminfo') as f:
499        for line in f:
500            m = _MEMINFO_RE.match(line)
501            if m:
502                if m.group(2):
503                    name = m.group(1) + '_' + m.group(2)[1:-1]
504                else:
505                    name = m.group(1)
506                info[name] = int(m.group(3))
507    return collections.namedtuple('MemInfo', list(info.keys()))(**info)
508
509
510def sysctl(key, value=None):
511    """Generic implementation of sysctl, to read and write.
512
513    @param key: A location under /proc/sys
514    @param value: If not None, a value to write into the sysctl.
515
516    @return The single-line sysctl value as a string.
517    """
518    path = '/proc/sys/%s' % key
519    if value is not None:
520        utils.write_one_line(path, str(value))
521    return utils.read_one_line(path)
522
523
524def sysctl_kernel(key, value=None):
525    """(Very) partial implementation of sysctl, for kernel params"""
526    if value is not None:
527        # write
528        utils.write_one_line('/proc/sys/kernel/%s' % key, str(value))
529    else:
530        # read
531        out = utils.read_one_line('/proc/sys/kernel/%s' % key)
532        return int(re.search(r'\d+', out).group(0))
533
534
535def get_num_allocated_file_handles():
536    """
537    Returns the number of currently allocated file handles.
538
539    Gets this information by parsing /proc/sys/fs/file-nr.
540    See https://www.kernel.org/doc/Documentation/sysctl/fs.txt
541    for details on this file.
542    """
543    with _open_file('/proc/sys/fs/file-nr') as f:
544        line = f.readline()
545    allocated_handles = int(line.split()[0])
546    return allocated_handles
547
548
549def dump_object(object):
550    """Dump an object's attributes and methods
551
552    kind of like dir()
553    """
554    for item in six.iteritems(object.__dict__):
555        print(item)
556        try:
557            (key, value) = item
558            dump_object(value)
559        except:
560            continue
561
562
563def environ(env_key):
564    """return the requested environment variable, or '' if unset"""
565    if (env_key in os.environ):
566        return os.environ[env_key]
567    else:
568        return ''
569
570
571def prepend_path(newpath, oldpath):
572    """prepend newpath to oldpath"""
573    if (oldpath):
574        return newpath + ':' + oldpath
575    else:
576        return newpath
577
578
579def append_path(oldpath, newpath):
580    """append newpath to oldpath"""
581    if (oldpath):
582        return oldpath + ':' + newpath
583    else:
584        return newpath
585
586
587_TIME_OUTPUT_RE = re.compile(
588        r'([\d\.]*)user ([\d\.]*)system '
589        r'(\d*):([\d\.]*)elapsed (\d*)%CPU')
590
591
592def to_seconds(time_string):
593    """Converts a string in M+:SS.SS format to S+.SS"""
594    elts = time_string.split(':')
595    if len(elts) == 1:
596        return time_string
597    return str(int(elts[0]) * 60 + float(elts[1]))
598
599
600_TIME_OUTPUT_RE_2 = re.compile(r'(.*?)user (.*?)system (.*?)elapsed')
601
602
603def running_config():
604    """
605    Return path of config file of the currently running kernel
606    """
607    version = utils.system_output('uname -r')
608    for config in ('/proc/config.gz', \
609                   '/boot/config-%s' % version,
610                   '/lib/modules/%s/build/.config' % version):
611        if os.path.isfile(config):
612            return config
613    return None
614
615
616def check_for_kernel_feature(feature):
617    config = running_config()
618
619    if not config:
620        raise TypeError("Can't find kernel config file")
621
622    if magic.guess_type(config) == 'application/x-gzip':
623        grep = 'zgrep'
624    else:
625        grep = 'grep'
626    grep += ' ^CONFIG_%s= %s' % (feature, config)
627
628    if not utils.system_output(grep, ignore_status=True):
629        raise ValueError("Kernel doesn't have a %s feature" % (feature))
630
631
632def check_glibc_ver(ver):
633    try:
634        glibc_ver = subprocess.check_output("ldd --version", shell=True)
635    except subprocess.CalledProcessError:
636        # To mimic previous behavior, if the command errors set the result to
637        # an empty str
638        glibc_ver = ''
639    glibc_ver = glibc_ver.splitlines()[0].decode()
640    glibc_ver = re.search(r'(\d+\.\d+(\.\d+)?)', glibc_ver).group()
641    if utils.compare_versions(glibc_ver, ver) == -1:
642        raise error.TestError("Glibc too old (%s). Glibc >= %s is needed." %
643                              (glibc_ver, ver))
644
645def check_kernel_ver(ver):
646    kernel_ver = utils.system_output('uname -r')
647    kv_tmp = re.split(r'[-]', kernel_ver)[0:3]
648    # In compare_versions, if v1 < v2, return value == -1
649    if utils.compare_versions(kv_tmp[0], ver) == -1:
650        raise error.TestError("Kernel too old (%s). Kernel > %s is needed." %
651                              (kernel_ver, ver))
652
653
654def numa_nodes():
655    node_paths = glob.glob('/sys/devices/system/node/node*')
656    nodes = [int(re.sub(r'.*node(\d+)', r'\1', x)) for x in node_paths]
657    return (sorted(nodes))
658
659
660# Return the kernel version and build timestamp.
661def running_os_release():
662    return os.uname()[2:4]
663
664
665def running_os_ident():
666    (version, timestamp) = running_os_release()
667    return version + '::' + timestamp
668
669
670def freespace(path):
671    """Return the disk free space, in bytes"""
672    s = os.statvfs(path)
673    return s.f_bavail * s.f_bsize
674
675
676_DISK_PARTITION_3_RE = re.compile(r'^(/dev/hd[a-z]+)3', re.M)
677
678
679def get_disk_size(disk_name):
680    """
681    Return size of disk in byte. Return 0 in Error Case
682
683    @param disk_name: disk name to find size
684    """
685    device = os.path.basename(disk_name)
686    with open('/proc/partitions') as f:
687        lines = f.readlines()
688    for line in lines:
689        try:
690            _, _, blocks, name = re.split(r' +', line.strip())
691        except ValueError:
692            continue
693        if name == device:
694            return 1024 * int(blocks)
695    return 0
696
697
698def get_disk_size_gb(disk_name):
699    """
700    Return size of disk in GB (10^9). Return 0 in Error Case
701
702    @param disk_name: disk name to find size
703    """
704    return int(get_disk_size(disk_name) / (10.0 ** 9) + 0.5)
705
706
707def get_disk_model(disk_name):
708    """
709    Return model name for internal storage device
710
711    @param disk_name: disk name to find model
712    """
713    cmd1 = 'udevadm info --query=property --name=%s' % disk_name
714    cmd2 = 'grep -E "ID_(NAME|MODEL)="'
715    cmd3 = 'cut -f 2 -d"="'
716    cmd = ' | '.join([cmd1, cmd2, cmd3])
717    return utils.system_output(cmd)
718
719
720_DISK_DEV_RE = re.compile(r'/dev/sd[a-z]|'
721                          r'/dev/mmcblk[0-9]+|'
722                          r'/dev/nvme[0-9]+n[0-9]+')
723
724
725def get_disk_from_filename(filename):
726    """
727    Return the disk device the filename is on.
728    If the file is on tmpfs or other special file systems,
729    return None.
730
731    @param filename: name of file, full path.
732    """
733
734    if not os.path.exists(filename):
735        raise error.TestError('file %s missing' % filename)
736
737    if filename[0] != '/':
738        raise error.TestError('This code works only with full path')
739
740    m = _DISK_DEV_RE.match(filename)
741    while not m:
742        if filename[0] != '/':
743            return None
744        if filename == '/dev/root':
745            cmd = 'rootdev -d -s'
746        elif filename.startswith('/dev/mapper'):
747            cmd = 'dmsetup table "%s"' % os.path.basename(filename)
748            dmsetup_output = utils.system_output(cmd).split(' ')
749            if dmsetup_output[2] == 'verity':
750                maj_min = dmsetup_output[4]
751            elif dmsetup_output[2] == 'crypt':
752                maj_min = dmsetup_output[6]
753            cmd = 'realpath "/dev/block/%s"' % maj_min
754        elif filename.startswith('/dev/loop'):
755            cmd = 'losetup -O BACK-FILE "%s" | tail -1' % filename
756        else:
757            cmd = 'df "%s" | tail -1 | cut -f 1 -d" "' % filename
758        filename = utils.system_output(cmd)
759        m = _DISK_DEV_RE.match(filename)
760    return m.group(0)
761
762
763def get_disk_firmware_version(disk_name):
764    """
765    Return firmware version for internal storage device. (empty string for eMMC)
766
767    @param disk_name: disk name to find model
768    """
769    cmd1 = 'udevadm info --query=property --name=%s' % disk_name
770    cmd2 = 'grep -E "ID_REVISION="'
771    cmd3 = 'cut -f 2 -d"="'
772    cmd = ' | '.join([cmd1, cmd2, cmd3])
773    return utils.system_output(cmd)
774
775
776def is_disk_nvme(disk_name):
777    """
778    Return true if disk is a nvme device, return false otherwise
779
780    @param disk_name: disk name to check
781    """
782    return re.match('/dev/nvme[0-9]+n[0-9]+', disk_name)
783
784
785def is_disk_scsi(disk_name):
786    """
787    Return true if disk is a scsi device, return false otherwise
788
789    @param disk_name: disk name check
790    """
791    return re.match('/dev/sd[a-z]+', disk_name)
792
793
794def is_disk_harddisk(disk_name):
795    """
796    Return true if disk is a harddisk, return false otherwise
797
798    @param disk_name: disk name check
799    """
800    cmd1 = 'udevadm info --query=property --name=%s' % disk_name
801    cmd2 = 'grep -E "ID_ATA_ROTATION_RATE_RPM="'
802    cmd3 = 'cut -f 2 -d"="'
803    cmd = ' | '.join([cmd1, cmd2, cmd3])
804
805    rtt = utils.system_output(cmd)
806
807    # eMMC will not have this field; rtt == ''
808    # SSD will have zero rotation rate; rtt == '0'
809    # For harddisk rtt > 0
810    return rtt and int(rtt) > 0
811
812def concat_partition(disk_name, partition_number):
813    """
814    Return the name of a partition:
815    sda, 3 --> sda3
816    mmcblk0, 3 --> mmcblk0p3
817
818    @param disk_name: diskname string
819    @param partition_number: integer
820    """
821    if disk_name.endswith(tuple(str(i) for i in range(0, 10))):
822        sep = 'p'
823    else:
824        sep = ''
825    return disk_name + sep + str(partition_number)
826
827def verify_hdparm_feature(disk_name, feature):
828    """
829    Check for feature support for SCSI disk using hdparm
830
831    @param disk_name: target disk
832    @param feature: hdparm output string of the feature
833    """
834    cmd = 'hdparm -I %s | grep -q "%s"' % (disk_name, feature)
835    ret = utils.system(cmd, ignore_status=True)
836    if ret == 0:
837        return True
838    elif ret == 1:
839        return False
840    else:
841        raise error.TestFail('Error running command %s' % cmd)
842
843def get_nvme_id_ns_feature(disk_name, feature):
844    """
845    Return feature value for NVMe disk using nvme id-ns
846
847    @param disk_name: target disk
848    @param feature: output string of the feature
849    """
850    cmd = "nvme id-ns -n 1 %s | grep %s" % (disk_name, feature)
851    feat = utils.system_output(cmd, ignore_status=True)
852    if not feat:
853        return 'None'
854    start = feat.find(':')
855    value = feat[start+2:]
856    return value
857
858def get_storage_error_msg(disk_name, reason):
859    """
860    Get Error message for storage test which include disk model.
861    and also include the firmware version for the SCSI disk
862
863    @param disk_name: target disk
864    @param reason: Reason of the error.
865    """
866
867    msg = reason
868
869    model = get_disk_model(disk_name)
870    msg += ' Disk model: %s' % model
871
872    if is_disk_scsi(disk_name):
873        fw = get_disk_firmware_version(disk_name)
874        msg += ' firmware: %s' % fw
875
876    return msg
877
878
879_IOSTAT_FIELDS = ('transfers_per_s', 'read_kb_per_s', 'written_kb_per_s',
880                  'read_kb', 'written_kb')
881_IOSTAT_RE = re.compile('ALL' + len(_IOSTAT_FIELDS) * r'\s+([\d\.]+)')
882
883def get_storage_statistics(device=None):
884    """
885    Fetches statistics for a storage device.
886
887    Using iostat(1) it retrieves statistics for a device since last boot.  See
888    the man page for iostat(1) for details on the different fields.
889
890    @param device: Path to a block device. Defaults to the device where root
891            is mounted.
892
893    @returns a dict mapping each field to its statistic.
894
895    @raises ValueError: If the output from iostat(1) can not be parsed.
896    """
897    if device is None:
898        device = get_root_device()
899    cmd = 'iostat -d -k -g ALL -H %s' % device
900    output = utils.system_output(cmd, ignore_status=True)
901    match = _IOSTAT_RE.search(output)
902    if not match:
903        raise ValueError('Unable to get iostat for %s' % device)
904    return dict(list(zip(_IOSTAT_FIELDS, list(map(float, match.groups())))))
905
906
907def load_module(module_name, params=None):
908    # Checks if a module has already been loaded
909    if module_is_loaded(module_name):
910        return False
911
912    cmd = '/sbin/modprobe ' + module_name
913    if params:
914        cmd += ' ' + params
915    utils.system(cmd)
916    return True
917
918
919def unload_module(module_name):
920    """
921    Removes a module. Handles dependencies. If even then it's not possible
922    to remove one of the modules, it will trhow an error.CmdError exception.
923
924    @param module_name: Name of the module we want to remove.
925    """
926    module_name = module_name.replace('-', '_')
927    l_raw = utils.system_output("/bin/lsmod").splitlines()
928    lsmod = [x for x in l_raw if x.split()[0] == module_name]
929    if len(lsmod) > 0:
930        line_parts = lsmod[0].split()
931        if len(line_parts) == 4:
932            submodules = line_parts[3].split(",")
933            for submodule in submodules:
934                unload_module(submodule)
935        utils.system("/sbin/modprobe -r %s" % module_name)
936        logging.info("Module %s unloaded", module_name)
937    else:
938        logging.info("Module %s is already unloaded", module_name)
939
940
941def module_is_loaded(module_name):
942    module_name = module_name.replace('-', '_')
943    modules = utils.system_output('/bin/lsmod').splitlines()
944    for module in modules:
945        if module.startswith(module_name) and module[len(module_name)] == ' ':
946            return True
947    return False
948
949
950def ping_default_gateway():
951    """Ping the default gateway."""
952
953    network = open('/etc/sysconfig/network')
954    m = re.search('GATEWAY=(\S+)', network.read())
955
956    if m:
957        gw = m.group(1)
958        cmd = 'ping %s -c 5 > /dev/null' % gw
959        return utils.system(cmd, ignore_status=True)
960
961    raise error.TestError('Unable to find default gateway')
962
963
964def drop_caches():
965    """Writes back all dirty pages to disk and clears all the caches."""
966    utils.system("sync")
967    # We ignore failures here as this will fail on 2.6.11 kernels.
968    utils.system("echo 3 > /proc/sys/vm/drop_caches", ignore_status=True)
969
970
971def set_hwclock(time='system',
972                utc=True,
973                rtc=None,
974                noadjfile=False,
975                ignore_status=False):
976    """Uses the hwclock command to set time of an RTC.
977
978    @param time: Either 'system', meaning use the system time, or a string
979                 to be passed to the --date argument of hwclock.
980    @param utc: Boolean of whether to use UTC or localtime.
981    @param rtc: String to be passed to the --rtc arg of hwclock.
982    @param noadjfile: Boolean of whether to use --noadjfile flag with hwclock.
983    @param ignore_status: Boolean of whether to ignore exit code of hwclock.
984    """
985    cmd = '/sbin/hwclock'
986    if time == 'system':
987        cmd += ' --systohc'
988    else:
989        cmd += ' --set --date "{}"'.format(time)
990    if utc:
991        cmd += ' --utc'
992    else:
993        cmd += ' --localtime'
994    if rtc is not None:
995        cmd += ' --rtc={}'.format(rtc)
996    if noadjfile:
997        cmd += ' --noadjfile'
998    return utils.system(cmd, ignore_status=ignore_status)
999
1000def set_wake_alarm(alarm_time):
1001    """
1002    Set the hardware RTC-based wake alarm to 'alarm_time'.
1003    """
1004    utils.write_one_line('/sys/class/rtc/rtc0/wakealarm', str(alarm_time))
1005
1006
1007_AUTOTEST_CLIENT_PATH = os.path.join(os.path.dirname(__file__), '..')
1008_AMD_PCI_IDS_FILE_PATH = os.path.join(_AUTOTEST_CLIENT_PATH,
1009                                      'bin/amd_pci_ids.json')
1010_INTEL_PCI_IDS_FILE_PATH = os.path.join(_AUTOTEST_CLIENT_PATH,
1011                                        'bin/intel_pci_ids.json')
1012_UI_USE_FLAGS_FILE_PATH = '/etc/ui_use_flags.txt'
1013
1014# Command to check if a package is installed. If the package is not installed
1015# the command shall fail.
1016_CHECK_PACKAGE_INSTALLED_COMMAND =(
1017        "dpkg-query -W -f='${Status}\n' %s | head -n1 | awk '{print $3;}' | "
1018        "grep -q '^installed$'")
1019
1020pciid_to_amd_architecture = {}
1021pciid_to_intel_architecture = {}
1022
1023class Crossystem(object):
1024    """A wrapper for the crossystem utility."""
1025
1026    def __init__(self, client):
1027        self.cros_system_data = {}
1028        self._client = client
1029
1030    def init(self):
1031        self.cros_system_data = {}
1032        (_, fname) = tempfile.mkstemp()
1033        f = open(fname, 'w')
1034        self._client.run('crossystem', stdout_tee=f)
1035        f.close()
1036        text = utils.read_file(fname)
1037        for line in text.splitlines():
1038            assignment_string = line.split('#')[0]
1039            if not assignment_string.count('='):
1040                continue
1041            (name, value) = assignment_string.split('=', 1)
1042            self.cros_system_data[name.strip()] = value.strip()
1043        os.remove(fname)
1044
1045    def __getattr__(self, name):
1046        """
1047        Retrieve a crosssystem attribute.
1048
1049        The call crossystemobject.name() will return the crossystem reported
1050        string.
1051        """
1052        return lambda: self.cros_system_data[name]
1053
1054
1055def get_oldest_pid_by_name(name):
1056    """
1057    Return the oldest pid of a process whose name perfectly matches |name|.
1058
1059    name is an egrep expression, which will be matched against the entire name
1060    of processes on the system.  For example:
1061
1062      get_oldest_pid_by_name('chrome')
1063
1064    on a system running
1065      8600 ?        00:00:04 chrome
1066      8601 ?        00:00:00 chrome
1067      8602 ?        00:00:00 chrome-sandbox
1068
1069    would return 8600, as that's the oldest process that matches.
1070    chrome-sandbox would not be matched.
1071
1072    Arguments:
1073      name: egrep expression to match.  Will be anchored at the beginning and
1074            end of the match string.
1075
1076    Returns:
1077      pid as an integer, or None if one cannot be found.
1078
1079    Raises:
1080      ValueError if pgrep returns something odd.
1081    """
1082    str_pid = utils.system_output('pgrep -o ^%s$' % name,
1083                                  ignore_status=True).rstrip()
1084    if str_pid:
1085        return int(str_pid)
1086
1087
1088def get_oldest_by_name(name):
1089    """Return pid and command line of oldest process whose name matches |name|.
1090
1091    @param name: egrep expression to match desired process name.
1092    @return: A tuple of (pid, command_line) of the oldest process whose name
1093             matches |name|.
1094
1095    """
1096    pid = get_oldest_pid_by_name(name)
1097    if pid:
1098        command_line = utils.system_output('ps -p %i -o command=' % pid,
1099                                           ignore_status=True).rstrip()
1100        return (pid, command_line)
1101
1102
1103def get_chrome_remote_debugging_port():
1104    """Returns remote debugging port for Chrome.
1105
1106    Parse chrome process's command line argument to get the remote debugging
1107    port. if it is 0, look at DevToolsActivePort for the ephemeral port.
1108    """
1109    _, command = get_oldest_by_name('chrome')
1110    matches = re.search('--remote-debugging-port=([0-9]+)', command)
1111    if not matches:
1112      return 0
1113    port = int(matches.group(1))
1114    if port:
1115      return port
1116    with open('/home/chronos/DevToolsActivePort') as f:
1117      return int(f.readline().rstrip())
1118
1119
1120def get_process_list(name, command_line=None):
1121    """
1122    Return the list of pid for matching process |name command_line|.
1123
1124    on a system running
1125      31475 ?    0:06 /opt/google/chrome/chrome --allow-webui-compositing -
1126      31478 ?    0:00 /opt/google/chrome/chrome-sandbox /opt/google/chrome/
1127      31485 ?    0:00 /opt/google/chrome/chrome --type=zygote --log-level=1
1128      31532 ?    1:05 /opt/google/chrome/chrome --type=renderer
1129
1130    get_process_list('chrome')
1131    would return ['31475', '31485', '31532']
1132
1133    get_process_list('chrome', '--type=renderer')
1134    would return ['31532']
1135
1136    Arguments:
1137      name: process name to search for. If command_line is provided, name is
1138            matched against full command line. If command_line is not provided,
1139            name is only matched against the process name.
1140      command line: when command line is passed, the full process command line
1141                    is used for matching.
1142
1143    Returns:
1144      list of PIDs of the matching processes.
1145
1146    """
1147    # TODO(rohitbm) crbug.com/268861
1148    flag = '-x' if not command_line else '-f'
1149    name = '\'%s.*%s\'' % (name, command_line) if command_line else name
1150    str_pid = utils.system_output('pgrep %s %s' % (flag, name),
1151                                  ignore_status=True).rstrip()
1152    return str_pid.split()
1153
1154
1155def nuke_process_by_name(name, with_prejudice=False):
1156    """Tell the oldest process specified by name to exit.
1157
1158    Arguments:
1159      name: process name specifier, as understood by pgrep.
1160      with_prejudice: if True, don't allow for graceful exit.
1161
1162    Raises:
1163      error.AutoservPidAlreadyDeadError: no existing process matches name.
1164    """
1165    try:
1166        pid = get_oldest_pid_by_name(name)
1167    except Exception as e:
1168        logging.error(e)
1169        return
1170    if pid is None:
1171        raise error.AutoservPidAlreadyDeadError('No process matching %s.' %
1172                                                name)
1173    if with_prejudice:
1174        utils.nuke_pid(pid, [signal.SIGKILL])
1175    else:
1176        utils.nuke_pid(pid)
1177
1178
1179def is_virtual_machine():
1180    if 'QEMU' in platform.processor():
1181        return True
1182
1183    try:
1184        with open('/sys/devices/virtual/dmi/id/sys_vendor') as f:
1185            if 'QEMU' in f.read():
1186                return True
1187    except IOError:
1188        pass
1189
1190    return False
1191
1192
1193def save_vm_state(checkpoint):
1194    """Saves the current state of the virtual machine.
1195
1196    This function is a NOOP if the test is not running under a virtual machine
1197    with the USB serial port redirected.
1198
1199    Arguments:
1200      checkpoint - Name used to identify this state
1201
1202    Returns:
1203      None
1204    """
1205    # The QEMU monitor has been redirected to the guest serial port located at
1206    # /dev/ttyUSB0. To save the state of the VM, we just send the 'savevm'
1207    # command to the serial port.
1208    if is_virtual_machine() and os.path.exists('/dev/ttyUSB0'):
1209        logging.info('Saving VM state "%s"', checkpoint)
1210        serial = open('/dev/ttyUSB0', 'w')
1211        serial.write('savevm %s\r\n' % checkpoint)
1212        logging.info('Done saving VM state "%s"', checkpoint)
1213
1214
1215def mounts():
1216    ret = []
1217    with open('/proc/mounts') as f:
1218        lines = f.readlines()
1219    for line in lines:
1220        m = re.match(
1221            r'(?P<src>\S+) (?P<dest>\S+) (?P<type>\S+) (?P<opts>\S+).*', line)
1222        if m:
1223            ret.append(m.groupdict())
1224    return ret
1225
1226
1227def is_mountpoint(path):
1228    return path in [m['dest'] for m in mounts()]
1229
1230
1231def require_mountpoint(path):
1232    """
1233    Raises an exception if path is not a mountpoint.
1234    """
1235    if not is_mountpoint(path):
1236        raise error.TestFail('Path not mounted: "%s"' % path)
1237
1238
1239def random_username():
1240    return str(uuid.uuid4()) + '@example.com'
1241
1242
1243def get_signin_credentials(filepath):
1244    """Returns user_id, password tuple from credentials file at filepath.
1245
1246    File must have one line of the format user_id:password
1247
1248    @param filepath: path of credentials file.
1249    @return user_id, password tuple.
1250    """
1251    user_id, password = None, None
1252    if os.path.isfile(filepath):
1253        with open(filepath) as f:
1254            user_id, password = f.read().rstrip().split(':')
1255    return user_id, password
1256
1257
1258def parse_cmd_output(command, run_method=utils.run):
1259    """Runs a command on a host object to retrieve host attributes.
1260
1261    The command should output to stdout in the format of:
1262    <key> = <value> # <optional_comment>
1263
1264
1265    @param command: Command to execute on the host.
1266    @param run_method: Function to use to execute the command. Defaults to
1267                       utils.run so that the command will be executed locally.
1268                       Can be replace with a host.run call so that it will
1269                       execute on a DUT or external machine. Method must accept
1270                       a command argument, stdout_tee and stderr_tee args and
1271                       return a result object with a string attribute stdout
1272                       which will be parsed.
1273
1274    @returns a dictionary mapping host attributes to their values.
1275    """
1276    result = {}
1277    # Suppresses stdout so that the files are not printed to the logs.
1278    cmd_result = run_method(command, stdout_tee=None, stderr_tee=None)
1279    for line in cmd_result.stdout.splitlines():
1280        # Lines are of the format "<key>     = <value>      # <comment>"
1281        key_value = re.match(r'^\s*(?P<key>[^ ]+)\s*=\s*(?P<value>[^ '
1282                             r']+)(?:\s*#.*)?$', line)
1283        if key_value:
1284            result[key_value.group('key')] = key_value.group('value')
1285    return result
1286
1287
1288def set_from_keyval_output(out, delimiter=' '):
1289    """Parse delimiter-separated key-val output into a set of tuples.
1290
1291    Output is expected to be multiline text output from a command.
1292    Stuffs the key-vals into tuples in a set to be later compared.
1293
1294    e.g.  deactivated 0
1295          disableForceClear 0
1296          ==>  set(('deactivated', '0'), ('disableForceClear', '0'))
1297
1298    @param out: multiple lines of space-separated key-val pairs.
1299    @param delimiter: character that separates key from val. Usually a
1300                      space but may be '=' or something else.
1301    @return set of key-val tuples.
1302    """
1303    results = set()
1304    kv_match_re = re.compile('([^ ]+)%s(.*)' % delimiter)
1305    for linecr in out.splitlines():
1306        match = kv_match_re.match(linecr.strip())
1307        if match:
1308            results.add((match.group(1), match.group(2)))
1309    return results
1310
1311
1312def get_cpu_usage():
1313    """Returns machine's CPU usage.
1314
1315    This function uses /proc/stat to identify CPU usage.
1316    Returns:
1317        A dictionary with values for all columns in /proc/stat
1318        Sample dictionary:
1319        {
1320            'user': 254544,
1321            'nice': 9,
1322            'system': 254768,
1323            'idle': 2859878,
1324            'iowait': 1,
1325            'irq': 2,
1326            'softirq': 3,
1327            'steal': 4,
1328            'guest': 5,
1329            'guest_nice': 6
1330        }
1331        If a column is missing or malformed in /proc/stat (typically on older
1332        systems), the value for that column is set to 0.
1333    """
1334    with _open_file('/proc/stat') as proc_stat:
1335        cpu_usage_str = proc_stat.readline().split()
1336    columns = ('user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq',
1337               'steal', 'guest', 'guest_nice')
1338    d = {}
1339    for index, col in enumerate(columns, 1):
1340        try:
1341            d[col] = int(cpu_usage_str[index])
1342        except:
1343            d[col] = 0
1344    return d
1345
1346def compute_active_cpu_time(cpu_usage_start, cpu_usage_end):
1347    """Computes the fraction of CPU time spent non-idling.
1348
1349    This function should be invoked using before/after values from calls to
1350    get_cpu_usage().
1351
1352    See https://stackoverflow.com/a/23376195 and
1353    https://unix.stackexchange.com/a/303224 for some more context how
1354    to calculate usage given two /proc/stat snapshots.
1355    """
1356    idle_cols = ('idle', 'iowait')  # All other cols are calculated as active.
1357    time_active_start = sum([x[1] for x in six.iteritems(cpu_usage_start)
1358                             if x[0] not in idle_cols])
1359    time_active_end = sum([x[1] for x in six.iteritems(cpu_usage_end)
1360                           if x[0] not in idle_cols])
1361    total_time_start = sum(cpu_usage_start.values())
1362    total_time_end = sum(cpu_usage_end.values())
1363    # Avoid bogus division which has been observed on Tegra.
1364    if total_time_end <= total_time_start:
1365        logging.warning('compute_active_cpu_time observed bogus data')
1366        # We pretend to be busy, this will force a longer wait for idle CPU.
1367        return 1.0
1368    return ((float(time_active_end) - time_active_start) /
1369            (total_time_end - total_time_start))
1370
1371
1372def is_pgo_mode():
1373    return 'USE_PGO' in os.environ
1374
1375
1376def wait_for_idle_cpu(timeout, utilization):
1377    """Waits for the CPU to become idle (< utilization).
1378
1379    Args:
1380        timeout: The longest time in seconds to wait before throwing an error.
1381        utilization: The CPU usage below which the system should be considered
1382                idle (between 0 and 1.0 independent of cores/hyperthreads).
1383    """
1384    time_passed = 0.0
1385    fraction_active_time = 1.0
1386    sleep_time = 1
1387    logging.info('Starting to wait up to %.1fs for idle CPU...', timeout)
1388    while fraction_active_time >= utilization:
1389        cpu_usage_start = get_cpu_usage()
1390        # Split timeout interval into not too many chunks to limit log spew.
1391        # Start at 1 second, increase exponentially
1392        time.sleep(sleep_time)
1393        time_passed += sleep_time
1394        sleep_time = min(16.0, 2.0 * sleep_time)
1395        cpu_usage_end = get_cpu_usage()
1396        fraction_active_time = compute_active_cpu_time(cpu_usage_start,
1397                                                       cpu_usage_end)
1398        logging.info('After waiting %.1fs CPU utilization is %.3f.',
1399                     time_passed, fraction_active_time)
1400        if time_passed > timeout:
1401            logging.warning('CPU did not become idle.')
1402            log_process_activity()
1403            # crosbug.com/37389
1404            if is_pgo_mode():
1405                logging.info('Still continuing because we are in PGO mode.')
1406                return True
1407
1408            return False
1409    logging.info('Wait for idle CPU took %.1fs (utilization = %.3f).',
1410                 time_passed, fraction_active_time)
1411    return True
1412
1413
1414def log_process_activity():
1415    """Logs the output of top.
1416
1417    Useful to debug performance tests and to find runaway processes.
1418    """
1419    logging.info('Logging current process activity using top and ps.')
1420    cmd = 'top -b -n1 -c'
1421    output = utils.run(cmd)
1422    logging.info(output)
1423    output = utils.run('ps axl')
1424    logging.info(output)
1425
1426
1427def wait_for_cool_machine():
1428    """
1429    A simple heuristic to wait for a machine to cool.
1430    The code looks a bit 'magic', but we don't know ambient temperature
1431    nor machine characteristics and still would like to return the caller
1432    a machine that cooled down as much as reasonably possible.
1433    """
1434    temperature = get_current_temperature_max()
1435    # We got here with a cold machine, return immediately. This should be the
1436    # most common case.
1437    if temperature < 45:
1438        return True
1439    logging.info('Got a hot machine of %dC. Sleeping 1 minute.', temperature)
1440    # A modest wait should cool the machine.
1441    time.sleep(60.0)
1442    temperature = get_current_temperature_max()
1443    # Atoms idle below 60 and everyone else should be even lower.
1444    if temperature < 62:
1445        return True
1446    # This should be rare.
1447    logging.info('Did not cool down (%dC). Sleeping 2 minutes.', temperature)
1448    time.sleep(120.0)
1449    temperature = get_current_temperature_max()
1450    # A temperature over 65'C doesn't give us much headroom to the critical
1451    # temperatures that start at 85'C (and PerfControl as of today will fail at
1452    # critical - 10'C).
1453    if temperature < 65:
1454        return True
1455    logging.warning('Did not cool down (%dC), giving up.', temperature)
1456    log_process_activity()
1457    return False
1458
1459
1460def report_temperature(test, keyname):
1461    """Report current max observed temperature with given keyname.
1462
1463    @param test: autotest_lib.client.bin.test.test instance
1464    @param keyname: key to be used when reporting perf value.
1465    """
1466    temperature = get_current_temperature_max()
1467    logging.info('%s = %f degree Celsius', keyname, temperature)
1468    test.output_perf_value(
1469        description=keyname,
1470        value=temperature,
1471        units='Celsius',
1472        higher_is_better=False)
1473
1474
1475# System paths for machine performance state.
1476_CPUINFO = '/proc/cpuinfo'
1477_DIRTY_WRITEBACK_CENTISECS = '/proc/sys/vm/dirty_writeback_centisecs'
1478_KERNEL_MAX = '/sys/devices/system/cpu/kernel_max'
1479_MEMINFO = '/proc/meminfo'
1480_TEMP_SENSOR_RE = 'Reading temperature...([0-9]*)'
1481
1482def _open_file(path):
1483    """
1484    Opens a file and returns the file object.
1485
1486    This method is intended to be mocked by tests.
1487    @return The open file object.
1488    """
1489    return open(path)
1490
1491def _get_line_from_file(path, line):
1492    """
1493    line can be an integer or
1494    line can be a string that matches the beginning of the line
1495    """
1496    with _open_file(path) as f:
1497        if isinstance(line, int):
1498            l = f.readline()
1499            for _ in range(0, line):
1500                l = f.readline()
1501            return l
1502        else:
1503            for l in f:
1504                if l.startswith(line):
1505                    return l
1506    return None
1507
1508
1509def _get_match_from_file(path, line, prefix, postfix):
1510    """
1511    Matches line in path and returns string between first prefix and postfix.
1512    """
1513    match = _get_line_from_file(path, line)
1514    # Strip everything from front of line including prefix.
1515    if prefix:
1516        match = re.split(prefix, match)[1]
1517    # Strip everything from back of string including first occurence of postfix.
1518    if postfix:
1519        match = re.split(postfix, match)[0]
1520    return match
1521
1522
1523def _get_float_from_file(path, line, prefix, postfix):
1524    match = _get_match_from_file(path, line, prefix, postfix)
1525    return float(match)
1526
1527
1528def _get_int_from_file(path, line, prefix, postfix):
1529    match = _get_match_from_file(path, line, prefix, postfix)
1530    return int(match)
1531
1532
1533def _get_hex_from_file(path, line, prefix, postfix):
1534    match = _get_match_from_file(path, line, prefix, postfix)
1535    return int(match, 16)
1536
1537
1538def is_system_thermally_throttled():
1539    """
1540    Returns whether the system appears to be thermally throttled.
1541    """
1542    for path in glob.glob('/sys/class/thermal/cooling_device*/type'):
1543        with _open_file(path) as f:
1544            cdev_type = f.read().strip()
1545
1546        if not (cdev_type == 'Processor' or
1547                cdev_type.startswith('thermal-devfreq') or
1548                cdev_type.startswith('thermal-cpufreq')):
1549            continue
1550
1551        cur_state_path = os.path.join(os.path.dirname(path), 'cur_state')
1552        if _get_int_from_file(cur_state_path, 0, None, None) > 0:
1553            return True
1554
1555    return False
1556
1557
1558# The paths don't change. Avoid running find all the time.
1559_hwmon_paths = {}
1560
1561def _get_hwmon_datas(file_pattern):
1562    """Returns a list of reading from hwmon."""
1563    # Some systems like daisy_spring only have the virtual hwmon.
1564    # And other systems like rambi only have coretemp.0. See crbug.com/360249.
1565    #    /sys/class/hwmon/hwmon*/
1566    #    /sys/devices/virtual/hwmon/hwmon*/
1567    #    /sys/devices/platform/coretemp.0/
1568    if file_pattern not in _hwmon_paths:
1569        cmd = 'find /sys/class /sys/devices -name "' + file_pattern + '"'
1570        _hwmon_paths[file_pattern] = \
1571            utils.run(cmd, verbose=False).stdout.splitlines()
1572    for _hwmon_path in _hwmon_paths[file_pattern]:
1573        try:
1574            yield _get_float_from_file(_hwmon_path, 0, None, None) * 0.001
1575        except IOError as err:
1576            # Files under /sys may get truncated and result in ENODATA.
1577            # Ignore those.
1578            if err.errno is not errno.ENODATA:
1579                raise
1580
1581
1582def _get_hwmon_temperatures():
1583    """
1584    Returns the currently observed temperatures from hwmon
1585    """
1586    return list(_get_hwmon_datas('temp*_input'))
1587
1588
1589def _get_thermal_zone_temperatures():
1590    """
1591    Returns the maximum currently observered temperature in thermal_zones.
1592    """
1593    temperatures = []
1594    for path in glob.glob('/sys/class/thermal/thermal_zone*/temp'):
1595        try:
1596            temperatures.append(
1597                _get_float_from_file(path, 0, None, None) * 0.001)
1598        except IOError:
1599            # Some devices (e.g. Veyron) may have reserved thermal zones that
1600            # are not active. Trying to read the temperature value would cause a
1601            # EINVAL IO error.
1602            continue
1603    return temperatures
1604
1605
1606def get_ec_temperatures():
1607    """
1608    Uses ectool to return a list of all sensor temperatures in Celsius.
1609
1610    Output from ectool is either '0: 300' or '0: 300 K' (newer ectool
1611    includes the unit).
1612    """
1613    temperatures = []
1614    try:
1615        full_cmd = 'ectool temps all'
1616        lines = utils.run(full_cmd, verbose=False).stdout.splitlines()
1617        pattern = re.compile('.*: (\d+)')
1618        for line in lines:
1619            matched = pattern.match(line)
1620            temperature = int(matched.group(1)) - 273
1621            temperatures.append(temperature)
1622    except Exception as e:
1623        logging.warning('Unable to read temperature sensors using ectool %s.',
1624                        e)
1625    # Sanity check for real world values.
1626    if not all(10.0 <= temperature <= 150.0 for temperature in temperatures):
1627        logging.warning('Unreasonable EC temperatures: %s.', temperatures)
1628    return temperatures
1629
1630
1631def get_current_temperature_max():
1632    """
1633    Returns the highest reported board temperature (all sensors) in Celsius.
1634    """
1635    all_temps = (_get_hwmon_temperatures() +
1636                 _get_thermal_zone_temperatures() +
1637                 get_ec_temperatures())
1638    if all_temps:
1639        temperature = max(all_temps)
1640    else:
1641        temperature = -1
1642    # Sanity check for real world values.
1643    assert ((temperature > 10.0) and
1644            (temperature < 150.0)), ('Unreasonable temperature %.1fC.' %
1645                                     temperature)
1646    return temperature
1647
1648
1649def get_cpu_max_frequency():
1650    """
1651    Returns the largest of the max CPU core frequencies. The unit is Hz.
1652    """
1653    max_frequency = -1
1654    paths = utils._get_cpufreq_paths('cpuinfo_max_freq')
1655    if not paths:
1656        raise ValueError('Could not find max freq; is cpufreq supported?')
1657    for path in paths:
1658        try:
1659            # Convert from kHz to Hz.
1660            frequency = 1000 * _get_float_from_file(path, 0, None, None)
1661        # CPUs may come and go. A missing entry or two aren't critical.
1662        except IOError:
1663            continue
1664        max_frequency = max(frequency, max_frequency)
1665    # Sanity check.
1666    assert max_frequency > 1e8, ('Unreasonably low CPU frequency: %.1f' %
1667            max_frequency)
1668    return max_frequency
1669
1670
1671def get_board_property(key):
1672    """
1673    Get a specific property from /etc/lsb-release.
1674
1675    @param key: board property to return value for
1676
1677    @return the value or '' if not present
1678    """
1679    with open('/etc/lsb-release') as f:
1680        pattern = '%s=(.*)' % key
1681        pat = re.search(pattern, f.read())
1682        if pat:
1683            return pat.group(1)
1684    return ''
1685
1686
1687def get_board():
1688    """
1689    Get the ChromeOS release board name from /etc/lsb-release.
1690    """
1691    return get_board_property('BOARD')
1692
1693
1694def get_board_type():
1695    """
1696    Get the ChromeOS board type from /etc/lsb-release.
1697
1698    @return device type.
1699    """
1700    return get_board_property('DEVICETYPE')
1701
1702
1703def get_chromeos_version():
1704    """
1705    Get the ChromeOS build version from /etc/lsb-release.
1706
1707    @return chromeos release version.
1708    """
1709    return get_board_property('CHROMEOS_RELEASE_VERSION')
1710
1711
1712def get_platform():
1713    """
1714    Get the ChromeOS platform name.
1715
1716    For unibuild this should be equal to model name.  For non-unibuild
1717    it will either be board name or empty string.  In the case of
1718    empty string return board name to match equivalent logic in
1719    server/hosts/cros_host.py
1720
1721    @returns platform name
1722    """
1723    platform = cros_config.call_cros_config_get_output('/ name', utils.run)
1724    if platform == '':
1725        platform = get_board()
1726    return platform
1727
1728
1729def get_sku():
1730    """
1731    Get the SKU number.
1732
1733    @returns SKU number
1734    """
1735    return cros_config.call_cros_config_get_output('/identity sku-id',
1736                                                   utils.run)
1737
1738
1739def get_ec_version():
1740    """Get the ec version as strings.
1741
1742    @returns a string representing this host's ec version.
1743    """
1744    command = 'mosys ec info -s fw_version'
1745    result = utils.run(command, ignore_status=True)
1746    if result.exit_status != 0:
1747        return ''
1748    return result.stdout.strip()
1749
1750
1751def get_firmware_version():
1752    """Get the firmware version as strings.
1753
1754    @returns a string representing this host's firmware version.
1755    """
1756    return utils.run('crossystem fwid').stdout.strip()
1757
1758
1759def get_hardware_revision():
1760    """Get the hardware revision as strings.
1761
1762    @returns a string representing this host's hardware revision.
1763    """
1764    command = 'mosys platform version'
1765    result = utils.run(command, ignore_status=True)
1766    if result.exit_status != 0:
1767        return ''
1768    return result.stdout.strip()
1769
1770
1771def get_kernel_version():
1772    """Get the kernel version as strings.
1773
1774    @returns a string representing this host's kernel version.
1775    """
1776    return utils.run('uname -r').stdout.strip()
1777
1778
1779def get_cpu_name():
1780    """Get the cpu name as strings.
1781
1782    @returns a string representing this host's cpu name.
1783    """
1784
1785    # Try get cpu name from device tree first
1786    if os.path.exists("/proc/device-tree/compatible"):
1787        command = "sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible | tail -1"
1788        return utils.run(command).stdout.strip().replace(',', ' ')
1789
1790
1791    # Get cpu name from uname -p
1792    command = "uname -p"
1793    ret = utils.run(command).stdout.strip()
1794
1795    # 'uname -p' return variant of unknown or amd64 or x86_64 or i686
1796    # Try get cpu name from /proc/cpuinfo instead
1797    if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE):
1798        command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1"
1799        ret = utils.run(command).stdout.strip()
1800
1801    # Remove bloat from CPU name, for example
1802    # 'Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz'       -> 'Intel Core i5-7Y57'
1803    # 'Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz'     -> 'Intel Xeon E5-2690 v4'
1804    # 'AMD A10-7850K APU with Radeon(TM) R7 Graphics' -> 'AMD A10-7850K'
1805    # 'AMD GX-212JC SOC with Radeon(TM) R2E Graphics' -> 'AMD GX-212JC'
1806    trim_re = " (@|processor|apu|soc|radeon).*|\(.*?\)| cpu"
1807    return re.sub(trim_re, '', ret, flags=re.IGNORECASE)
1808
1809
1810def get_screen_resolution():
1811    """Get the screen(s) resolution as strings.
1812    In case of more than 1 monitor, return resolution for each monitor separate
1813    with plus sign.
1814
1815    @returns a string representing this host's screen(s) resolution.
1816    """
1817    command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done'
1818    ret = utils.run(command, ignore_status=True)
1819    # We might have Chromebox without a screen
1820    if ret.exit_status != 0:
1821        return ''
1822    return ret.stdout.strip().replace('\n', '+')
1823
1824
1825def get_board_with_frequency_and_memory():
1826    """
1827    Returns a board name modified with CPU frequency and memory size to
1828    differentiate between different board variants. For instance
1829    link -> link_1.8GHz_4GB.
1830    """
1831    board_name = get_board()
1832    if is_virtual_machine():
1833        board = '%s_VM' % board_name
1834    else:
1835        memory = get_mem_total_gb()
1836        # Convert frequency to GHz with 1 digit accuracy after the
1837        # decimal point.
1838        frequency = int(round(get_cpu_max_frequency() * 1e-8)) * 0.1
1839        board = '%s_%1.1fGHz_%dGB' % (board_name, frequency, memory)
1840    return board
1841
1842
1843def get_mem_total():
1844    """
1845    Returns the total memory available in the system in MBytes.
1846    """
1847    mem_total = _get_float_from_file(_MEMINFO, 'MemTotal:', 'MemTotal:', ' kB')
1848    # Sanity check, all Chromebooks have at least 1GB of memory.
1849    assert mem_total > 256 * 1024, 'Unreasonable amount of memory.'
1850    return int(mem_total / 1024)
1851
1852
1853def get_mem_total_gb():
1854    """
1855    Returns the total memory available in the system in GBytes.
1856    """
1857    return int(round(get_mem_total() / 1024.0))
1858
1859
1860def get_mem_free():
1861    """
1862    Returns the currently free memory in the system in MBytes.
1863    """
1864    mem_free = _get_float_from_file(_MEMINFO, 'MemFree:', 'MemFree:', ' kB')
1865    return int(mem_free / 1024)
1866
1867def get_mem_free_plus_buffers_and_cached():
1868    """
1869    Returns the free memory in MBytes, counting buffers and cached as free.
1870
1871    This is most often the most interesting number since buffers and cached
1872    memory can be reclaimed on demand. Note however, that there are cases
1873    where this as misleading as well, for example used tmpfs space
1874    count as Cached but can not be reclaimed on demand.
1875    See https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt.
1876    """
1877    free_mb = get_mem_free()
1878    cached_mb = (_get_float_from_file(
1879        _MEMINFO, 'Cached:', 'Cached:', ' kB') / 1024)
1880    buffers_mb = (_get_float_from_file(
1881        _MEMINFO, 'Buffers:', 'Buffers:', ' kB') / 1024)
1882    return free_mb + buffers_mb + cached_mb
1883
1884
1885def get_dirty_writeback_centisecs():
1886    """
1887    Reads /proc/sys/vm/dirty_writeback_centisecs.
1888    """
1889    time = _get_int_from_file(_DIRTY_WRITEBACK_CENTISECS, 0, None, None)
1890    return time
1891
1892
1893def set_dirty_writeback_centisecs(time=60000):
1894    """
1895    In hundredths of a second, this is how often pdflush wakes up to write data
1896    to disk. The default wakes up the two (or more) active threads every five
1897    seconds. The ChromeOS default is 10 minutes.
1898
1899    We use this to set as low as 1 second to flush error messages in system
1900    logs earlier to disk.
1901    """
1902    # Flush buffers first to make this function synchronous.
1903    utils.system('sync')
1904    if time >= 0:
1905        cmd = 'echo %d > %s' % (time, _DIRTY_WRITEBACK_CENTISECS)
1906        utils.system(cmd)
1907
1908
1909def wflinfo_cmd():
1910    """
1911    Returns a wflinfo command appropriate to the current graphics platform/api.
1912    """
1913    return 'wflinfo -p %s -a %s' % (graphics_platform(), graphics_api())
1914
1915
1916def has_mali():
1917    """ @return: True if system has a Mali GPU enabled."""
1918    return os.path.exists('/dev/mali0')
1919
1920def get_gpu_family():
1921    """Returns the GPU family name."""
1922    global pciid_to_amd_architecture
1923    global pciid_to_intel_architecture
1924
1925    socfamily = get_cpu_soc_family()
1926    if socfamily == 'exynos5' or socfamily == 'rockchip' or has_mali():
1927        cmd = wflinfo_cmd()
1928        wflinfo = utils.system_output(cmd,
1929                                      retain_output=True,
1930                                      ignore_status=False)
1931        m = re.findall(r'OpenGL renderer string: (Mali-\w+)', wflinfo)
1932        if m:
1933            return m[0].lower()
1934        return 'mali-unrecognized'
1935    if socfamily == 'tegra':
1936        return 'tegra'
1937    if socfamily == 'qualcomm':
1938        return 'qualcomm'
1939    if os.path.exists('/sys/kernel/debug/pvr'):
1940        return 'rogue'
1941
1942    pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n')
1943    bus_device_function = pci_vga_device.partition(' ')[0]
1944    pci_path = '/sys/bus/pci/devices/0000:' + bus_device_function + '/device'
1945
1946    if not os.path.exists(pci_path):
1947        raise error.TestError('PCI device 0000:' + bus_device_function + ' not found')
1948
1949    device_id = utils.read_one_line(pci_path).lower()
1950
1951    if "Advanced Micro Devices" in pci_vga_device:
1952        if not pciid_to_amd_architecture:
1953            with open(_AMD_PCI_IDS_FILE_PATH, 'r') as in_f:
1954                pciid_to_amd_architecture = json.load(in_f)
1955
1956        return pciid_to_amd_architecture[device_id]
1957
1958    if "Intel Corporation" in pci_vga_device:
1959        # Only load Intel PCI ID file once and only if necessary.
1960        if not pciid_to_intel_architecture:
1961            with open(_INTEL_PCI_IDS_FILE_PATH, 'r') as in_f:
1962                pciid_to_intel_architecture = json.load(in_f)
1963
1964        return pciid_to_intel_architecture[device_id]
1965
1966# TODO(ihf): Consider using /etc/lsb-release DEVICETYPE != CHROMEBOOK/CHROMEBASE
1967# for sanity check, but usage seems a bit inconsistent. See
1968# src/third_party/chromiumos-overlay/eclass/appid.eclass
1969_BOARDS_WITHOUT_MONITOR = [
1970    'anglar', 'mccloud', 'monroe', 'ninja', 'rikku', 'guado', 'jecht', 'tidus',
1971    'beltino', 'panther', 'stumpy', 'panther', 'tricky', 'zako', 'veyron_rialto'
1972]
1973
1974
1975def has_no_monitor():
1976    """Returns whether a machine doesn't have a built-in monitor."""
1977    board_name = get_board()
1978    if board_name in _BOARDS_WITHOUT_MONITOR:
1979        return True
1980
1981    return False
1982
1983
1984def get_fixed_dst_drive():
1985    """
1986    Return device name for internal disk.
1987    Example: return /dev/sda for falco booted from usb
1988    """
1989    cmd = ' '.join(['. /usr/sbin/write_gpt.sh;',
1990                    '. /usr/share/misc/chromeos-common.sh;',
1991                    'load_base_vars;',
1992                    'get_fixed_dst_drive'])
1993    return utils.system_output(cmd)
1994
1995
1996def get_root_device():
1997    """
1998    Return root device.
1999    Will return correct disk device even system boot from /dev/dm-0
2000    Example: return /dev/sdb for falco booted from usb
2001    """
2002    return utils.system_output('rootdev -s -d')
2003
2004
2005def get_other_device():
2006    """
2007    Return the non root devices.
2008    Will return a list of other block devices, that are not the root device.
2009    """
2010
2011    cmd = 'lsblk -dpn -o NAME | grep -v -E "(loop|zram|boot|rpmb)"'
2012    devs = utils.system_output(cmd).splitlines()
2013
2014    for dev in devs[:]:
2015        if not re.match(r'/dev/(sd[a-z]|mmcblk[0-9]+|nvme[0-9]+)p?[0-9]*', dev):
2016            devs.remove(dev)
2017        if dev == get_root_device():
2018            devs.remove(dev)
2019    return devs
2020
2021
2022def get_root_partition():
2023    """
2024    Return current root partition
2025    Example: return /dev/sdb3 for falco booted from usb
2026    """
2027    return utils.system_output('rootdev -s')
2028
2029
2030def get_free_root_partition(root_part=None):
2031    """
2032    Return currently unused root partion
2033    Example: return /dev/sdb5 for falco booted from usb
2034
2035    @param root_part: cuurent root partition
2036    """
2037    spare_root_map = {'3': '5', '5': '3'}
2038    if not root_part:
2039        root_part = get_root_partition()
2040    return root_part[:-1] + spare_root_map[root_part[-1]]
2041
2042
2043def get_kernel_partition(root_part=None):
2044    """
2045    Return current kernel partition
2046    Example: return /dev/sda2 for falco booted from usb
2047
2048    @param root_part: current root partition
2049    """
2050    if not root_part:
2051         root_part = get_root_partition()
2052    current_kernel_map = {'3': '2', '5': '4'}
2053    return root_part[:-1] + current_kernel_map[root_part[-1]]
2054
2055
2056def get_free_kernel_partition(root_part=None):
2057    """
2058    return currently unused kernel partition
2059    Example: return /dev/sda4 for falco booted from usb
2060
2061    @param root_part: current root partition
2062    """
2063    kernel_part = get_kernel_partition(root_part)
2064    spare_kernel_map = {'2': '4', '4': '2'}
2065    return kernel_part[:-1] + spare_kernel_map[kernel_part[-1]]
2066
2067
2068def is_booted_from_internal_disk():
2069    """Return True if boot from internal disk. False, otherwise."""
2070    return get_root_device() == get_fixed_dst_drive()
2071
2072
2073def get_ui_use_flags():
2074    """Parses the USE flags as listed in /etc/ui_use_flags.txt.
2075
2076    @return: A list of flag strings found in the ui use flags file.
2077    """
2078    flags = []
2079    for flag in utils.read_file(_UI_USE_FLAGS_FILE_PATH).splitlines():
2080        # Removes everything after the '#'.
2081        flag_before_comment = flag.split('#')[0].strip()
2082        if len(flag_before_comment) != 0:
2083            flags.append(flag_before_comment)
2084
2085    return flags
2086
2087
2088def graphics_platform():
2089    """
2090    Return a string identifying the graphics platform,
2091    e.g. 'glx' or 'x11_egl' or 'gbm'
2092    """
2093    return 'null'
2094
2095
2096def graphics_api():
2097    """Return a string identifying the graphics api, e.g. gl or gles2."""
2098    use_flags = get_ui_use_flags()
2099    if 'opengles' in use_flags:
2100        return 'gles2'
2101    return 'gl'
2102
2103
2104def is_package_installed(package):
2105    """Check if a package is installed already.
2106
2107    @return: True if the package is already installed, otherwise return False.
2108    """
2109    try:
2110        utils.run(_CHECK_PACKAGE_INSTALLED_COMMAND % package)
2111        return True
2112    except error.CmdError:
2113        logging.warn('Package %s is not installed.', package)
2114        return False
2115
2116
2117def is_python_package_installed(package):
2118    """Check if a Python package is installed already.
2119
2120    @return: True if the package is already installed, otherwise return False.
2121    """
2122    try:
2123        __import__(package)
2124        return True
2125    except ImportError:
2126        logging.warn('Python package %s is not installed.', package)
2127        return False
2128
2129
2130def run_sql_cmd(server, user, password, command, database=''):
2131    """Run the given sql command against the specified database.
2132
2133    @param server: Hostname or IP address of the MySQL server.
2134    @param user: User name to log in the MySQL server.
2135    @param password: Password to log in the MySQL server.
2136    @param command: SQL command to run.
2137    @param database: Name of the database to run the command. Default to empty
2138                     for command that does not require specifying database.
2139
2140    @return: The stdout of the command line.
2141    """
2142    cmd = ('mysql -u%s -p%s --host %s %s -e "%s"' %
2143           (user, password, server, database, command))
2144    # Set verbose to False so the command line won't be logged, as it includes
2145    # database credential.
2146    return utils.run(cmd, verbose=False).stdout
2147
2148
2149def strip_non_printable(s):
2150    """Strip non printable characters from string.
2151
2152    @param s: Input string
2153
2154    @return: The input string with only printable characters.
2155    """
2156    return ''.join(x for x in s if x in string.printable)
2157
2158
2159def recursive_func(obj, func, types, sequence_types=(list, tuple, set),
2160                   dict_types=(dict,), fix_num_key=False):
2161    """Apply func to obj recursively.
2162
2163    This function traverses recursively through any sequence-like and
2164    dict-like elements in obj.
2165
2166    @param obj: the object to apply the function func recursively.
2167    @param func: the function to invoke.
2168    @param types: the target types in the object to apply func.
2169    @param sequence_types: the sequence types in python.
2170    @param dict_types: the dict types in python.
2171    @param fix_num_key: to indicate if the key of a dict should be
2172            converted from str type to a number, int or float, type.
2173            It is a culprit of json that it always treats the key of
2174            a dict as string.
2175            Refer to https://docs.python.org/2/library/json.html
2176            for more information.
2177
2178    @return: the result object after applying the func recursively.
2179    """
2180    def ancestors(obj, types):
2181        """Any ancestor of the object class is a subclass of the types?
2182
2183        @param obj: the object to apply the function func.
2184        @param types: the target types of the object.
2185
2186        @return: True if any ancestor class of the obj is found in types;
2187                 False otherwise.
2188        """
2189        return any([issubclass(anc, types) for anc in type(obj).__mro__])
2190
2191    if isinstance(obj, sequence_types) or ancestors(obj, sequence_types):
2192        result_lst = [recursive_func(elm, func, types, fix_num_key=fix_num_key)
2193                      for elm in obj]
2194        # Convert the result list to the object's original sequence type.
2195        return type(obj)(result_lst)
2196    elif isinstance(obj, dict_types) or ancestors(obj, dict_types):
2197        result_lst = [
2198                (recursive_func(key, func, types, fix_num_key=fix_num_key),
2199                 recursive_func(value, func, types, fix_num_key=fix_num_key))
2200                for (key, value) in obj.items()]
2201        # Convert the result list to the object's original dict type.
2202        return type(obj)(result_lst)
2203    # Here are the basic types.
2204    elif isinstance(obj, types) or ancestors(obj, types):
2205        if fix_num_key:
2206            # Check if this is a int or float
2207            try:
2208                result_obj = int(obj)
2209                return result_obj
2210            except ValueError:
2211                try:
2212                    result_obj = float(obj)
2213                    return result_obj
2214                except ValueError:
2215                    pass
2216
2217        result_obj = func(obj)
2218        return result_obj
2219    else:
2220        return obj
2221
2222
2223def base64_recursive_encode(obj):
2224    """Apply base64 encode recursively into the obj structure.
2225
2226    Most of the string-like types could be traced to basestring and bytearray
2227    as follows:
2228        str: basestring
2229        bytes: basestring
2230        dbus.String: basestring
2231        dbus.Signature: basestring
2232        dbus.ByteArray: basestring
2233
2234    Note that all the above types except dbus.String could be traced back to
2235    str. In order to cover dbus.String, basestring is used as the ancestor
2236    class for string-like types.
2237
2238    The other type that needs encoding with base64 in a structure includes
2239        bytearray: bytearray
2240
2241    The sequence types include (list, tuple, set). The dbus.Array is also
2242    covered as
2243        dbus.Array: list
2244
2245    The base dictionary type is dict. The dbus.Dictionary is also covered as
2246        dbus.Dictionary: dict
2247
2248    An example code and output look like
2249        obj = {'a': 10, 'b': 'hello',
2250               'c': [100, 200, bytearray(b'\xf0\xf1\xf2\xf3\xf4')],
2251               'd': {784: bytearray(b'@\x14\x01P'),
2252                     78.0: bytearray(b'\x10\x05\x0b\x10\xb2\x1b\x00')}}
2253        encode_obj = base64_recursive_encode(obj)
2254        decode_obj = base64_recursive_decode(encode_obj)
2255
2256        print 'obj: ', obj
2257        print 'encode_obj: ', encode_obj
2258        print 'decode_obj: ', decode_obj
2259        print 'Equal?', obj == decode_obj
2260
2261        Output:
2262        obj:  {'a': 10,
2263               'c': [100, 200, bytearray(b'\xf0\xf1\xf2\xf3\xf4')],
2264               'b': 'hello',
2265               'd': {784: bytearray(b'@\x14\x01P'),
2266                     78.0: bytearray(b'\x10\x05\x0b\x10\xb2\x1b\x00')}}
2267
2268        encode_obj:  {'YQ==': 10,
2269                      'Yw==': [100, 200, '8PHy8/Q='],
2270                      'Yg==': 'aGVsbG8='
2271                      'ZA==': {784: 'QBQBUA==', 78.0: 'EAULELIbAA=='}}
2272        decode_obj:  {'a': 10,
2273                      'c': [100, 200, '\xf0\xf1\xf2\xf3\xf4'],
2274                      'b': 'hello',
2275                      'd': {784: '@\x14\x01P',
2276                            78.0: '\x10\x05\x0b\x10\xb2\x1b\x00'}}
2277        Equal? True
2278
2279    @param obj: the object to apply base64 encoding recursively.
2280
2281    @return: the base64 encoded object.
2282    """
2283    encode_types = (six.string_types, bytearray)
2284    return recursive_func(obj, base64.standard_b64encode, encode_types)
2285
2286
2287def base64_recursive_decode(obj):
2288    """Apply base64 decode recursively into the obj structure.
2289
2290    @param obj: the object to apply base64 decoding recursively.
2291
2292    @return: the base64 decoded object.
2293    """
2294    decode_types = (six.string_types,)
2295    return recursive_func(obj, base64.standard_b64decode, decode_types,
2296                          fix_num_key=True)
2297