# Copyright 2017 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ Convenience functions for use by tests or whomever. """ # pylint: disable=missing-docstring import base64 import collections import commands import fnmatch import glob import json import logging import math import multiprocessing import os import pickle import platform import re import shutil import signal import string import tempfile import time import uuid from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib import magic from autotest_lib.client.common_lib import utils from autotest_lib.client.common_lib.utils import * def grep(pattern, file): """ This is mainly to fix the return code inversion from grep Also handles compressed files. returns 1 if the pattern is present in the file, 0 if not. """ command = 'grep "%s" > /dev/null' % pattern ret = cat_file_to_cmd(file, command, ignore_status=True) return not ret def difflist(list1, list2): """returns items in list2 that are not in list1""" diff = []; for x in list2: if x not in list1: diff.append(x) return diff def cat_file_to_cmd(file, command, ignore_status=0, return_output=False): """ equivalent to 'cat file | command' but knows to use zcat or bzcat if appropriate """ if not os.path.isfile(file): raise NameError('invalid file %s to cat to command %s' % (file, command)) if return_output: run_cmd = utils.system_output else: run_cmd = utils.system if magic.guess_type(file) == 'application/x-bzip2': cat = 'bzcat' elif magic.guess_type(file) == 'application/x-gzip': cat = 'zcat' else: cat = 'cat' return run_cmd('%s %s | %s' % (cat, file, command), ignore_status=ignore_status) def extract_tarball_to_dir(tarball, dir): """ Extract a tarball to a specified directory name instead of whatever the top level of a tarball is - useful for versioned directory names, etc """ if os.path.exists(dir): if os.path.isdir(dir): shutil.rmtree(dir) else: os.remove(dir) pwd = os.getcwd() os.chdir(os.path.dirname(os.path.abspath(dir))) newdir = extract_tarball(tarball) os.rename(newdir, dir) os.chdir(pwd) def extract_tarball(tarball): """Returns the directory extracted by the tarball.""" extracted = cat_file_to_cmd(tarball, 'tar xvf - 2>/dev/null', return_output=True).splitlines() dir = None for line in extracted: if line.startswith('./'): line = line[2:] if not line or line == '.': continue topdir = line.split('/')[0] if os.path.isdir(topdir): if dir: assert(dir == topdir) else: dir = topdir if dir: return dir else: raise NameError('extracting tarball produced no dir') def force_copy(src, dest): """Replace dest with a new copy of src, even if it exists""" if os.path.isfile(dest): os.remove(dest) if os.path.isdir(dest): dest = os.path.join(dest, os.path.basename(src)) shutil.copyfile(src, dest) return dest def force_link(src, dest): """Link src to dest, overwriting it if it exists""" return utils.system("ln -sf %s %s" % (src, dest)) def file_contains_pattern(file, pattern): """Return true if file contains the specified egrep pattern""" if not os.path.isfile(file): raise NameError('file %s does not exist' % file) return not utils.system('egrep -q "' + pattern + '" ' + file, ignore_status=True) def list_grep(list, pattern): """True if any item in list matches the specified pattern.""" compiled = re.compile(pattern) for line in list: match = compiled.search(line) if (match): return 1 return 0 def get_os_vendor(): """Try to guess what's the os vendor """ if os.path.isfile('/etc/SuSE-release'): return 'SUSE' issue = '/etc/issue' if not os.path.isfile(issue): return 'Unknown' if file_contains_pattern(issue, 'Red Hat'): return 'Red Hat' elif file_contains_pattern(issue, 'Fedora'): return 'Fedora Core' elif file_contains_pattern(issue, 'SUSE'): return 'SUSE' elif file_contains_pattern(issue, 'Ubuntu'): return 'Ubuntu' elif file_contains_pattern(issue, 'Debian'): return 'Debian' else: return 'Unknown' def get_cc(): try: return os.environ['CC'] except KeyError: return 'gcc' def get_vmlinux(): """Return the full path to vmlinux Ahem. This is crap. Pray harder. Bad Martin. """ vmlinux = '/boot/vmlinux-%s' % utils.system_output('uname -r') if os.path.isfile(vmlinux): return vmlinux vmlinux = '/lib/modules/%s/build/vmlinux' % utils.system_output('uname -r') if os.path.isfile(vmlinux): return vmlinux return None def get_systemmap(): """Return the full path to System.map Ahem. This is crap. Pray harder. Bad Martin. """ map = '/boot/System.map-%s' % utils.system_output('uname -r') if os.path.isfile(map): return map map = '/lib/modules/%s/build/System.map' % utils.system_output('uname -r') if os.path.isfile(map): return map return None def get_modules_dir(): """Return the modules dir for the running kernel version""" kernel_version = utils.system_output('uname -r') return '/lib/modules/%s/kernel' % kernel_version _CPUINFO_RE = re.compile(r'^(?P[^\t]*)\t*: ?(?P.*)$') def get_cpuinfo(): """Read /proc/cpuinfo and convert to a list of dicts.""" cpuinfo = [] with open('/proc/cpuinfo', 'r') as f: cpu = {} for line in f: line = line.strip() if not line: cpuinfo.append(cpu) cpu = {} continue match = _CPUINFO_RE.match(line) cpu[match.group('key')] = match.group('value') if cpu: # cpuinfo usually ends in a blank line, so this shouldn't happen. cpuinfo.append(cpu) return cpuinfo def get_cpu_arch(): """Work out which CPU architecture we're running on""" # Using 'uname -m' should be a very portable way to do this since the # format is pretty standard. machine_name = utils.system_output('uname -m').strip() # Apparently ARM64 and ARM have both historically returned the string 'arm' # here so continue the tradition. Use startswith() because: # - On most of our arm devices we'll actually see the string armv7l. # - In theory the machine name could include a suffix for endianness. if machine_name.startswith('aarch64') or machine_name.startswith('arm'): return 'arm' # Historically we _have_ treated x86_64 and i386 separately. if machine_name in ('x86_64', 'i386'): return machine_name raise error.TestError('unsupported machine type %s' % machine_name) def get_arm_soc_family_from_devicetree(): """ Work out which ARM SoC we're running on based on the 'compatible' property of the base node of devicetree, if it exists. """ devicetree_compatible = '/sys/firmware/devicetree/base/compatible' if not os.path.isfile(devicetree_compatible): return None f = open(devicetree_compatible, 'r') compatible = f.read().split(chr(0)) f.close() if list_grep(compatible, '^rockchip,'): return 'rockchip' elif list_grep(compatible, '^mediatek,'): return 'mediatek' elif list_grep(compatible, '^qcom,'): return 'qualcomm' return None def get_arm_soc_family(): """Work out which ARM SoC we're running on""" family = get_arm_soc_family_from_devicetree() if family is not None: return family f = open('/proc/cpuinfo', 'r') cpuinfo = f.readlines() f.close() if list_grep(cpuinfo, 'EXYNOS5'): return 'exynos5' elif list_grep(cpuinfo, 'Tegra'): return 'tegra' elif list_grep(cpuinfo, 'Rockchip'): return 'rockchip' return 'arm' def get_cpu_soc_family(): """Like get_cpu_arch, but for ARM, returns the SoC family name""" f = open('/proc/cpuinfo', 'r') cpuinfo = f.readlines() f.close() family = get_cpu_arch() if family == 'arm': family = get_arm_soc_family() if list_grep(cpuinfo, '^vendor_id.*:.*AMD'): family = 'amd' return family INTEL_UARCH_TABLE = { '06_4C': 'Airmont', '06_1C': 'Atom', '06_26': 'Atom', '06_27': 'Atom', '06_35': 'Atom', '06_36': 'Atom', '06_3D': 'Broadwell', '06_47': 'Broadwell', '06_4F': 'Broadwell', '06_56': 'Broadwell', '06_0D': 'Dothan', '06_5C': 'Goldmont', '06_7A': 'Goldmont', '06_3C': 'Haswell', '06_45': 'Haswell', '06_46': 'Haswell', '06_3F': 'Haswell-E', '06_3A': 'Ivy Bridge', '06_3E': 'Ivy Bridge-E', '06_8E': 'Kaby Lake', '06_9E': 'Kaby Lake', '06_0F': 'Merom', '06_16': 'Merom', '06_17': 'Nehalem', '06_1A': 'Nehalem', '06_1D': 'Nehalem', '06_1E': 'Nehalem', '06_1F': 'Nehalem', '06_2E': 'Nehalem', '0F_03': 'Prescott', '0F_04': 'Prescott', '0F_06': 'Presler', '06_2A': 'Sandy Bridge', '06_2D': 'Sandy Bridge', '06_37': 'Silvermont', '06_4A': 'Silvermont', '06_4D': 'Silvermont', '06_5A': 'Silvermont', '06_5D': 'Silvermont', '06_4E': 'Skylake', '06_5E': 'Skylake', '06_55': 'Skylake', '06_25': 'Westmere', '06_2C': 'Westmere', '06_2F': 'Westmere', } def get_intel_cpu_uarch(numeric=False): """Return the Intel microarchitecture we're running on, or None. Returns None if this is not an Intel CPU. Returns the family and model as underscore-separated hex (per Intel manual convention) if the uarch is not known, or if numeric is True. """ if not get_current_kernel_arch().startswith('x86'): return None cpuinfo = get_cpuinfo()[0] if cpuinfo['vendor_id'] != 'GenuineIntel': return None family_model = '%02X_%02X' % (int(cpuinfo['cpu family']), int(cpuinfo['model'])) if numeric: return family_model return INTEL_UARCH_TABLE.get(family_model, family_model) INTEL_SILVERMONT_BCLK_TABLE = [83333, 100000, 133333, 116667, 80000]; def get_intel_bclk_khz(): """Return Intel CPU base clock. This only worked with SandyBridge (released in 2011) or newer. Older CPU has 133 MHz bclk. See turbostat code for implementation that also works with older CPU. https://git.io/vpyKT """ if get_intel_cpu_uarch() == 'Silvermont': MSR_FSB_FREQ = 0xcd return INTEL_SILVERMONT_BCLK_TABLE[utils.rdmsr(MSR_FSB_FREQ) & 0xf] return 100000 def get_current_kernel_arch(): """Get the machine architecture, now just a wrap of 'uname -m'.""" return os.popen('uname -m').read().rstrip() def get_file_arch(filename): # -L means follow symlinks file_data = utils.system_output('file -L ' + filename) if file_data.count('80386'): return 'i386' return None def count_cpus(): """number of CPUs in the local machine according to /proc/cpuinfo""" try: return multiprocessing.cpu_count() except Exception: logging.exception('can not get cpu count from' ' multiprocessing.cpu_count()') cpuinfo = get_cpuinfo() # Returns at least one cpu. Check comment #1 in crosbug.com/p/9582. return len(cpuinfo) or 1 def cpu_online_map(): """ Check out the available cpu online map """ cpuinfo = get_cpuinfo() cpus = [] for cpu in cpuinfo: cpus.append(cpu['processor']) # grab cpu number return cpus def get_cpu_family(): cpuinfo = get_cpuinfo()[0] return int(cpuinfo['cpu_family']) def get_cpu_vendor(): cpuinfo = get_cpuinfo() vendors = [cpu['vendor_id'] for cpu in cpuinfo] for v in vendors[1:]: if v != vendors[0]: raise error.TestError('multiple cpu vendors found: ' + str(vendors)) return vendors[0] def probe_cpus(): """ This routine returns a list of cpu devices found under /sys/devices/system/cpu. """ cmd = 'find /sys/devices/system/cpu/ -maxdepth 1 -type d -name cpu*' return utils.system_output(cmd).splitlines() # Returns total memory in kb def read_from_meminfo(key): meminfo = utils.system_output('grep %s /proc/meminfo' % key) return int(re.search(r'\d+', meminfo).group(0)) def memtotal(): return read_from_meminfo('MemTotal') def freememtotal(): return read_from_meminfo('MemFree') def usable_memtotal(): # Reserved 5% for OS use return int(read_from_meminfo('MemFree') * 0.95) def swaptotal(): return read_from_meminfo('SwapTotal') def rounded_memtotal(): # Get total of all physical mem, in kbytes usable_kbytes = memtotal() # usable_kbytes is system's usable DRAM in kbytes, # as reported by memtotal() from device /proc/meminfo memtotal # after Linux deducts 1.5% to 5.1% for system table overhead # Undo the unknown actual deduction by rounding up # to next small multiple of a big power-of-two # eg 12GB - 5.1% gets rounded back up to 12GB mindeduct = 0.015 # 1.5 percent maxdeduct = 0.055 # 5.5 percent # deduction range 1.5% .. 5.5% supports physical mem sizes # 6GB .. 12GB in steps of .5GB # 12GB .. 24GB in steps of 1 GB # 24GB .. 48GB in steps of 2 GB ... # Finer granularity in physical mem sizes would require # tighter spread between min and max possible deductions # increase mem size by at least min deduction, without rounding min_kbytes = int(usable_kbytes / (1.0 - mindeduct)) # increase mem size further by 2**n rounding, by 0..roundKb or more round_kbytes = int(usable_kbytes / (1.0 - maxdeduct)) - min_kbytes # find least binary roundup 2**n that covers worst-cast roundKb mod2n = 1 << int(math.ceil(math.log(round_kbytes, 2))) # have round_kbytes <= mod2n < round_kbytes*2 # round min_kbytes up to next multiple of mod2n phys_kbytes = min_kbytes + mod2n - 1 phys_kbytes = phys_kbytes - (phys_kbytes % mod2n) # clear low bits return phys_kbytes _MEMINFO_RE = re.compile('^(\w+)(\(\w+\))?:\s+(\d+)') def get_meminfo(): """Returns a namedtuple of pairs from /proc/meminfo. Example /proc/meminfo snippets: MemTotal: 2048000 kB Active(anon): 409600 kB Example usage: meminfo = utils.get_meminfo() print meminfo.Active_anon """ info = {} with _open_file('/proc/meminfo') as f: for line in f: m = _MEMINFO_RE.match(line) if m: if m.group(2): name = m.group(1) + '_' + m.group(2)[1:-1] else: name = m.group(1) info[name] = int(m.group(3)) return collections.namedtuple('MemInfo', info.keys())(**info) def sysctl(key, value=None): """Generic implementation of sysctl, to read and write. @param key: A location under /proc/sys @param value: If not None, a value to write into the sysctl. @return The single-line sysctl value as a string. """ path = '/proc/sys/%s' % key if value is not None: utils.write_one_line(path, str(value)) return utils.read_one_line(path) def sysctl_kernel(key, value=None): """(Very) partial implementation of sysctl, for kernel params""" if value is not None: # write utils.write_one_line('/proc/sys/kernel/%s' % key, str(value)) else: # read out = utils.read_one_line('/proc/sys/kernel/%s' % key) return int(re.search(r'\d+', out).group(0)) def _convert_exit_status(sts): if os.WIFSIGNALED(sts): return -os.WTERMSIG(sts) elif os.WIFEXITED(sts): return os.WEXITSTATUS(sts) else: # impossible? raise RuntimeError("Unknown exit status %d!" % sts) def where_art_thy_filehandles(): """Dump the current list of filehandles""" os.system("ls -l /proc/%d/fd >> /dev/tty" % os.getpid()) def get_num_allocated_file_handles(): """ Returns the number of currently allocated file handles. Gets this information by parsing /proc/sys/fs/file-nr. See https://www.kernel.org/doc/Documentation/sysctl/fs.txt for details on this file. """ with _open_file('/proc/sys/fs/file-nr') as f: line = f.readline() allocated_handles = int(line.split()[0]) return allocated_handles def print_to_tty(string): """Output string straight to the tty""" open('/dev/tty', 'w').write(string + '\n') def dump_object(object): """Dump an object's attributes and methods kind of like dir() """ for item in object.__dict__.iteritems(): print item try: (key, value) = item dump_object(value) except: continue def environ(env_key): """return the requested environment variable, or '' if unset""" if (os.environ.has_key(env_key)): return os.environ[env_key] else: return '' def prepend_path(newpath, oldpath): """prepend newpath to oldpath""" if (oldpath): return newpath + ':' + oldpath else: return newpath def append_path(oldpath, newpath): """append newpath to oldpath""" if (oldpath): return oldpath + ':' + newpath else: return newpath _TIME_OUTPUT_RE = re.compile( r'([\d\.]*)user ([\d\.]*)system ' r'(\d*):([\d\.]*)elapsed (\d*)%CPU') def avgtime_print(dir): """ Calculate some benchmarking statistics. Input is a directory containing a file called 'time'. File contains one-per-line results of /usr/bin/time. Output is average Elapsed, User, and System time in seconds, and average CPU percentage. """ user = system = elapsed = cpu = count = 0 with open(dir + "/time") as f: for line in f: try: m = _TIME_OUTPUT_RE.match(line); user += float(m.group(1)) system += float(m.group(2)) elapsed += (float(m.group(3)) * 60) + float(m.group(4)) cpu += float(m.group(5)) count += 1 except: raise ValueError("badly formatted times") return "Elapsed: %0.2fs User: %0.2fs System: %0.2fs CPU: %0.0f%%" % \ (elapsed / count, user / count, system / count, cpu / count) def to_seconds(time_string): """Converts a string in M+:SS.SS format to S+.SS""" elts = time_string.split(':') if len(elts) == 1: return time_string return str(int(elts[0]) * 60 + float(elts[1])) _TIME_OUTPUT_RE_2 = re.compile(r'(.*?)user (.*?)system (.*?)elapsed') def extract_all_time_results(results_string): """Extract user, system, and elapsed times into a list of tuples""" results = [] for result in _TIME_OUTPUT_RE_2.findall(results_string): results.append(tuple([to_seconds(elt) for elt in result])) return results def running_config(): """ Return path of config file of the currently running kernel """ version = utils.system_output('uname -r') for config in ('/proc/config.gz', \ '/boot/config-%s' % version, '/lib/modules/%s/build/.config' % version): if os.path.isfile(config): return config return None def check_for_kernel_feature(feature): config = running_config() if not config: raise TypeError("Can't find kernel config file") if magic.guess_type(config) == 'application/x-gzip': grep = 'zgrep' else: grep = 'grep' grep += ' ^CONFIG_%s= %s' % (feature, config) if not utils.system_output(grep, ignore_status=True): raise ValueError("Kernel doesn't have a %s feature" % (feature)) def check_glibc_ver(ver): glibc_ver = commands.getoutput('ldd --version').splitlines()[0] glibc_ver = re.search(r'(\d+\.\d+(\.\d+)?)', glibc_ver).group() if utils.compare_versions(glibc_ver, ver) == -1: raise error.TestError("Glibc too old (%s). Glibc >= %s is needed." % (glibc_ver, ver)) def check_kernel_ver(ver): kernel_ver = utils.system_output('uname -r') kv_tmp = re.split(r'[-]', kernel_ver)[0:3] # In compare_versions, if v1 < v2, return value == -1 if utils.compare_versions(kv_tmp[0], ver) == -1: raise error.TestError("Kernel too old (%s). Kernel > %s is needed." % (kernel_ver, ver)) def human_format(number): # Convert number to kilo / mega / giga format. if number < 1024: return "%d" % number kilo = float(number) / 1024.0 if kilo < 1024: return "%.2fk" % kilo meg = kilo / 1024.0 if meg < 1024: return "%.2fM" % meg gig = meg / 1024.0 return "%.2fG" % gig def numa_nodes(): node_paths = glob.glob('/sys/devices/system/node/node*') nodes = [int(re.sub(r'.*node(\d+)', r'\1', x)) for x in node_paths] return (sorted(nodes)) def node_size(): nodes = max(len(numa_nodes()), 1) return ((memtotal() * 1024) / nodes) def pickle_load(filename): return pickle.load(open(filename, 'r')) # Return the kernel version and build timestamp. def running_os_release(): return os.uname()[2:4] def running_os_ident(): (version, timestamp) = running_os_release() return version + '::' + timestamp def running_os_full_version(): (version, timestamp) = running_os_release() return version # much like find . -name 'pattern' def locate(pattern, root=os.getcwd()): for path, dirs, files in os.walk(root): for f in files: if fnmatch.fnmatch(f, pattern): yield os.path.abspath(os.path.join(path, f)) def freespace(path): """Return the disk free space, in bytes""" s = os.statvfs(path) return s.f_bavail * s.f_bsize def disk_block_size(path): """Return the disk block size, in bytes""" return os.statvfs(path).f_bsize _DISK_PARTITION_3_RE = re.compile(r'^(/dev/hd[a-z]+)3', re.M) def get_disks(): df_output = utils.system_output('df') return _DISK_PARTITION_3_RE.findall(df_output) def get_disk_size(disk_name): """ Return size of disk in byte. Return 0 in Error Case @param disk_name: disk name to find size """ device = os.path.basename(disk_name) for line in file('/proc/partitions'): try: _, _, blocks, name = re.split(r' +', line.strip()) except ValueError: continue if name == device: return 1024 * int(blocks) return 0 def get_disk_size_gb(disk_name): """ Return size of disk in GB (10^9). Return 0 in Error Case @param disk_name: disk name to find size """ return int(get_disk_size(disk_name) / (10.0 ** 9) + 0.5) def get_disk_model(disk_name): """ Return model name for internal storage device @param disk_name: disk name to find model """ cmd1 = 'udevadm info --query=property --name=%s' % disk_name cmd2 = 'grep -E "ID_(NAME|MODEL)="' cmd3 = 'cut -f 2 -d"="' cmd = ' | '.join([cmd1, cmd2, cmd3]) return utils.system_output(cmd) _DISK_DEV_RE = re.compile(r'/dev/sd[a-z]|' r'/dev/mmcblk[0-9]+|' r'/dev/nvme[0-9]+n[0-9]+') def get_disk_from_filename(filename): """ Return the disk device the filename is on. If the file is on tmpfs or other special file systems, return None. @param filename: name of file, full path. """ if not os.path.exists(filename): raise error.TestError('file %s missing' % filename) if filename[0] != '/': raise error.TestError('This code works only with full path') m = _DISK_DEV_RE.match(filename) while not m: if filename[0] != '/': return None if filename == '/dev/root': cmd = 'rootdev -d -s' elif filename.startswith('/dev/mapper'): cmd = 'dmsetup table "%s"' % os.path.basename(filename) dmsetup_output = utils.system_output(cmd).split(' ') if dmsetup_output[2] == 'verity': maj_min = dmsetup_output[4] elif dmsetup_output[2] == 'crypt': maj_min = dmsetup_output[6] cmd = 'realpath "/dev/block/%s"' % maj_min elif filename.startswith('/dev/loop'): cmd = 'losetup -O BACK-FILE "%s" | tail -1' % filename else: cmd = 'df "%s" | tail -1 | cut -f 1 -d" "' % filename filename = utils.system_output(cmd) m = _DISK_DEV_RE.match(filename) return m.group(0) def get_disk_firmware_version(disk_name): """ Return firmware version for internal storage device. (empty string for eMMC) @param disk_name: disk name to find model """ cmd1 = 'udevadm info --query=property --name=%s' % disk_name cmd2 = 'grep -E "ID_REVISION="' cmd3 = 'cut -f 2 -d"="' cmd = ' | '.join([cmd1, cmd2, cmd3]) return utils.system_output(cmd) def is_disk_nvme(disk_name): """ Return true if disk is a nvme device, return false otherwise @param disk_name: disk name to check """ return re.match('/dev/nvme[0-9]+n[0-9]+', disk_name) def is_disk_scsi(disk_name): """ Return true if disk is a scsi device, return false otherwise @param disk_name: disk name check """ return re.match('/dev/sd[a-z]+', disk_name) def is_disk_harddisk(disk_name): """ Return true if disk is a harddisk, return false otherwise @param disk_name: disk name check """ cmd1 = 'udevadm info --query=property --name=%s' % disk_name cmd2 = 'grep -E "ID_ATA_ROTATION_RATE_RPM="' cmd3 = 'cut -f 2 -d"="' cmd = ' | '.join([cmd1, cmd2, cmd3]) rtt = utils.system_output(cmd) # eMMC will not have this field; rtt == '' # SSD will have zero rotation rate; rtt == '0' # For harddisk rtt > 0 return rtt and int(rtt) > 0 def concat_partition(disk_name, partition_number): """ Return the name of a partition: sda, 3 --> sda3 mmcblk0, 3 --> mmcblk0p3 @param disk_name: diskname string @param partition_number: integer """ if disk_name.endswith(tuple(str(i) for i in range(0, 10))): sep = 'p' else: sep = '' return disk_name + sep + str(partition_number) def verify_hdparm_feature(disk_name, feature): """ Check for feature support for SCSI disk using hdparm @param disk_name: target disk @param feature: hdparm output string of the feature """ cmd = 'hdparm -I %s | grep -q "%s"' % (disk_name, feature) ret = utils.system(cmd, ignore_status=True) if ret == 0: return True elif ret == 1: return False else: raise error.TestFail('Error running command %s' % cmd) def get_nvme_id_ns_feature(disk_name, feature): """ Return feature value for NVMe disk using nvme id-ns @param disk_name: target disk @param feature: output string of the feature """ cmd = "nvme id-ns -n 1 %s | grep %s" % (disk_name, feature) feat = utils.system_output(cmd, ignore_status=True) if not feat: return 'None' start = feat.find(':') value = feat[start+2:] return value def get_storage_error_msg(disk_name, reason): """ Get Error message for storage test which include disk model. and also include the firmware version for the SCSI disk @param disk_name: target disk @param reason: Reason of the error. """ msg = reason model = get_disk_model(disk_name) msg += ' Disk model: %s' % model if is_disk_scsi(disk_name): fw = get_disk_firmware_version(disk_name) msg += ' firmware: %s' % fw return msg _IOSTAT_FIELDS = ('transfers_per_s', 'read_kb_per_s', 'written_kb_per_s', 'read_kb', 'written_kb') _IOSTAT_RE = re.compile('ALL' + len(_IOSTAT_FIELDS) * r'\s+([\d\.]+)') def get_storage_statistics(device=None): """ Fetches statistics for a storage device. Using iostat(1) it retrieves statistics for a device since last boot. See the man page for iostat(1) for details on the different fields. @param device: Path to a block device. Defaults to the device where root is mounted. @returns a dict mapping each field to its statistic. @raises ValueError: If the output from iostat(1) can not be parsed. """ if device is None: device = get_root_device() cmd = 'iostat -d -k -g ALL -H %s' % device output = utils.system_output(cmd, ignore_status=True) match = _IOSTAT_RE.search(output) if not match: raise ValueError('Unable to get iostat for %s' % device) return dict(zip(_IOSTAT_FIELDS, map(float, match.groups()))) def load_module(module_name, params=None): # Checks if a module has already been loaded if module_is_loaded(module_name): return False cmd = '/sbin/modprobe ' + module_name if params: cmd += ' ' + params utils.system(cmd) return True def unload_module(module_name): """ Removes a module. Handles dependencies. If even then it's not possible to remove one of the modules, it will trhow an error.CmdError exception. @param module_name: Name of the module we want to remove. """ l_raw = utils.system_output("/bin/lsmod").splitlines() lsmod = [x for x in l_raw if x.split()[0] == module_name] if len(lsmod) > 0: line_parts = lsmod[0].split() if len(line_parts) == 4: submodules = line_parts[3].split(",") for submodule in submodules: unload_module(submodule) utils.system("/sbin/modprobe -r %s" % module_name) logging.info("Module %s unloaded", module_name) else: logging.info("Module %s is already unloaded", module_name) def module_is_loaded(module_name): module_name = module_name.replace('-', '_') modules = utils.system_output('/bin/lsmod').splitlines() for module in modules: if module.startswith(module_name) and module[len(module_name)] == ' ': return True return False def get_loaded_modules(): lsmod_output = utils.system_output('/bin/lsmod').splitlines()[1:] return [line.split(None, 1)[0] for line in lsmod_output] def get_huge_page_size(): output = utils.system_output('grep Hugepagesize /proc/meminfo') return int(output.split()[1]) # Assumes units always in kB. :( def get_num_huge_pages(): raw_hugepages = utils.system_output('/sbin/sysctl vm.nr_hugepages') return int(raw_hugepages.split()[2]) def set_num_huge_pages(num): utils.system('/sbin/sysctl vm.nr_hugepages=%d' % num) def ping_default_gateway(): """Ping the default gateway.""" network = open('/etc/sysconfig/network') m = re.search('GATEWAY=(\S+)', network.read()) if m: gw = m.group(1) cmd = 'ping %s -c 5 > /dev/null' % gw return utils.system(cmd, ignore_status=True) raise error.TestError('Unable to find default gateway') def drop_caches(): """Writes back all dirty pages to disk and clears all the caches.""" utils.system("sync") # We ignore failures here as this will fail on 2.6.11 kernels. utils.system("echo 3 > /proc/sys/vm/drop_caches", ignore_status=True) def process_is_alive(name_pattern): """ 'pgrep name' misses all python processes and also long process names. 'pgrep -f name' gets all shell commands with name in args. So look only for command whose initial pathname ends with name. Name itself is an egrep pattern, so it can use | etc for variations. """ return utils.system("pgrep -f '^([^ /]*/)*(%s)([ ]|$)'" % name_pattern, ignore_status=True) == 0 def get_hwclock_seconds(utc=True): """ Return the hardware clock in seconds as a floating point value. Use Coordinated Universal Time if utc is True, local time otherwise. Raise a ValueError if unable to read the hardware clock. """ cmd = '/sbin/hwclock --debug' if utc: cmd += ' --utc' hwclock_output = utils.system_output(cmd, ignore_status=True) match = re.search(r'= ([0-9]+) seconds since .+ (-?[0-9.]+) seconds$', hwclock_output, re.DOTALL) if match: seconds = int(match.group(1)) + float(match.group(2)) logging.debug('hwclock seconds = %f', seconds) return seconds raise ValueError('Unable to read the hardware clock -- ' + hwclock_output) def set_wake_alarm(alarm_time): """ Set the hardware RTC-based wake alarm to 'alarm_time'. """ utils.write_one_line('/sys/class/rtc/rtc0/wakealarm', str(alarm_time)) def set_power_state(state): """ Set the system power state to 'state'. """ utils.write_one_line('/sys/power/state', state) def standby(): """ Power-on suspend (S1) """ set_power_state('standby') def suspend_to_ram(): """ Suspend the system to RAM (S3) """ set_power_state('mem') def suspend_to_disk(): """ Suspend the system to disk (S4) """ set_power_state('disk') _AUTOTEST_CLIENT_PATH = os.path.join(os.path.dirname(__file__), '..') _AMD_PCI_IDS_FILE_PATH = os.path.join(_AUTOTEST_CLIENT_PATH, 'bin/amd_pci_ids.json') _INTEL_PCI_IDS_FILE_PATH = os.path.join(_AUTOTEST_CLIENT_PATH, 'bin/intel_pci_ids.json') _UI_USE_FLAGS_FILE_PATH = '/etc/ui_use_flags.txt' # Command to check if a package is installed. If the package is not installed # the command shall fail. _CHECK_PACKAGE_INSTALLED_COMMAND =( "dpkg-query -W -f='${Status}\n' %s | head -n1 | awk '{print $3;}' | " "grep -q '^installed$'") pciid_to_amd_architecture = {} pciid_to_intel_architecture = {} class Crossystem(object): """A wrapper for the crossystem utility.""" def __init__(self, client): self.cros_system_data = {} self._client = client def init(self): self.cros_system_data = {} (_, fname) = tempfile.mkstemp() f = open(fname, 'w') self._client.run('crossystem', stdout_tee=f) f.close() text = utils.read_file(fname) for line in text.splitlines(): assignment_string = line.split('#')[0] if not assignment_string.count('='): continue (name, value) = assignment_string.split('=', 1) self.cros_system_data[name.strip()] = value.strip() os.remove(fname) def __getattr__(self, name): """ Retrieve a crosssystem attribute. The call crossystemobject.name() will return the crossystem reported string. """ return lambda: self.cros_system_data[name] def get_oldest_pid_by_name(name): """ Return the oldest pid of a process whose name perfectly matches |name|. name is an egrep expression, which will be matched against the entire name of processes on the system. For example: get_oldest_pid_by_name('chrome') on a system running 8600 ? 00:00:04 chrome 8601 ? 00:00:00 chrome 8602 ? 00:00:00 chrome-sandbox would return 8600, as that's the oldest process that matches. chrome-sandbox would not be matched. Arguments: name: egrep expression to match. Will be anchored at the beginning and end of the match string. Returns: pid as an integer, or None if one cannot be found. Raises: ValueError if pgrep returns something odd. """ str_pid = utils.system_output('pgrep -o ^%s$' % name, ignore_status=True).rstrip() if str_pid: return int(str_pid) def get_oldest_by_name(name): """Return pid and command line of oldest process whose name matches |name|. @param name: egrep expression to match desired process name. @return: A tuple of (pid, command_line) of the oldest process whose name matches |name|. """ pid = get_oldest_pid_by_name(name) if pid: command_line = utils.system_output('ps -p %i -o command=' % pid, ignore_status=True).rstrip() return (pid, command_line) def get_chrome_remote_debugging_port(): """Returns remote debugging port for Chrome. Parse chrome process's command line argument to get the remote debugging port. if it is 0, look at DevToolsActivePort for the ephemeral port. """ _, command = get_oldest_by_name('chrome') matches = re.search('--remote-debugging-port=([0-9]+)', command) if not matches: return 0 port = int(matches.group(1)) if port: return port with open('/home/chronos/DevToolsActivePort') as f: return int(f.readline().rstrip()) def get_process_list(name, command_line=None): """ Return the list of pid for matching process |name command_line|. on a system running 31475 ? 0:06 /opt/google/chrome/chrome --allow-webui-compositing - 31478 ? 0:00 /opt/google/chrome/chrome-sandbox /opt/google/chrome/ 31485 ? 0:00 /opt/google/chrome/chrome --type=zygote --log-level=1 31532 ? 1:05 /opt/google/chrome/chrome --type=renderer get_process_list('chrome') would return ['31475', '31485', '31532'] get_process_list('chrome', '--type=renderer') would return ['31532'] Arguments: name: process name to search for. If command_line is provided, name is matched against full command line. If command_line is not provided, name is only matched against the process name. command line: when command line is passed, the full process command line is used for matching. Returns: list of PIDs of the matching processes. """ # TODO(rohitbm) crbug.com/268861 flag = '-x' if not command_line else '-f' name = '\'%s.*%s\'' % (name, command_line) if command_line else name str_pid = utils.system_output('pgrep %s %s' % (flag, name), ignore_status=True).rstrip() return str_pid.split() def nuke_process_by_name(name, with_prejudice=False): """Tell the oldest process specified by name to exit. Arguments: name: process name specifier, as understood by pgrep. with_prejudice: if True, don't allow for graceful exit. Raises: error.AutoservPidAlreadyDeadError: no existing process matches name. """ try: pid = get_oldest_pid_by_name(name) except Exception as e: logging.error(e) return if pid is None: raise error.AutoservPidAlreadyDeadError('No process matching %s.' % name) if with_prejudice: utils.nuke_pid(pid, [signal.SIGKILL]) else: utils.nuke_pid(pid) def ensure_processes_are_dead_by_name(name, timeout_sec=10): """Terminate all processes specified by name and ensure they're gone. Arguments: name: process name specifier, as understood by pgrep. timeout_sec: maximum number of seconds to wait for processes to die. Raises: error.AutoservPidAlreadyDeadError: no existing process matches name. utils.TimeoutError: if processes still exist after timeout_sec. """ def list_and_kill_processes(name): process_list = get_process_list(name) try: for pid in [int(str_pid) for str_pid in process_list]: utils.nuke_pid(pid) except error.AutoservPidAlreadyDeadError: pass return process_list utils.poll_for_condition(lambda: list_and_kill_processes(name) == [], timeout=timeout_sec) def is_virtual_machine(): if 'QEMU' in platform.processor(): return True try: with open('/sys/devices/virtual/dmi/id/sys_vendor') as f: if 'QEMU' in f.read(): return True except IOError: pass return False def save_vm_state(checkpoint): """Saves the current state of the virtual machine. This function is a NOOP if the test is not running under a virtual machine with the USB serial port redirected. Arguments: checkpoint - Name used to identify this state Returns: None """ # The QEMU monitor has been redirected to the guest serial port located at # /dev/ttyUSB0. To save the state of the VM, we just send the 'savevm' # command to the serial port. if is_virtual_machine() and os.path.exists('/dev/ttyUSB0'): logging.info('Saving VM state "%s"', checkpoint) serial = open('/dev/ttyUSB0', 'w') serial.write('savevm %s\r\n' % checkpoint) logging.info('Done saving VM state "%s"', checkpoint) def check_raw_dmesg(dmesg, message_level, whitelist): """Checks dmesg for unexpected warnings. This function parses dmesg for message with message_level <= message_level which do not appear in the whitelist. Arguments: dmesg - string containing raw dmesg buffer message_level - minimum message priority to check whitelist - messages to ignore Returns: List of unexpected warnings """ whitelist_re = re.compile(r'(%s)' % '|'.join(whitelist)) unexpected = [] for line in dmesg.splitlines(): if int(line[1]) <= message_level: stripped_line = line.split('] ', 1)[1] if whitelist_re.search(stripped_line): continue unexpected.append(stripped_line) return unexpected def verify_mesg_set(mesg, regex, whitelist): """Verifies that the exact set of messages are present in a text. This function finds all strings in the text matching a certain regex, and then verifies that all expected strings are present in the set, and no unexpected strings are there. Arguments: mesg - the mutiline text to be scanned regex - regular expression to match whitelist - messages to find in the output, a list of strings (potentially regexes) to look for in the filtered output. All these strings must be there, and no other strings should be present in the filtered output. Returns: string of inconsistent findings (i.e. an empty string on success). """ rv = [] missing_strings = [] present_strings = [] for line in mesg.splitlines(): if not re.search(r'%s' % regex, line): continue present_strings.append(line.split('] ', 1)[1]) for string in whitelist: for present_string in list(present_strings): if re.search(r'^%s$' % string, present_string): present_strings.remove(present_string) break else: missing_strings.append(string) if present_strings: rv.append('unexpected strings:') rv.extend(present_strings) if missing_strings: rv.append('missing strings:') rv.extend(missing_strings) return '\n'.join(rv) def target_is_pie(): """Returns whether the toolchain produces a PIE (position independent executable) by default. Arguments: None Returns: True if the target toolchain produces a PIE by default. False otherwise. """ command = 'echo | ${CC} -E -dD -P - | grep -i pie' result = utils.system_output(command, retain_output=True, ignore_status=True) if re.search('#define __PIE__', result): return True else: return False def target_is_x86(): """Returns whether the toolchain produces an x86 object Arguments: None Returns: True if the target toolchain produces an x86 object False otherwise. """ command = 'echo | ${CC} -E -dD -P - | grep -i 86' result = utils.system_output(command, retain_output=True, ignore_status=True) if re.search('__i386__', result) or re.search('__x86_64__', result): return True else: return False def mounts(): ret = [] for line in file('/proc/mounts'): m = re.match( r'(?P\S+) (?P\S+) (?P\S+) (?P\S+).*', line) if m: ret.append(m.groupdict()) return ret def is_mountpoint(path): return path in [m['dest'] for m in mounts()] def require_mountpoint(path): """ Raises an exception if path is not a mountpoint. """ if not is_mountpoint(path): raise error.TestFail('Path not mounted: "%s"' % path) def random_username(): return str(uuid.uuid4()) + '@example.com' def get_signin_credentials(filepath): """Returns user_id, password tuple from credentials file at filepath. File must have one line of the format user_id:password @param filepath: path of credentials file. @return user_id, password tuple. """ user_id, password = None, None if os.path.isfile(filepath): with open(filepath) as f: user_id, password = f.read().rstrip().split(':') return user_id, password def parse_cmd_output(command, run_method=utils.run): """Runs a command on a host object to retrieve host attributes. The command should output to stdout in the format of: = # @param command: Command to execute on the host. @param run_method: Function to use to execute the command. Defaults to utils.run so that the command will be executed locally. Can be replace with a host.run call so that it will execute on a DUT or external machine. Method must accept a command argument, stdout_tee and stderr_tee args and return a result object with a string attribute stdout which will be parsed. @returns a dictionary mapping host attributes to their values. """ result = {} # Suppresses stdout so that the files are not printed to the logs. cmd_result = run_method(command, stdout_tee=None, stderr_tee=None) for line in cmd_result.stdout.splitlines(): # Lines are of the format " = # " key_value = re.match(r'^\s*(?P[^ ]+)\s*=\s*(?P[^ ' r']+)(?:\s*#.*)?$', line) if key_value: result[key_value.group('key')] = key_value.group('value') return result def set_from_keyval_output(out, delimiter=' '): """Parse delimiter-separated key-val output into a set of tuples. Output is expected to be multiline text output from a command. Stuffs the key-vals into tuples in a set to be later compared. e.g. deactivated 0 disableForceClear 0 ==> set(('deactivated', '0'), ('disableForceClear', '0')) @param out: multiple lines of space-separated key-val pairs. @param delimiter: character that separates key from val. Usually a space but may be '=' or something else. @return set of key-val tuples. """ results = set() kv_match_re = re.compile('([^ ]+)%s(.*)' % delimiter) for linecr in out.splitlines(): match = kv_match_re.match(linecr.strip()) if match: results.add((match.group(1), match.group(2))) return results def get_cpu_usage(): """Returns machine's CPU usage. This function uses /proc/stat to identify CPU usage. Returns: A dictionary with values for all columns in /proc/stat Sample dictionary: { 'user': 254544, 'nice': 9, 'system': 254768, 'idle': 2859878, 'iowait': 1, 'irq': 2, 'softirq': 3, 'steal': 4, 'guest': 5, 'guest_nice': 6 } If a column is missing or malformed in /proc/stat (typically on older systems), the value for that column is set to 0. """ with _open_file('/proc/stat') as proc_stat: cpu_usage_str = proc_stat.readline().split() columns = ('user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice') d = {} for index, col in enumerate(columns, 1): try: d[col] = int(cpu_usage_str[index]) except: d[col] = 0 return d def compute_active_cpu_time(cpu_usage_start, cpu_usage_end): """Computes the fraction of CPU time spent non-idling. This function should be invoked using before/after values from calls to get_cpu_usage(). See https://stackoverflow.com/a/23376195 and https://unix.stackexchange.com/a/303224 for some more context how to calculate usage given two /proc/stat snapshots. """ idle_cols = ('idle', 'iowait') # All other cols are calculated as active. time_active_start = sum([x[1] for x in cpu_usage_start.iteritems() if x[0] not in idle_cols]) time_active_end = sum([x[1] for x in cpu_usage_end.iteritems() if x[0] not in idle_cols]) total_time_start = sum(cpu_usage_start.values()) total_time_end = sum(cpu_usage_end.values()) # Avoid bogus division which has been observed on Tegra. if total_time_end <= total_time_start: logging.warning('compute_active_cpu_time observed bogus data') # We pretend to be busy, this will force a longer wait for idle CPU. return 1.0 return ((float(time_active_end) - time_active_start) / (total_time_end - total_time_start)) def is_pgo_mode(): return 'USE_PGO' in os.environ def wait_for_idle_cpu(timeout, utilization): """Waits for the CPU to become idle (< utilization). Args: timeout: The longest time in seconds to wait before throwing an error. utilization: The CPU usage below which the system should be considered idle (between 0 and 1.0 independent of cores/hyperthreads). """ time_passed = 0.0 fraction_active_time = 1.0 sleep_time = 1 logging.info('Starting to wait up to %.1fs for idle CPU...', timeout) while fraction_active_time >= utilization: cpu_usage_start = get_cpu_usage() # Split timeout interval into not too many chunks to limit log spew. # Start at 1 second, increase exponentially time.sleep(sleep_time) time_passed += sleep_time sleep_time = min(16.0, 2.0 * sleep_time) cpu_usage_end = get_cpu_usage() fraction_active_time = compute_active_cpu_time(cpu_usage_start, cpu_usage_end) logging.info('After waiting %.1fs CPU utilization is %.3f.', time_passed, fraction_active_time) if time_passed > timeout: logging.warning('CPU did not become idle.') log_process_activity() # crosbug.com/37389 if is_pgo_mode(): logging.info('Still continuing because we are in PGO mode.') return True return False logging.info('Wait for idle CPU took %.1fs (utilization = %.3f).', time_passed, fraction_active_time) return True def log_process_activity(): """Logs the output of top. Useful to debug performance tests and to find runaway processes. """ logging.info('Logging current process activity using top and ps.') cmd = 'top -b -n1 -c' output = utils.run(cmd) logging.info(output) output = utils.run('ps axl') logging.info(output) def wait_for_cool_machine(): """ A simple heuristic to wait for a machine to cool. The code looks a bit 'magic', but we don't know ambient temperature nor machine characteristics and still would like to return the caller a machine that cooled down as much as reasonably possible. """ temperature = get_current_temperature_max() # We got here with a cold machine, return immediately. This should be the # most common case. if temperature < 50: return True logging.info('Got a hot machine of %dC. Sleeping 1 minute.', temperature) # A modest wait should cool the machine. time.sleep(60.0) temperature = get_current_temperature_max() # Atoms idle below 60 and everyone else should be even lower. if temperature < 62: return True # This should be rare. logging.info('Did not cool down (%dC). Sleeping 2 minutes.', temperature) time.sleep(120.0) temperature = get_current_temperature_max() # A temperature over 65'C doesn't give us much headroom to the critical # temperatures that start at 85'C (and PerfControl as of today will fail at # critical - 10'C). if temperature < 65: return True logging.warning('Did not cool down (%dC), giving up.', temperature) log_process_activity() return False # System paths for machine performance state. _CPUINFO = '/proc/cpuinfo' _DIRTY_WRITEBACK_CENTISECS = '/proc/sys/vm/dirty_writeback_centisecs' _KERNEL_MAX = '/sys/devices/system/cpu/kernel_max' _MEMINFO = '/proc/meminfo' _TEMP_SENSOR_RE = 'Reading temperature...([0-9]*)' def _open_file(path): """ Opens a file and returns the file object. This method is intended to be mocked by tests. @return The open file object. """ return open(path) def _get_line_from_file(path, line): """ line can be an integer or line can be a string that matches the beginning of the line """ with _open_file(path) as f: if isinstance(line, int): l = f.readline() for _ in range(0, line): l = f.readline() return l else: for l in f: if l.startswith(line): return l return None def _get_match_from_file(path, line, prefix, postfix): """ Matches line in path and returns string between first prefix and postfix. """ match = _get_line_from_file(path, line) # Strip everything from front of line including prefix. if prefix: match = re.split(prefix, match)[1] # Strip everything from back of string including first occurence of postfix. if postfix: match = re.split(postfix, match)[0] return match def _get_float_from_file(path, line, prefix, postfix): match = _get_match_from_file(path, line, prefix, postfix) return float(match) def _get_int_from_file(path, line, prefix, postfix): match = _get_match_from_file(path, line, prefix, postfix) return int(match) def _get_hex_from_file(path, line, prefix, postfix): match = _get_match_from_file(path, line, prefix, postfix) return int(match, 16) # The paths don't change. Avoid running find all the time. _hwmon_paths = None def _get_hwmon_paths(file_pattern): """ Returns a list of paths to the temperature sensors. """ # Some systems like daisy_spring only have the virtual hwmon. # And other systems like rambi only have coretemp.0. See crbug.com/360249. # /sys/class/hwmon/hwmon*/ # /sys/devices/virtual/hwmon/hwmon*/ # /sys/devices/platform/coretemp.0/ if not _hwmon_paths: cmd = 'find /sys/ -name "' + file_pattern + '"' _hwon_paths = utils.run(cmd, verbose=False).stdout.splitlines() return _hwon_paths def get_temperature_critical(): """ Returns temperature at which we will see some throttling in the system. """ min_temperature = 1000.0 paths = _get_hwmon_paths('temp*_crit') for path in paths: temperature = _get_float_from_file(path, 0, None, None) * 0.001 # Today typical for Intel is 98'C to 105'C while ARM is 85'C. Clamp to 98 # if Intel device or the lowest known value otherwise. result = utils.system_output('crossystem arch', retain_output=True, ignore_status=True) if (min_temperature < 60.0) or min_temperature > 150.0: if 'x86' in result: min_temperature = 98.0 else: min_temperature = 85.0 logging.warning('Critical temperature was reset to %.1fC.', min_temperature) min_temperature = min(temperature, min_temperature) return min_temperature def get_temperature_input_max(): """ Returns the maximum currently observed temperature. """ max_temperature = -1000.0 paths = _get_hwmon_paths('temp*_input') for path in paths: temperature = _get_float_from_file(path, 0, None, None) * 0.001 max_temperature = max(temperature, max_temperature) return max_temperature def get_thermal_zone_temperatures(): """ Returns the maximum currently observered temperature in thermal_zones. """ temperatures = [] for path in glob.glob('/sys/class/thermal/thermal_zone*/temp'): try: temperatures.append( _get_float_from_file(path, 0, None, None) * 0.001) except IOError: # Some devices (e.g. Veyron) may have reserved thermal zones that # are not active. Trying to read the temperature value would cause a # EINVAL IO error. continue return temperatures def get_ec_temperatures(): """ Uses ectool to return a list of all sensor temperatures in Celsius. Output from ectool is either '0: 300' or '0: 300 K' (newer ectool includes the unit). """ temperatures = [] try: full_cmd = 'ectool temps all' lines = utils.run(full_cmd, verbose=False).stdout.splitlines() pattern = re.compile('.*: (\d+)') for line in lines: matched = pattern.match(line) temperature = int(matched.group(1)) - 273 temperatures.append(temperature) except Exception: logging.warning('Unable to read temperature sensors using ectool.') for temperature in temperatures: # Sanity check for real world values. assert ((temperature > 10.0) and (temperature < 150.0)), ('Unreasonable temperature %.1fC.' % temperature) return temperatures def get_current_temperature_max(): """ Returns the highest reported board temperature (all sensors) in Celsius. """ temperature = max([get_temperature_input_max()] + get_thermal_zone_temperatures() + get_ec_temperatures()) # Sanity check for real world values. assert ((temperature > 10.0) and (temperature < 150.0)), ('Unreasonable temperature %.1fC.' % temperature) return temperature def get_cpu_cache_size(): """ Returns the last level CPU cache size in kBytes. """ cache_size = _get_int_from_file(_CPUINFO, 'cache size', ': ', ' KB') # Sanity check. assert cache_size >= 64, 'Unreasonably small cache.' return cache_size def get_cpu_model_frequency(): """ Returns the model frequency from the CPU model name on Intel only. This might be redundant with get_cpu_max_frequency. Unit is Hz. """ frequency = _get_float_from_file(_CPUINFO, 'model name', ' @ ', 'GHz') return 1.e9 * frequency def get_cpu_max_frequency(): """ Returns the largest of the max CPU core frequencies. The unit is Hz. """ max_frequency = -1 paths = utils._get_cpufreq_paths('cpuinfo_max_freq') if not paths: raise ValueError('Could not find max freq; is cpufreq supported?') for path in paths: try: # Convert from kHz to Hz. frequency = 1000 * _get_float_from_file(path, 0, None, None) # CPUs may come and go. A missing entry or two aren't critical. except IOError: continue max_frequency = max(frequency, max_frequency) # Sanity check. assert max_frequency > 1e8, ('Unreasonably low CPU frequency: %.1f' % max_frequency) return max_frequency def get_cpu_model(): """ Returns the CPU model. Only works on Intel. """ cpu_model = _get_int_from_file(_CPUINFO, 'model\t', ': ', None) return cpu_model def get_cpu_family_intel(): """ Returns the CPU family. Only works on Intel. """ cpu_family = _get_int_from_file(_CPUINFO, 'cpu family\t', ': ', None) return cpu_family def get_board_property(key): """ Get a specific property from /etc/lsb-release. @param key: board property to return value for @return the value or '' if not present """ with open('/etc/lsb-release') as f: pattern = '%s=(.*)' % key pat = re.search(pattern, f.read()) if pat: return pat.group(1) return '' def get_board(): """ Get the ChromeOS release board name from /etc/lsb-release. """ return get_board_property('BOARD') def get_board_type(): """ Get the ChromeOS board type from /etc/lsb-release. @return device type. """ return get_board_property('DEVICETYPE') def get_platform(): """ Get the ChromeOS platform name. For unibuild this should be equal to model name. For non-unibuild it will either be board name or empty string. In the case of empty string return board name to match equivalent logic in server/hosts/cros_host.py @returns platform name """ platform = '' command = 'mosys platform model' result = utils.run(command, ignore_status=True) if result.exit_status == 0: platform = result.stdout.strip() if platform == '': platform = get_board() return platform def get_ec_version(): """Get the ec version as strings. @returns a string representing this host's ec version. """ command = 'mosys ec info -s fw_version' result = utils.run(command, ignore_status=True) if result.exit_status != 0: return '' return result.stdout.strip() def get_firmware_version(): """Get the firmware version as strings. @returns a string representing this host's firmware version. """ return utils.run('crossystem fwid').stdout.strip() def get_hardware_revision(): """Get the hardware revision as strings. @returns a string representing this host's hardware revision. """ command = 'mosys platform version' result = utils.run(command, ignore_status=True) if result.exit_status != 0: return '' return result.stdout.strip() def get_kernel_version(): """Get the kernel version as strings. @returns a string representing this host's kernel version. """ return utils.run('uname -r').stdout.strip() def get_cpu_name(): """Get the cpu name as strings. @returns a string representing this host's cpu name. """ # Try get cpu name from device tree first if os.path.exists("/proc/device-tree/compatible"): command = "sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible | tail -1" return utils.run(command).stdout.strip().replace(',', ' ') # Get cpu name from uname -p command = "uname -p" ret = utils.run(command).stdout.strip() # 'uname -p' return variant of unknown or amd64 or x86_64 or i686 # Try get cpu name from /proc/cpuinfo instead if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE): command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1" ret = utils.run(command).stdout.strip() # Remove bloat from CPU name, for example # 'Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz' -> 'Intel Core i5-7Y57' # 'Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz' -> 'Intel Xeon E5-2690 v4' # 'AMD A10-7850K APU with Radeon(TM) R7 Graphics' -> 'AMD A10-7850K' # 'AMD GX-212JC SOC with Radeon(TM) R2E Graphics' -> 'AMD GX-212JC' trim_re = " (@|processor|apu|soc|radeon).*|\(.*?\)| cpu" return re.sub(trim_re, '', ret, flags=re.IGNORECASE) def get_screen_resolution(): """Get the screen(s) resolution as strings. In case of more than 1 monitor, return resolution for each monitor separate with plus sign. @returns a string representing this host's screen(s) resolution. """ command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done' ret = utils.run(command, ignore_status=True) # We might have Chromebox without a screen if ret.exit_status != 0: return '' return ret.stdout.strip().replace('\n', '+') def get_board_with_frequency_and_memory(): """ Returns a board name modified with CPU frequency and memory size to differentiate between different board variants. For instance link -> link_1.8GHz_4GB. """ board_name = get_board() if is_virtual_machine(): board = '%s_VM' % board_name else: memory = get_mem_total_gb() # Convert frequency to GHz with 1 digit accuracy after the # decimal point. frequency = int(round(get_cpu_max_frequency() * 1e-8)) * 0.1 board = '%s_%1.1fGHz_%dGB' % (board_name, frequency, memory) return board def get_mem_total(): """ Returns the total memory available in the system in MBytes. """ mem_total = _get_float_from_file(_MEMINFO, 'MemTotal:', 'MemTotal:', ' kB') # Sanity check, all Chromebooks have at least 1GB of memory. assert mem_total > 256 * 1024, 'Unreasonable amount of memory.' return mem_total / 1024 def get_mem_total_gb(): """ Returns the total memory available in the system in GBytes. """ return int(round(get_mem_total() / 1024.0)) def get_mem_free(): """ Returns the currently free memory in the system in MBytes. """ mem_free = _get_float_from_file(_MEMINFO, 'MemFree:', 'MemFree:', ' kB') return mem_free / 1024 def get_mem_free_plus_buffers_and_cached(): """ Returns the free memory in MBytes, counting buffers and cached as free. This is most often the most interesting number since buffers and cached memory can be reclaimed on demand. Note however, that there are cases where this as misleading as well, for example used tmpfs space count as Cached but can not be reclaimed on demand. See https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt. """ free_mb = get_mem_free() cached_mb = (_get_float_from_file( _MEMINFO, 'Cached:', 'Cached:', ' kB') / 1024) buffers_mb = (_get_float_from_file( _MEMINFO, 'Buffers:', 'Buffers:', ' kB') / 1024) return free_mb + buffers_mb + cached_mb def get_kernel_max(): """ Returns content of kernel_max. """ kernel_max = _get_int_from_file(_KERNEL_MAX, 0, None, None) # Sanity check. assert ((kernel_max > 0) and (kernel_max < 257)), 'Unreasonable kernel_max.' return kernel_max def get_dirty_writeback_centisecs(): """ Reads /proc/sys/vm/dirty_writeback_centisecs. """ time = _get_int_from_file(_DIRTY_WRITEBACK_CENTISECS, 0, None, None) return time def set_dirty_writeback_centisecs(time=60000): """ In hundredths of a second, this is how often pdflush wakes up to write data to disk. The default wakes up the two (or more) active threads every five seconds. The ChromeOS default is 10 minutes. We use this to set as low as 1 second to flush error messages in system logs earlier to disk. """ # Flush buffers first to make this function synchronous. utils.system('sync') if time >= 0: cmd = 'echo %d > %s' % (time, _DIRTY_WRITEBACK_CENTISECS) utils.system(cmd) def wflinfo_cmd(): """ Returns a wflinfo command appropriate to the current graphics platform/api. """ return 'wflinfo -p %s -a %s' % (graphics_platform(), graphics_api()) def has_mali(): """ @return: True if system has a Mali GPU enabled.""" return os.path.exists('/dev/mali0') def get_gpu_family(): """Returns the GPU family name.""" global pciid_to_amd_architecture global pciid_to_intel_architecture socfamily = get_cpu_soc_family() if socfamily == 'exynos5' or socfamily == 'rockchip' or has_mali(): cmd = wflinfo_cmd() wflinfo = utils.system_output(cmd, retain_output=True, ignore_status=False) version = re.findall(r'OpenGL renderer string: ' r'Mali-T([0-9]+)', wflinfo) if version: return 'mali-t%s' % version[0] return 'mali-unrecognized' if socfamily == 'tegra': return 'tegra' if socfamily == 'qualcomm': return 'qualcomm' if os.path.exists('/sys/kernel/debug/pvr'): return 'rogue' pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n') bus_device_function = pci_vga_device.partition(' ')[0] pci_path = '/sys/bus/pci/devices/0000:' + bus_device_function + '/device' if not os.path.exists(pci_path): raise error.TestError('PCI device 0000:' + bus_device_function + ' not found') device_id = utils.read_one_line(pci_path).lower() if "Advanced Micro Devices" in pci_vga_device: if not pciid_to_amd_architecture: with open(_AMD_PCI_IDS_FILE_PATH, 'r') as in_f: pciid_to_amd_architecture = json.load(in_f) return pciid_to_amd_architecture[device_id] if "Intel Corporation" in pci_vga_device: # Only load Intel PCI ID file once and only if necessary. if not pciid_to_intel_architecture: with open(_INTEL_PCI_IDS_FILE_PATH, 'r') as in_f: pciid_to_intel_architecture = json.load(in_f) return pciid_to_intel_architecture[device_id] # TODO(ihf): Consider using /etc/lsb-release DEVICETYPE != CHROMEBOOK/CHROMEBASE # for sanity check, but usage seems a bit inconsistent. See # src/third_party/chromiumos-overlay/eclass/appid.eclass _BOARDS_WITHOUT_MONITOR = [ 'anglar', 'mccloud', 'monroe', 'ninja', 'rikku', 'guado', 'jecht', 'tidus', 'beltino', 'panther', 'stumpy', 'panther', 'tricky', 'zako', 'veyron_rialto' ] def has_no_monitor(): """Returns whether a machine doesn't have a built-in monitor.""" board_name = get_board() if board_name in _BOARDS_WITHOUT_MONITOR: return True return False def get_fixed_dst_drive(): """ Return device name for internal disk. Example: return /dev/sda for falco booted from usb """ cmd = ' '.join(['. /usr/sbin/write_gpt.sh;', '. /usr/share/misc/chromeos-common.sh;', 'load_base_vars;', 'get_fixed_dst_drive']) return utils.system_output(cmd) def get_root_device(): """ Return root device. Will return correct disk device even system boot from /dev/dm-0 Example: return /dev/sdb for falco booted from usb """ return utils.system_output('rootdev -s -d') def get_root_partition(): """ Return current root partition Example: return /dev/sdb3 for falco booted from usb """ return utils.system_output('rootdev -s') def get_free_root_partition(root_part=None): """ Return currently unused root partion Example: return /dev/sdb5 for falco booted from usb @param root_part: cuurent root partition """ spare_root_map = {'3': '5', '5': '3'} if not root_part: root_part = get_root_partition() return root_part[:-1] + spare_root_map[root_part[-1]] def get_kernel_partition(root_part=None): """ Return current kernel partition Example: return /dev/sda2 for falco booted from usb @param root_part: current root partition """ if not root_part: root_part = get_root_partition() current_kernel_map = {'3': '2', '5': '4'} return root_part[:-1] + current_kernel_map[root_part[-1]] def get_free_kernel_partition(root_part=None): """ return currently unused kernel partition Example: return /dev/sda4 for falco booted from usb @param root_part: current root partition """ kernel_part = get_kernel_partition(root_part) spare_kernel_map = {'2': '4', '4': '2'} return kernel_part[:-1] + spare_kernel_map[kernel_part[-1]] def is_booted_from_internal_disk(): """Return True if boot from internal disk. False, otherwise.""" return get_root_device() == get_fixed_dst_drive() def get_ui_use_flags(): """Parses the USE flags as listed in /etc/ui_use_flags.txt. @return: A list of flag strings found in the ui use flags file. """ flags = [] for flag in utils.read_file(_UI_USE_FLAGS_FILE_PATH).splitlines(): # Removes everything after the '#'. flag_before_comment = flag.split('#')[0].strip() if len(flag_before_comment) != 0: flags.append(flag_before_comment) return flags def graphics_platform(): """ Return a string identifying the graphics platform, e.g. 'glx' or 'x11_egl' or 'gbm' """ return 'null' def graphics_api(): """Return a string identifying the graphics api, e.g. gl or gles2.""" use_flags = get_ui_use_flags() if 'opengles' in use_flags: return 'gles2' return 'gl' def is_package_installed(package): """Check if a package is installed already. @return: True if the package is already installed, otherwise return False. """ try: utils.run(_CHECK_PACKAGE_INSTALLED_COMMAND % package) return True except error.CmdError: logging.warn('Package %s is not installed.', package) return False def is_python_package_installed(package): """Check if a Python package is installed already. @return: True if the package is already installed, otherwise return False. """ try: __import__(package) return True except ImportError: logging.warn('Python package %s is not installed.', package) return False def run_sql_cmd(server, user, password, command, database=''): """Run the given sql command against the specified database. @param server: Hostname or IP address of the MySQL server. @param user: User name to log in the MySQL server. @param password: Password to log in the MySQL server. @param command: SQL command to run. @param database: Name of the database to run the command. Default to empty for command that does not require specifying database. @return: The stdout of the command line. """ cmd = ('mysql -u%s -p%s --host %s %s -e "%s"' % (user, password, server, database, command)) # Set verbose to False so the command line won't be logged, as it includes # database credential. return utils.run(cmd, verbose=False).stdout def strip_non_printable(s): """Strip non printable characters from string. @param s: Input string @return: The input string with only printable characters. """ return ''.join(x for x in s if x in string.printable) def recursive_func(obj, func, types, sequence_types=(list, tuple, set), dict_types=(dict,), fix_num_key=False): """Apply func to obj recursively. This function traverses recursively through any sequence-like and dict-like elements in obj. @param obj: the object to apply the function func recursively. @param func: the function to invoke. @param types: the target types in the object to apply func. @param sequence_types: the sequence types in python. @param dict_types: the dict types in python. @param fix_num_key: to indicate if the key of a dict should be converted from str type to a number, int or float, type. It is a culprit of json that it always treats the key of a dict as string. Refer to https://docs.python.org/2/library/json.html for more information. @return: the result object after applying the func recursively. """ def ancestors(obj, types): """Any ancestor of the object class is a subclass of the types? @param obj: the object to apply the function func. @param types: the target types of the object. @return: True if any ancestor class of the obj is found in types; False otherwise. """ return any([issubclass(anc, types) for anc in type(obj).__mro__]) if isinstance(obj, sequence_types) or ancestors(obj, sequence_types): result_lst = [recursive_func(elm, func, types, fix_num_key=fix_num_key) for elm in obj] # Convert the result list to the object's original sequence type. return type(obj)(result_lst) elif isinstance(obj, dict_types) or ancestors(obj, dict_types): result_lst = [ (recursive_func(key, func, types, fix_num_key=fix_num_key), recursive_func(value, func, types, fix_num_key=fix_num_key)) for (key, value) in obj.items()] # Convert the result list to the object's original dict type. return type(obj)(result_lst) # Here are the basic types. elif isinstance(obj, types) or ancestors(obj, types): if fix_num_key: # Check if this is a int or float try: result_obj = int(obj) return result_obj except ValueError: try: result_obj = float(obj) return result_obj except ValueError: pass result_obj = func(obj) return result_obj else: return obj def base64_recursive_encode(obj): """Apply base64 encode recursively into the obj structure. Most of the string-like types could be traced to basestring and bytearray as follows: str: basestring bytes: basestring dbus.String: basestring dbus.Signature: basestring dbus.ByteArray: basestring Note that all the above types except dbus.String could be traced back to str. In order to cover dbus.String, basestring is used as the ancestor class for string-like types. The other type that needs encoding with base64 in a structure includes bytearray: bytearray The sequence types include (list, tuple, set). The dbus.Array is also covered as dbus.Array: list The base dictionary type is dict. The dbus.Dictionary is also covered as dbus.Dictionary: dict An example code and output look like obj = {'a': 10, 'b': 'hello', 'c': [100, 200, bytearray(b'\xf0\xf1\xf2\xf3\xf4')], 'd': {784: bytearray(b'@\x14\x01P'), 78.0: bytearray(b'\x10\x05\x0b\x10\xb2\x1b\x00')}} encode_obj = base64_recursive_encode(obj) decode_obj = base64_recursive_decode(encode_obj) print 'obj: ', obj print 'encode_obj: ', encode_obj print 'decode_obj: ', decode_obj print 'Equal?', obj == decode_obj Output: obj: {'a': 10, 'c': [100, 200, bytearray(b'\xf0\xf1\xf2\xf3\xf4')], 'b': 'hello', 'd': {784: bytearray(b'@\x14\x01P'), 78.0: bytearray(b'\x10\x05\x0b\x10\xb2\x1b\x00')}} encode_obj: {'YQ==': 10, 'Yw==': [100, 200, '8PHy8/Q='], 'Yg==': 'aGVsbG8=' 'ZA==': {784: 'QBQBUA==', 78.0: 'EAULELIbAA=='}} decode_obj: {'a': 10, 'c': [100, 200, '\xf0\xf1\xf2\xf3\xf4'], 'b': 'hello', 'd': {784: '@\x14\x01P', 78.0: '\x10\x05\x0b\x10\xb2\x1b\x00'}} Equal? True @param obj: the object to apply base64 encoding recursively. @return: the base64 encoded object. """ encode_types = (basestring, bytearray) return recursive_func(obj, base64.standard_b64encode, encode_types) def base64_recursive_decode(obj): """Apply base64 decode recursively into the obj structure. @param obj: the object to apply base64 decoding recursively. @return: the base64 decoded object. """ decode_types = (basestring,) return recursive_func(obj, base64.standard_b64decode, decode_types, fix_num_key=True)