# Copyright (c) 2010 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. """A module to provide interface to OS services.""" import datetime import errno import logging import os import re import struct import shell_wrapper class OSInterfaceError(Exception): """OS interface specific exception.""" pass class Crossystem(object): """A wrapper for the crossystem utility.""" # Code dedicated for user triggering recovery mode through crossystem. USER_RECOVERY_REQUEST_CODE = '193' def __init__(self, os_if): """Init the instance. If running on Mario - adjust the map.""" self.os_if = os_if def __getattr__(self, name): """ Retrieve a crosssystem attribute. Attempt to access crossystemobject.name will invoke `crossystem name' and return the stdout as the value. """ return self.os_if.run_shell_command_get_output( 'crossystem %s' % name)[0] def __setattr__(self, name, value): if name in ('os_if', ): self.__dict__[name] = value else: self.os_if.run_shell_command( 'crossystem "%s=%s"' % (name, value), modifies_device=True) def request_recovery(self): """Request recovery mode next time the target reboots.""" self.__setattr__('recovery_request', self.USER_RECOVERY_REQUEST_CODE) class OSInterface(object): """An object to encapsulate OS services functions.""" def __init__(self, state_dir=None, log_file=None, test_mode=False): """Object initialization (side effect: creates the state_dir) @param state_dir: the name of the directory to use for storing state. The contents of this directory persist over system restarts and power cycles. @param log_file: the name of the log file kept in the state directory. @param test_mode: if true, skip (and just log) any shell call marked with modifies_device=True """ # We keep the state of FAFT test in a permanent directory over reboots. if state_dir is None: state_dir = '/usr/local/tmp/faft' if log_file is None: log_file = 'faft_client.log' if not os.path.isabs(log_file): log_file = os.path.join(state_dir, log_file) self.state_dir = state_dir self.log_file = log_file self.test_mode = test_mode self._use_log_file = False self.shell = shell_wrapper.LocalShell(self) self.host_shell = None self.create_dir(self.state_dir) self.cs = Crossystem(self) def run_shell_command(self, cmd, block=True, modifies_device=False): """Run a shell command. @param cmd: the command to run @param block: if True (default), wait for command to finish @param modifies_device: If True and running in test mode, just log the command, but don't actually run it. This should be set for RPC commands that alter the OS or firmware in some persistent way. @raise autotest_lib.client.common_lib.error.CmdError: if command fails """ if self.test_mode and modifies_device: self.log('[SKIPPED] %s' % cmd) else: self.shell.run_command(cmd, block=block) def run_shell_command_check_output(self, cmd, success_token): """Run shell command and check its stdout for a string.""" return self.shell.run_command_check_output(cmd, success_token) def run_shell_command_get_result(self, cmd, ignore_status=False): """Run shell command and get a CmdResult object as a result. @param cmd: the command to run @param ignore_status: if True, do not raise CmdError, even if rc != 0. @rtype: autotest_lib.client.common_lib.utils.CmdResult @raise autotest_lib.client.common_lib.error.CmdError: if command fails """ return self.shell.run_command_get_result(cmd, ignore_status) def run_shell_command_get_status(self, cmd): """Run shell command and return its return code.""" return self.shell.run_command_get_status(cmd) def run_shell_command_get_output(self, cmd, include_stderr=False): """Run shell command and return its console output.""" return self.shell.run_command_get_output(cmd, include_stderr) def read_file(self, path): """Read the content of the file.""" return self.shell.read_file(path) def write_file(self, path, data): """Write the data to the file.""" self.shell.write_file(path, data) def append_file(self, path, data): """Append the data to the file.""" self.shell.append_file(path, data) def path_exists(self, path): """Return True if the path exists on DUT.""" cmd = 'test -e %s' % path return self.run_shell_command_get_status(cmd) == 0 def is_dir(self, path): """Return True if the path is a directory.""" cmd = 'test -d %s' % path return self.run_shell_command_get_status(cmd) == 0 def create_dir(self, path): """Create a new directory.""" cmd = 'mkdir -p %s' % path return self.run_shell_command(cmd) def create_temp_file(self, prefix): """Create a temporary file with a prefix.""" tmp_path = '/tmp' cmd = 'mktemp -p %s %sXXXXXX' % (tmp_path, prefix) return self.run_shell_command_get_output(cmd)[0] def copy_file(self, from_path, to_path): """Copy the file.""" cmd = 'cp -f %s %s' % (from_path, to_path) return self.run_shell_command(cmd) def copy_dir(self, from_path, to_path): """Copy the directory.""" cmd = 'cp -rf %s %s' % (from_path, to_path) return self.run_shell_command(cmd) def remove_file(self, path): """Remove the file.""" cmd = 'rm -f %s' % path return self.run_shell_command(cmd) def remove_dir(self, path): """Remove the directory.""" cmd = 'rm -rf %s' % path return self.run_shell_command(cmd) def get_file_size(self, path): """Get the size of the file.""" cmd = 'stat -c %%s %s' % path return int(self.run_shell_command_get_output(cmd)[0]) def target_hosted(self): """Return True if running on DUT.""" with open('/etc/lsb-release', 'r') as lsb_release: signature = lsb_release.readlines()[0] return bool(re.search(r'chrom(ium|e)os', signature, re.IGNORECASE)) def state_dir_file(self, file_name): """Get a full path of a file in the state directory.""" return os.path.join(self.state_dir, file_name) def log(self, text): """Write text to the log file and print it on the screen, if enabled. The entire log (kept across reboots) can be found in self.log_file. """ if not self._use_log_file: # Called during init, during shutdown, or after a log write fails. logging.info('%s', text) return timestamp = datetime.datetime.strftime(datetime.datetime.now(), '%I:%M:%S %p:') try: with open(self.log_file, 'a') as log_f: log_f.write('%s %s\n' % (timestamp, text)) log_f.flush() os.fdatasync(log_f.fileno()) except EnvironmentError: logging.info('%s', text) logging.warn("Couldn't write RPC Log: %s", self.log_file, exc_info=True) # Report error only once. self._use_log_file = False def start_file_logging(self): """Create and start using using the log file (or report failure)""" if self._use_log_file: return try: with open(self.log_file, 'a'): self._use_log_file = True # log to stderr, showing the filename (extra newline to add a gap) logging.debug('Begin RPC Log: %s\n', self.log_file) # log into the file, to indicate the start time self.log('Begin RPC Log: %s (this file)' % self.log_file) except EnvironmentError: logging.warn("Couldn't write RPC Log: %s", self.log_file, exc_info=True) self._use_log_file = False def stop_file_logging(self): """Stop using the log file (switch back to stderr).""" if not self._use_log_file: return # log to the file, to indicate when done (extra newline to add a gap) self.log('End RPC Log.\n') self._use_log_file = False # log to stderr, to tie timestamps together logging.debug('End RPC Log.') def remove_log_file(self): """Delete the log file.""" if not self.test_mode: # Test mode shouldn't be able to actually remove the log. try: os.remove(self.log_file) except EnvironmentError as e: if e.errno != errno.ENOENT: self.log("Could not remove log file: %s" % e) def dump_log(self, remove_log=False): """Dump the log file. @param remove_log: Remove the log file after dump @return: String of the log file content. """ if remove_log and not self.test_mode: # Make sure "end RPC log" is printed before grabbing the log self.stop_file_logging() try: with open(self.log_file, 'r') as f: log = f.read() except EnvironmentError as e: log = '<%s>' % e if remove_log and not self.test_mode: self.remove_log_file() return log def is_removable_device(self, device): """Check if a certain storage device is removable. device - a string, file name of a storage device or a device partition (as in /dev/sda[0-9] or /dev/mmcblk0p[0-9]). Returns True if the device is removable, False if not. """ if not self.target_hosted(): return False # Drop trailing digit(s) and letter(s) (if any) base_dev = self.strip_part(device.split('/')[2]) removable = int(self.read_file('/sys/block/%s/removable' % base_dev)) return removable == 1 def get_internal_disk(self, device): """Get the internal disk by given the current disk. If device is removable device, internal disk is decided by which kind of divice (arm or x86). Otherwise, return device itself. device - a string, file name of a storage device or a device partition (as in /dev/sda[0-9] or /dev/mmcblk0p[0-9]). Return internal kernel disk. """ if self.is_removable_device(device): for p in ('/dev/mmcblk0', '/dev/mmcblk1', '/dev/nvme0n1'): if self.path_exists(p): devicetype = '/sys/block/%s/device/type' % p.split('/')[2] if (not self.path_exists(devicetype) or self.read_file(devicetype).strip() != 'SD'): return p return '/dev/sda' else: return self.strip_part(device) def get_root_part(self): """Return a string, the name of root device with partition number""" return self.run_shell_command_get_output('rootdev -s')[0] def get_root_dev(self): """Return a string, the name of root device without partition number""" return self.strip_part(self.get_root_part()) def join_part(self, dev, part): """Return a concatenated string of device and partition number""" if dev.endswith(tuple(str(i) for i in range(0, 10))): return dev + 'p' + part else: return dev + part def strip_part(self, dev_with_part): """Return a stripped string without partition number""" dev_name_stripper = re.compile('p?[0-9]+$') return dev_name_stripper.sub('', dev_with_part) def retrieve_body_version(self, blob): """Given a blob, retrieve body version. Currently works for both, firmware and kernel blobs. Returns '-1' in case the version can not be retrieved reliably. """ header_format = '<8s8sQ' preamble_format = '<40sQ' magic, _, kb_size = struct.unpack_from(header_format, blob) if magic != 'CHROMEOS': return -1 # This could be a corrupted version case. _, version = struct.unpack_from(preamble_format, blob, kb_size) return version def retrieve_datakey_version(self, blob): """Given a blob, retrieve firmware data key version. Currently works for both, firmware and kernel blobs. Returns '-1' in case the version can not be retrieved reliably. """ header_format = '<8s96sQ' magic, _, version = struct.unpack_from(header_format, blob) if magic != 'CHROMEOS': return -1 # This could be a corrupted version case. return version def retrieve_kernel_subkey_version(self, blob): """Given a blob, retrieve kernel subkey version. It is in firmware vblock's preamble. """ header_format = '<8s8sQ' preamble_format = '<72sQ' magic, _, kb_size = struct.unpack_from(header_format, blob) if magic != 'CHROMEOS': return -1 _, version = struct.unpack_from(preamble_format, blob, kb_size) return version def retrieve_preamble_flags(self, blob): """Given a blob, retrieve preamble flags if available. It only works for firmware. If the version of preamble header is less than 2.1, no preamble flags supported, just returns 0. """ header_format = '<8s8sQ' preamble_format = '<32sII64sI' magic, _, kb_size = struct.unpack_from(header_format, blob) if magic != 'CHROMEOS': return -1 # This could be a corrupted version case. _, ver, subver, _, flags = struct.unpack_from(preamble_format, blob, kb_size) if ver > 2 or (ver == 2 and subver >= 1): return flags else: return 0 # Returns 0 if preamble flags not available. def read_partition(self, partition, size): """Read the requested partition, up to size bytes.""" tmp_file = self.state_dir_file('part.tmp') self.run_shell_command( 'dd if=%s of=%s bs=1 count=%d' % (partition, tmp_file, size)) data = self.read_file(tmp_file) self.remove_file(tmp_file) return data