2821 lines
115 KiB
Python
2821 lines
115 KiB
Python
# Copyright (c) 2012 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.
|
|
|
|
import functools
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import time
|
|
|
|
import common
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import autotemp
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib import global_config
|
|
from autotest_lib.client.common_lib import lsbrelease_utils
|
|
from autotest_lib.client.common_lib.cros import autoupdater
|
|
from autotest_lib.client.common_lib.cros import dev_server
|
|
from autotest_lib.client.common_lib.cros.graphite import autotest_es
|
|
from autotest_lib.client.common_lib.cros.graphite import autotest_stats
|
|
from autotest_lib.client.cros import constants as client_constants
|
|
from autotest_lib.client.cros import cros_ui
|
|
from autotest_lib.client.cros.audio import cras_utils
|
|
from autotest_lib.client.cros.input_playback import input_playback
|
|
from autotest_lib.client.cros.video import constants as video_test_constants
|
|
from autotest_lib.server import afe_utils
|
|
from autotest_lib.server import autoserv_parser
|
|
from autotest_lib.server import autotest
|
|
from autotest_lib.server import constants
|
|
from autotest_lib.server import crashcollect
|
|
from autotest_lib.server import utils as server_utils
|
|
from autotest_lib.server.cros import provision
|
|
from autotest_lib.server.cros.dynamic_suite import constants as ds_constants
|
|
from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers
|
|
from autotest_lib.server.cros.faft.config.config import Config as FAFTConfig
|
|
from autotest_lib.server.cros.servo import plankton
|
|
from autotest_lib.server.hosts import abstract_ssh
|
|
from autotest_lib.server.hosts import chameleon_host
|
|
from autotest_lib.server.hosts import plankton_host
|
|
from autotest_lib.server.hosts import servo_host
|
|
from autotest_lib.site_utils.rpm_control_system import rpm_client
|
|
|
|
|
|
CONFIG = global_config.global_config
|
|
|
|
LUCID_SLEEP_BOARDS = ['samus', 'lulu']
|
|
|
|
# A file to indicate provision failure and require Repair job to powerwash the
|
|
# dut.
|
|
PROVISION_FAILED = '/var/tmp/provision_failed'
|
|
|
|
class FactoryImageCheckerException(error.AutoservError):
|
|
"""Exception raised when an image is a factory image."""
|
|
pass
|
|
|
|
|
|
class CrosHost(abstract_ssh.AbstractSSHHost):
|
|
"""Chromium OS specific subclass of Host."""
|
|
|
|
VERSION_PREFIX = provision.CROS_VERSION_PREFIX
|
|
|
|
_parser = autoserv_parser.autoserv_parser
|
|
_AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
|
|
|
|
# Timeout values (in seconds) associated with various Chrome OS
|
|
# state changes.
|
|
#
|
|
# In general, a good rule of thumb is that the timeout can be up
|
|
# to twice the typical measured value on the slowest platform.
|
|
# The times here have not necessarily been empirically tested to
|
|
# meet this criterion.
|
|
#
|
|
# SLEEP_TIMEOUT: Time to allow for suspend to memory.
|
|
# RESUME_TIMEOUT: Time to allow for resume after suspend, plus
|
|
# time to restart the netwowrk.
|
|
# SHUTDOWN_TIMEOUT: Time to allow for shut down.
|
|
# BOOT_TIMEOUT: Time to allow for boot from power off. Among
|
|
# other things, this must account for the 30 second dev-mode
|
|
# screen delay, time to start the network on the DUT, and the
|
|
# ssh timeout of 120 seconds.
|
|
# USB_BOOT_TIMEOUT: Time to allow for boot from a USB device,
|
|
# including the 30 second dev-mode delay and time to start the
|
|
# network.
|
|
# INSTALL_TIMEOUT: Time to allow for chromeos-install.
|
|
# POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that
|
|
# includes powerwash.
|
|
|
|
SLEEP_TIMEOUT = 2
|
|
RESUME_TIMEOUT = 10
|
|
SHUTDOWN_TIMEOUT = 10
|
|
BOOT_TIMEOUT = 150
|
|
USB_BOOT_TIMEOUT = 300
|
|
INSTALL_TIMEOUT = 480
|
|
POWERWASH_BOOT_TIMEOUT = 60
|
|
|
|
# Minimum OS version that supports server side packaging. Older builds may
|
|
# not have server side package built or with Autotest code change to support
|
|
# server-side packaging.
|
|
MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value(
|
|
'AUTOSERV', 'min_version_support_ssp', type=int)
|
|
|
|
# REBOOT_TIMEOUT: How long to wait for a reboot.
|
|
#
|
|
# We have a long timeout to ensure we don't flakily fail due to other
|
|
# issues. Shorter timeouts are vetted in platform_RebootAfterUpdate.
|
|
# TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not
|
|
# return from reboot' bug is solved.
|
|
REBOOT_TIMEOUT = 480
|
|
|
|
# _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF.
|
|
# _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle.
|
|
_USB_POWER_TIMEOUT = 5
|
|
_POWER_CYCLE_TIMEOUT = 10
|
|
|
|
_RPM_RECOVERY_BOARDS = CONFIG.get_config_value('CROS',
|
|
'rpm_recovery_boards', type=str).split(',')
|
|
|
|
_MAX_POWER_CYCLE_ATTEMPTS = 6
|
|
_LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine'
|
|
_RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)'
|
|
'-host(\d+)')
|
|
_LIGHTSENSOR_FILES = [ "in_illuminance0_input",
|
|
"in_illuminance_input",
|
|
"in_illuminance0_raw",
|
|
"in_illuminance_raw",
|
|
"illuminance0_input"]
|
|
_LIGHTSENSOR_SEARCH_DIR = '/sys/bus/iio/devices'
|
|
_LABEL_FUNCTIONS = []
|
|
_DETECTABLE_LABELS = []
|
|
label_decorator = functools.partial(server_utils.add_label_detector,
|
|
_LABEL_FUNCTIONS,
|
|
_DETECTABLE_LABELS)
|
|
|
|
# Constants used in ping_wait_up() and ping_wait_down().
|
|
#
|
|
# _PING_WAIT_COUNT is the approximate number of polling
|
|
# cycles to use when waiting for a host state change.
|
|
#
|
|
# _PING_STATUS_DOWN and _PING_STATUS_UP are names used
|
|
# for arguments to the internal _ping_wait_for_status()
|
|
# method.
|
|
_PING_WAIT_COUNT = 40
|
|
_PING_STATUS_DOWN = False
|
|
_PING_STATUS_UP = True
|
|
|
|
# Allowed values for the power_method argument.
|
|
|
|
# POWER_CONTROL_RPM: Passed as default arg for power_off/on/cycle() methods.
|
|
# POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods.
|
|
# POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods.
|
|
POWER_CONTROL_RPM = 'RPM'
|
|
POWER_CONTROL_SERVO = 'servoj10'
|
|
POWER_CONTROL_MANUAL = 'manual'
|
|
|
|
POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
|
|
POWER_CONTROL_SERVO,
|
|
POWER_CONTROL_MANUAL)
|
|
|
|
_RPM_OUTLET_CHANGED = 'outlet_changed'
|
|
|
|
# URL pattern to download firmware image.
|
|
_FW_IMAGE_URL_PATTERN = CONFIG.get_config_value(
|
|
'CROS', 'firmware_url_pattern', type=str)
|
|
|
|
# File that has a list of directories to be collected
|
|
_LOGS_TO_COLLECT_FILE = os.path.join(
|
|
common.client_dir, 'common_lib', 'logs_to_collect')
|
|
|
|
# Prefix of logging message w.r.t. crash collection
|
|
_CRASHLOGS_PREFIX = 'collect_crashlogs'
|
|
|
|
# Time duration waiting for host up/down check
|
|
_CHECK_HOST_UP_TIMEOUT_SECS = 15
|
|
|
|
# A command that interacts with kernel and hardware (e.g., rm, mkdir, etc)
|
|
# might not be completely done deep through the hardware when the machine
|
|
# is powered down right after the command returns.
|
|
# We should wait for a few seconds to make them done. Finger crossed.
|
|
_SAFE_WAIT_SECS = 10
|
|
|
|
|
|
@staticmethod
|
|
def check_host(host, timeout=10):
|
|
"""
|
|
Check if the given host is a chrome-os host.
|
|
|
|
@param host: An ssh host representing a device.
|
|
@param timeout: The timeout for the run command.
|
|
|
|
@return: True if the host device is chromeos.
|
|
|
|
"""
|
|
try:
|
|
result = host.run(
|
|
'grep -q CHROMEOS /etc/lsb-release && '
|
|
'! test -f /mnt/stateful_partition/.android_tester && '
|
|
'! grep -q moblab /etc/lsb-release',
|
|
ignore_status=True, timeout=timeout)
|
|
except (error.AutoservRunError, error.AutoservSSHTimeout):
|
|
return False
|
|
return result.exit_status == 0
|
|
|
|
|
|
@staticmethod
|
|
def _extract_arguments(args_dict, key_subset):
|
|
"""Extract options from `args_dict` and return a subset result.
|
|
|
|
Take the provided dictionary of argument options and return
|
|
a subset that represent standard arguments needed to construct
|
|
a test-assistant object (chameleon or servo) for a host. The
|
|
intent is to provide standard argument processing from
|
|
CrosHost for tests that require a test-assistant board
|
|
to operate.
|
|
|
|
@param args_dict Dictionary from which to extract the arguments.
|
|
@param key_subset Tuple of keys to extract from the args_dict, e.g.
|
|
('servo_host', 'servo_port').
|
|
"""
|
|
result = {}
|
|
for arg in key_subset:
|
|
if arg in args_dict:
|
|
result[arg] = args_dict[arg]
|
|
return result
|
|
|
|
|
|
@staticmethod
|
|
def get_chameleon_arguments(args_dict):
|
|
"""Extract chameleon options from `args_dict` and return the result.
|
|
|
|
Recommended usage:
|
|
~~~~~~~~
|
|
args_dict = utils.args_to_dict(args)
|
|
chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
|
|
host = hosts.create_host(machine, chameleon_args=chameleon_args)
|
|
~~~~~~~~
|
|
|
|
@param args_dict Dictionary from which to extract the chameleon
|
|
arguments.
|
|
"""
|
|
return CrosHost._extract_arguments(
|
|
args_dict, ('chameleon_host', 'chameleon_port'))
|
|
|
|
|
|
@staticmethod
|
|
def get_plankton_arguments(args_dict):
|
|
"""Extract chameleon options from `args_dict` and return the result.
|
|
|
|
Recommended usage:
|
|
~~~~~~~~
|
|
args_dict = utils.args_to_dict(args)
|
|
plankon_args = hosts.CrosHost.get_plankton_arguments(args_dict)
|
|
host = hosts.create_host(machine, plankton_args=polankton_args)
|
|
~~~~~~~~
|
|
|
|
@param args_dict Dictionary from which to extract the plankton
|
|
arguments.
|
|
"""
|
|
args = CrosHost._extract_arguments(
|
|
args_dict, ('plankton_host', 'plankton_port'))
|
|
return args
|
|
|
|
|
|
@staticmethod
|
|
def get_servo_arguments(args_dict):
|
|
"""Extract servo options from `args_dict` and return the result.
|
|
|
|
Recommended usage:
|
|
~~~~~~~~
|
|
args_dict = utils.args_to_dict(args)
|
|
servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
|
|
host = hosts.create_host(machine, servo_args=servo_args)
|
|
~~~~~~~~
|
|
|
|
@param args_dict Dictionary from which to extract the servo
|
|
arguments.
|
|
"""
|
|
return CrosHost._extract_arguments(
|
|
args_dict, ('servo_host', 'servo_port'))
|
|
|
|
|
|
def _initialize(self, hostname, chameleon_args=None, servo_args=None, plankton_args=None,
|
|
try_lab_servo=False, ssh_verbosity_flag='', ssh_options='',
|
|
*args, **dargs):
|
|
"""Initialize superclasses, |self.chameleon|, and |self.servo|.
|
|
|
|
This method will attempt to create the test-assistant object
|
|
(chameleon/servo) when it is needed by the test. Check
|
|
the docstring of chameleon_host.create_chameleon_host and
|
|
servo_host.create_servo_host for how this is determined.
|
|
|
|
@param hostname: Hostname of the dut.
|
|
@param chameleon_args: A dictionary that contains args for creating
|
|
a ChameleonHost. See chameleon_host for details.
|
|
@param servo_args: A dictionary that contains args for creating
|
|
a ServoHost object. See servo_host for details.
|
|
@param try_lab_servo: Boolean, False indicates that ServoHost should
|
|
not be created for a device in Cros test lab.
|
|
See servo_host for details.
|
|
@param ssh_verbosity_flag: String, to pass to the ssh command to control
|
|
verbosity.
|
|
@param ssh_options: String, other ssh options to pass to the ssh
|
|
command.
|
|
"""
|
|
super(CrosHost, self)._initialize(hostname=hostname,
|
|
*args, **dargs)
|
|
# self.env is a dictionary of environment variable settings
|
|
# to be exported for commands run on the host.
|
|
# LIBC_FATAL_STDERR_ can be useful for diagnosing certain
|
|
# errors that might happen.
|
|
self.env['LIBC_FATAL_STDERR_'] = '1'
|
|
self._ssh_verbosity_flag = ssh_verbosity_flag
|
|
self._ssh_options = ssh_options
|
|
# TODO(fdeng): We need to simplify the
|
|
# process of servo and servo_host initialization.
|
|
# crbug.com/298432
|
|
self._servo_host = servo_host.create_servo_host(
|
|
dut=self.hostname, servo_args=servo_args,
|
|
try_lab_servo=try_lab_servo)
|
|
# TODO(waihong): Do the simplication on Chameleon too.
|
|
self._chameleon_host = chameleon_host.create_chameleon_host(
|
|
dut=self.hostname, chameleon_args=chameleon_args)
|
|
# Add plankton host if plankton args were added on command line
|
|
self._plankton_host = plankton_host.create_plankton_host(plankton_args)
|
|
|
|
if self._servo_host is not None:
|
|
self.servo = self._servo_host.get_servo()
|
|
else:
|
|
self.servo = None
|
|
|
|
if self._chameleon_host:
|
|
self.chameleon = self._chameleon_host.create_chameleon_board()
|
|
else:
|
|
self.chameleon = None
|
|
|
|
if self._plankton_host:
|
|
self.plankton_servo = self._plankton_host.get_servo()
|
|
logging.info('plankton_servo: %r', self.plankton_servo)
|
|
# Create the plankton object used to access the ec uart
|
|
self.plankton = plankton.Plankton(self.plankton_servo,
|
|
self._plankton_host.get_servod_server_proxy())
|
|
else:
|
|
self.plankton = None
|
|
|
|
|
|
def get_repair_image_name(self, image_type='cros'):
|
|
"""Generate a image_name from variables in the global config.
|
|
|
|
image_type is used to differentiate different images. Default is CrOS,
|
|
in which case, repair image's name follows the naming convention defined
|
|
in global setting CROS/stable_build_pattern.
|
|
If the image_type is not `cros`, the repair image will be looked up
|
|
using key `board_name/image_type`, e.g., daisy_spring/firmware.
|
|
|
|
@param image_type: Type of the image. Default is `cros`.
|
|
|
|
@returns a str of $board-version/$BUILD. Returns None if stable version
|
|
for the board and the default are both not set, e.g., stable
|
|
firmware version for a new board.
|
|
|
|
"""
|
|
board = self._get_board_from_afe()
|
|
if board is None:
|
|
raise error.AutoservError('DUT has no board attribute, '
|
|
'cannot be repaired.')
|
|
if image_type != 'cros':
|
|
board = '%s/%s' % (board, image_type)
|
|
stable_version = afe_utils.get_stable_version(board=board)
|
|
if image_type == 'cros':
|
|
build_pattern = CONFIG.get_config_value(
|
|
'CROS', 'stable_build_pattern')
|
|
stable_version = build_pattern % (board, stable_version)
|
|
elif image_type == 'firmware':
|
|
# If firmware stable version is not specified, `stable_version`
|
|
# from the RPC is the default stable version for CrOS image.
|
|
# firmware stable version must be from firmware branch, thus its
|
|
# value must be like board-firmware/R31-1234.0.0. Check if
|
|
# firmware exists in the stable version, if not, return None.
|
|
if 'firmware' not in stable_version:
|
|
return None
|
|
return stable_version
|
|
|
|
|
|
def lookup_job_repo_url(self):
|
|
"""Looks up the job_repo_url for the host.
|
|
|
|
@returns job_repo_url from AFE or None if not found.
|
|
|
|
@raises KeyError if the host does not have a job_repo_url
|
|
"""
|
|
hosts = self._AFE.get_hosts(hostname=self.hostname)
|
|
if hosts and ds_constants.JOB_REPO_URL in hosts[0].attributes:
|
|
return hosts[0].attributes[ds_constants.JOB_REPO_URL]
|
|
else:
|
|
return None
|
|
|
|
|
|
def clear_job_repo_url(self):
|
|
"""Clear host attribute job_repo_url."""
|
|
if not afe_utils.host_in_lab(self):
|
|
return
|
|
self.update_job_repo_url(None, None)
|
|
|
|
|
|
def update_job_repo_url(self, devserver_url, image_name):
|
|
"""
|
|
Updates the job_repo_url host attribute and asserts it's value.
|
|
|
|
@param devserver_url: The devserver to use in the job_repo_url.
|
|
@param image_name: The name of the image to use in the job_repo_url.
|
|
|
|
@raises AutoservError: If we failed to update the job_repo_url.
|
|
"""
|
|
repo_url = None
|
|
if devserver_url and image_name:
|
|
repo_url = tools.get_package_url(devserver_url, image_name)
|
|
self._AFE.set_host_attribute(ds_constants.JOB_REPO_URL, repo_url,
|
|
hostname=self.hostname)
|
|
if self.lookup_job_repo_url() != repo_url:
|
|
raise error.AutoservError('Failed to update job_repo_url with %s, '
|
|
'host %s' % (repo_url, self.hostname))
|
|
|
|
|
|
def add_job_repo_url(self, image_name):
|
|
"""Add cros_version labels and host attribute job_repo_url.
|
|
|
|
@param image_name: The name of the image e.g.
|
|
lumpy-release/R27-3837.0.0
|
|
|
|
"""
|
|
if not afe_utils.host_in_lab(self):
|
|
return
|
|
|
|
devserver_url = dev_server.ImageServer.resolve(image_name,
|
|
self.hostname).url()
|
|
self.update_job_repo_url(devserver_url, image_name)
|
|
|
|
|
|
def verify_job_repo_url(self, tag=''):
|
|
"""
|
|
Make sure job_repo_url of this host is valid.
|
|
|
|
Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\
|
|
lumpy-release/R29-4279.0.0/autotest/packages" claims to have the
|
|
autotest package for lumpy-release/R29-4279.0.0. If this isn't the case,
|
|
download and extract it. If the devserver embedded in the url is
|
|
unresponsive, update the job_repo_url of the host after staging it on
|
|
another devserver.
|
|
|
|
@param job_repo_url: A url pointing to the devserver where the autotest
|
|
package for this build should be staged.
|
|
@param tag: The tag from the server job, in the format
|
|
<job_id>-<user>/<hostname>, or <hostless> for a server job.
|
|
|
|
@raises DevServerException: If we could not resolve a devserver.
|
|
@raises AutoservError: If we're unable to save the new job_repo_url as
|
|
a result of choosing a new devserver because the old one failed to
|
|
respond to a health check.
|
|
@raises urllib2.URLError: If the devserver embedded in job_repo_url
|
|
doesn't respond within the timeout.
|
|
"""
|
|
job_repo_url = self.lookup_job_repo_url()
|
|
if not job_repo_url:
|
|
logging.warning('No job repo url set on host %s', self.hostname)
|
|
return
|
|
|
|
logging.info('Verifying job repo url %s', job_repo_url)
|
|
devserver_url, image_name = tools.get_devserver_build_from_package_url(
|
|
job_repo_url)
|
|
|
|
ds = dev_server.ImageServer(devserver_url)
|
|
|
|
logging.info('Staging autotest artifacts for %s on devserver %s',
|
|
image_name, ds.url())
|
|
|
|
start_time = time.time()
|
|
ds.stage_artifacts(image_name, ['autotest_packages'])
|
|
stage_time = time.time() - start_time
|
|
|
|
# Record how much of the verification time comes from a devserver
|
|
# restage. If we're doing things right we should not see multiple
|
|
# devservers for a given board/build/branch path.
|
|
try:
|
|
board, build_type, branch = server_utils.ParseBuildName(
|
|
image_name)[:3]
|
|
except server_utils.ParseBuildNameException:
|
|
pass
|
|
else:
|
|
devserver = devserver_url[
|
|
devserver_url.find('/') + 2:devserver_url.rfind(':')]
|
|
stats_key = {
|
|
'board': board,
|
|
'build_type': build_type,
|
|
'branch': branch,
|
|
'devserver': devserver.replace('.', '_'),
|
|
}
|
|
autotest_stats.Gauge('verify_job_repo_url').send(
|
|
'%(board)s.%(build_type)s.%(branch)s.%(devserver)s' % stats_key,
|
|
stage_time)
|
|
|
|
|
|
def stage_server_side_package(self, image=None):
|
|
"""Stage autotest server-side package on devserver.
|
|
|
|
@param image: Full path of an OS image to install or a build name.
|
|
|
|
@return: A url to the autotest server-side package.
|
|
"""
|
|
if image:
|
|
image_name = tools.get_build_from_image(image)
|
|
if not image_name:
|
|
raise error.AutoservError(
|
|
'Failed to parse build name from %s' % image)
|
|
ds = dev_server.ImageServer.resolve(image_name)
|
|
else:
|
|
job_repo_url = self.lookup_job_repo_url()
|
|
if job_repo_url:
|
|
devserver_url, image_name = (
|
|
tools.get_devserver_build_from_package_url(job_repo_url))
|
|
ds = dev_server.ImageServer(devserver_url)
|
|
else:
|
|
labels = self._AFE.get_labels(
|
|
name__startswith=ds_constants.VERSION_PREFIX,
|
|
host__hostname=self.hostname)
|
|
if not labels:
|
|
raise error.AutoservError(
|
|
'Failed to stage server-side package. The host has '
|
|
'no job_report_url attribute or version label.')
|
|
image_name = labels[0].name[len(ds_constants.VERSION_PREFIX):]
|
|
ds = dev_server.ImageServer.resolve(image_name)
|
|
|
|
# Get the OS version of the build, for any build older than
|
|
# MIN_VERSION_SUPPORT_SSP, server side packaging is not supported.
|
|
match = re.match('.*/R\d+-(\d+)\.', image_name)
|
|
if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP:
|
|
logging.warn('Build %s is older than %s. Server side packaging is '
|
|
'disabled.', image_name, self.MIN_VERSION_SUPPORT_SSP)
|
|
return None
|
|
|
|
ds.stage_artifacts(image_name, ['autotest_server_package'])
|
|
return '%s/static/%s/%s' % (ds.url(), image_name,
|
|
'autotest_server_package.tar.bz2')
|
|
|
|
|
|
def _try_stateful_update(self, update_url, force_update, updater):
|
|
"""Try to use stateful update to initialize DUT.
|
|
|
|
When DUT is already running the same version that machine_install
|
|
tries to install, stateful update is a much faster way to clean up
|
|
the DUT for testing, compared to a full reimage. It is implemeted
|
|
by calling autoupdater.run_update, but skipping updating root, as
|
|
updating the kernel is time consuming and not necessary.
|
|
|
|
@param update_url: url of the image.
|
|
@param force_update: Set to True to update the image even if the DUT
|
|
is running the same version.
|
|
@param updater: ChromiumOSUpdater instance used to update the DUT.
|
|
@returns: True if the DUT was updated with stateful update.
|
|
|
|
"""
|
|
# Stop service ap-update-manager to prevent rebooting during autoupdate.
|
|
# The service is used in jetstream boards, but not other CrOS devices.
|
|
self.run('sudo stop ap-update-manager', ignore_status=True)
|
|
|
|
# TODO(jrbarnette): Yes, I hate this re.match() test case.
|
|
# It's better than the alternative: see crbug.com/360944.
|
|
image_name = autoupdater.url_to_image_name(update_url)
|
|
release_pattern = r'^.*-release/R[0-9]+-[0-9]+\.[0-9]+\.0$'
|
|
if not re.match(release_pattern, image_name):
|
|
return False
|
|
if not updater.check_version():
|
|
return False
|
|
if not force_update:
|
|
logging.info('Canceling stateful update because the new and '
|
|
'old versions are the same.')
|
|
return False
|
|
# Following folders should be rebuilt after stateful update.
|
|
# A test file is used to confirm each folder gets rebuilt after
|
|
# the stateful update.
|
|
folders_to_check = ['/var', '/home', '/mnt/stateful_partition']
|
|
test_file = '.test_file_to_be_deleted'
|
|
for folder in folders_to_check:
|
|
touch_path = os.path.join(folder, test_file)
|
|
self.run('touch %s' % touch_path)
|
|
|
|
updater.run_update(update_root=False)
|
|
|
|
# Reboot to complete stateful update.
|
|
self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
|
|
check_file_cmd = 'test -f %s; echo $?'
|
|
for folder in folders_to_check:
|
|
test_file_path = os.path.join(folder, test_file)
|
|
result = self.run(check_file_cmd % test_file_path,
|
|
ignore_status=True)
|
|
if result.exit_status == 1:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _post_update_processing(self, updater, expected_kernel=None):
|
|
"""After the DUT is updated, confirm machine_install succeeded.
|
|
|
|
@param updater: ChromiumOSUpdater instance used to update the DUT.
|
|
@param expected_kernel: kernel expected to be active after reboot,
|
|
or `None` to skip rollback checking.
|
|
|
|
"""
|
|
# Touch the lab machine file to leave a marker that
|
|
# distinguishes this image from other test images.
|
|
# Afterwards, we must re-run the autoreboot script because
|
|
# it depends on the _LAB_MACHINE_FILE.
|
|
autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
|
|
'( touch "$FILE" ; start autoreboot )')
|
|
self.run(autoreboot_cmd % self._LAB_MACHINE_FILE)
|
|
updater.verify_boot_expectations(
|
|
expected_kernel, rollback_message=
|
|
'Build %s failed to boot on %s; system rolled back to previous '
|
|
'build' % (updater.update_version, self.hostname))
|
|
# Check that we've got the build we meant to install.
|
|
if not updater.check_version_to_confirm_install():
|
|
raise autoupdater.ChromiumOSError(
|
|
'Failed to update %s to build %s; found build '
|
|
'%s instead' % (self.hostname,
|
|
updater.update_version,
|
|
self.get_release_version()))
|
|
|
|
logging.debug('Cleaning up old autotest directories.')
|
|
try:
|
|
installed_autodir = autotest.Autotest.get_installed_autodir(self)
|
|
self.run('rm -rf ' + installed_autodir)
|
|
except autotest.AutodirNotFoundError:
|
|
logging.debug('No autotest installed directory found.')
|
|
|
|
|
|
def _stage_image_for_update(self, image_name=None):
|
|
"""Stage a build on a devserver and return the update_url and devserver.
|
|
|
|
@param image_name: a name like lumpy-release/R27-3837.0.0
|
|
@returns a tuple with an update URL like:
|
|
http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
|
|
and the devserver instance.
|
|
"""
|
|
if not image_name:
|
|
image_name = self.get_repair_image_name()
|
|
|
|
logging.info('Staging build for AU: %s', image_name)
|
|
devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
|
|
devserver.trigger_download(image_name, synchronous=False)
|
|
return (tools.image_url_pattern() % (devserver.url(), image_name),
|
|
devserver)
|
|
|
|
|
|
def stage_image_for_servo(self, image_name=None):
|
|
"""Stage a build on a devserver and return the update_url.
|
|
|
|
@param image_name: a name like lumpy-release/R27-3837.0.0
|
|
@returns an update URL like:
|
|
http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
|
|
"""
|
|
if not image_name:
|
|
image_name = self.get_repair_image_name()
|
|
logging.info('Staging build for servo install: %s', image_name)
|
|
devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
|
|
devserver.stage_artifacts(image_name, ['test_image'])
|
|
return devserver.get_test_image_url(image_name)
|
|
|
|
|
|
def stage_factory_image_for_servo(self, image_name):
|
|
"""Stage a build on a devserver and return the update_url.
|
|
|
|
@param image_name: a name like <baord>/4262.204.0
|
|
|
|
@return: An update URL, eg:
|
|
http://<devserver>/static/canary-channel/\
|
|
<board>/4262.204.0/factory_test/chromiumos_factory_image.bin
|
|
|
|
@raises: ValueError if the factory artifact name is missing from
|
|
the config.
|
|
|
|
"""
|
|
if not image_name:
|
|
logging.error('Need an image_name to stage a factory image.')
|
|
return
|
|
|
|
factory_artifact = CONFIG.get_config_value(
|
|
'CROS', 'factory_artifact', type=str, default='')
|
|
if not factory_artifact:
|
|
raise ValueError('Cannot retrieve the factory artifact name from '
|
|
'autotest config, and hence cannot stage factory '
|
|
'artifacts.')
|
|
|
|
logging.info('Staging build for servo install: %s', image_name)
|
|
devserver = dev_server.ImageServer.resolve(image_name, self.hostname)
|
|
devserver.stage_artifacts(
|
|
image_name,
|
|
[factory_artifact],
|
|
archive_url=None)
|
|
|
|
return tools.factory_image_url_pattern() % (devserver.url(), image_name)
|
|
|
|
|
|
def machine_install(self, update_url=None, force_update=False,
|
|
local_devserver=False, repair=False,
|
|
force_full_update=False):
|
|
"""Install the DUT.
|
|
|
|
Use stateful update if the DUT is already running the same build.
|
|
Stateful update does not update kernel and tends to run much faster
|
|
than a full reimage. If the DUT is running a different build, or it
|
|
failed to do a stateful update, full update, including kernel update,
|
|
will be applied to the DUT.
|
|
|
|
Once a host enters machine_install its host attribute job_repo_url
|
|
(used for package install) will be removed and then updated.
|
|
|
|
@param update_url: The url to use for the update
|
|
pattern: http://$devserver:###/update/$build
|
|
If update_url is None and repair is True we will install the
|
|
stable image listed in afe_stable_versions table. If the table
|
|
is not setup, global_config value under CROS.stable_cros_version
|
|
will be used instead.
|
|
@param force_update: Force an update even if the version installed
|
|
is the same. Default:False
|
|
@param local_devserver: Used by test_that to allow people to
|
|
use their local devserver. Default: False
|
|
@param repair: Forces update to repair image. Implies force_update.
|
|
@param force_full_update: If True, do not attempt to run stateful
|
|
update, force a full reimage. If False, try stateful update
|
|
first when the dut is already installed with the same version.
|
|
@raises autoupdater.ChromiumOSError
|
|
|
|
@returns Name of the image installed.
|
|
"""
|
|
devserver = None
|
|
if repair:
|
|
update_url, devserver = self._stage_image_for_update()
|
|
force_update = True
|
|
|
|
if not update_url and not self._parser.options.image:
|
|
raise error.AutoservError(
|
|
'There is no update URL, nor a method to get one.')
|
|
|
|
if not update_url and self._parser.options.image:
|
|
# This is the base case where we have no given update URL i.e.
|
|
# dynamic suites logic etc. This is the most flexible case where we
|
|
# can serve an update from any of our fleet of devservers.
|
|
requested_build = self._parser.options.image
|
|
if not requested_build.startswith('http://'):
|
|
logging.debug('Update will be staged for this installation')
|
|
update_url, devserver = self._stage_image_for_update(
|
|
requested_build)
|
|
else:
|
|
update_url = requested_build
|
|
|
|
logging.debug('Update URL is %s', update_url)
|
|
|
|
# Remove cros-version and job_repo_url host attribute from host.
|
|
self.clear_job_repo_url()
|
|
|
|
# Create a file to indicate if provision fails. The file will be removed
|
|
# by stateful update or full install.
|
|
self.run('touch %s' % PROVISION_FAILED)
|
|
|
|
update_complete = False
|
|
updater = autoupdater.ChromiumOSUpdater(
|
|
update_url, host=self, local_devserver=local_devserver)
|
|
if not force_full_update:
|
|
try:
|
|
# If the DUT is already running the same build, try stateful
|
|
# update first as it's much quicker than a full re-image.
|
|
update_complete = self._try_stateful_update(
|
|
update_url, force_update, updater)
|
|
except Exception as e:
|
|
logging.exception(e)
|
|
|
|
inactive_kernel = None
|
|
if update_complete or (not force_update and updater.check_version()):
|
|
logging.info('Install complete without full update')
|
|
else:
|
|
logging.info('DUT requires full update.')
|
|
self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
|
|
# Stop service ap-update-manager to prevent rebooting during
|
|
# autoupdate. The service is used in jetstream boards, but not other
|
|
# CrOS devices.
|
|
self.run('sudo stop ap-update-manager', ignore_status=True)
|
|
|
|
num_of_attempts = provision.FLAKY_DEVSERVER_ATTEMPTS
|
|
|
|
while num_of_attempts > 0:
|
|
num_of_attempts -= 1
|
|
try:
|
|
updater.run_update()
|
|
except Exception:
|
|
logging.warn('Autoupdate did not complete.')
|
|
# Do additional check for the devserver health. Ideally,
|
|
# the autoupdater.py could raise an exception when it
|
|
# detected network flake but that would require
|
|
# instrumenting the update engine and parsing it log.
|
|
if (num_of_attempts <= 0 or
|
|
devserver is None or
|
|
dev_server.DevServer.devserver_healthy(
|
|
devserver.url())):
|
|
raise
|
|
|
|
logging.warn('Devserver looks unhealthy. Trying another')
|
|
update_url, devserver = self._stage_image_for_update(
|
|
requested_build)
|
|
logging.debug('New Update URL is %s', update_url)
|
|
updater = autoupdater.ChromiumOSUpdater(
|
|
update_url, host=self,
|
|
local_devserver=local_devserver)
|
|
else:
|
|
break
|
|
|
|
# Give it some time in case of IO issues.
|
|
time.sleep(10)
|
|
|
|
# Figure out active and inactive kernel.
|
|
active_kernel, inactive_kernel = updater.get_kernel_state()
|
|
|
|
# Ensure inactive kernel has higher priority than active.
|
|
if (updater.get_kernel_priority(inactive_kernel)
|
|
< updater.get_kernel_priority(active_kernel)):
|
|
raise autoupdater.ChromiumOSError(
|
|
'Update failed. The priority of the inactive kernel'
|
|
' partition is less than that of the active kernel'
|
|
' partition.')
|
|
|
|
# Updater has returned successfully; reboot the host.
|
|
self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True)
|
|
|
|
self._post_update_processing(updater, inactive_kernel)
|
|
image_name = autoupdater.url_to_image_name(update_url)
|
|
self.add_job_repo_url(image_name)
|
|
return image_name
|
|
|
|
|
|
def _clear_fw_version_labels(self, rw_only):
|
|
"""Clear firmware version labels from the machine.
|
|
|
|
@param rw_only: True to only clear fwrw_version; otherewise, clear
|
|
both fwro_version and fwrw_version.
|
|
"""
|
|
labels = self._AFE.get_labels(
|
|
name__startswith=provision.FW_RW_VERSION_PREFIX,
|
|
host__hostname=self.hostname)
|
|
if not rw_only:
|
|
labels = labels + self._AFE.get_labels(
|
|
name__startswith=provision.FW_RO_VERSION_PREFIX,
|
|
host__hostname=self.hostname)
|
|
for label in labels:
|
|
label.remove_hosts(hosts=[self.hostname])
|
|
|
|
|
|
def _add_fw_version_label(self, build, rw_only):
|
|
"""Add firmware version label to the machine.
|
|
|
|
@param build: Build of firmware.
|
|
@param rw_only: True to only add fwrw_version; otherwise, add both
|
|
fwro_version and fwrw_version.
|
|
|
|
"""
|
|
fw_label = provision.fwrw_version_to_label(build)
|
|
self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
|
|
if not rw_only:
|
|
fw_label = provision.fwro_version_to_label(build)
|
|
self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
|
|
|
|
|
|
def firmware_install(self, build=None, rw_only=False):
|
|
"""Install firmware to the DUT.
|
|
|
|
Use stateful update if the DUT is already running the same build.
|
|
Stateful update does not update kernel and tends to run much faster
|
|
than a full reimage. If the DUT is running a different build, or it
|
|
failed to do a stateful update, full update, including kernel update,
|
|
will be applied to the DUT.
|
|
|
|
Once a host enters firmware_install its fw[ro|rw]_version label will
|
|
be removed. After the firmware is updated successfully, a new
|
|
fw[ro|rw]_version label will be added to the host.
|
|
|
|
@param build: The build version to which we want to provision the
|
|
firmware of the machine,
|
|
e.g. 'link-firmware/R22-2695.1.144'.
|
|
@param rw_only: True to only install firmware to its RW portions. Keep
|
|
the RO portions unchanged.
|
|
|
|
TODO(dshi): After bug 381718 is fixed, update here with corresponding
|
|
exceptions that could be raised.
|
|
|
|
"""
|
|
if not self.servo:
|
|
raise error.TestError('Host %s does not have servo.' %
|
|
self.hostname)
|
|
|
|
# TODO(fdeng): use host.get_board() after
|
|
# crbug.com/271834 is fixed.
|
|
board = self._get_board_from_afe()
|
|
|
|
# If build is not set, try to install firmware from stable CrOS.
|
|
if not build:
|
|
build = self.get_repair_image_name(image_type='firmware')
|
|
if not build:
|
|
raise error.TestError(
|
|
'Failed to find stable firmware build for %s.',
|
|
self.hostname)
|
|
logging.info('Will install firmware from build %s.', build)
|
|
|
|
config = FAFTConfig(board)
|
|
if config.use_u_boot:
|
|
ap_image = 'image-%s.bin' % board
|
|
else: # Depthcharge platform
|
|
ap_image = 'image.bin'
|
|
ec_image = 'ec.bin'
|
|
ds = dev_server.ImageServer.resolve(build, self.hostname)
|
|
ds.stage_artifacts(build, ['firmware'])
|
|
|
|
tmpd = autotemp.tempdir(unique_id='fwimage')
|
|
try:
|
|
fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
|
|
local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl))
|
|
server_utils.system('wget -O %s %s' % (local_tarball, fwurl),
|
|
timeout=60)
|
|
server_utils.system('tar xf %s -C %s %s %s' %
|
|
(local_tarball, tmpd.name, ap_image, ec_image),
|
|
timeout=60)
|
|
server_utils.system('tar xf %s --wildcards -C %s "dts/*"' %
|
|
(local_tarball, tmpd.name),
|
|
timeout=60, ignore_status=True)
|
|
|
|
self._clear_fw_version_labels(rw_only)
|
|
logging.info('Will re-program EC %snow', 'RW ' if rw_only else '')
|
|
self.servo.program_ec(os.path.join(tmpd.name, ec_image), rw_only)
|
|
logging.info('Will re-program BIOS %snow', 'RW ' if rw_only else '')
|
|
self.servo.program_bios(os.path.join(tmpd.name, ap_image), rw_only)
|
|
self.servo.get_power_state_controller().reset()
|
|
time.sleep(self.servo.BOOT_DELAY)
|
|
self._add_fw_version_label(build, rw_only)
|
|
finally:
|
|
tmpd.clean()
|
|
|
|
|
|
def show_update_engine_log(self):
|
|
"""Output update engine log."""
|
|
logging.debug('Dumping %s', client_constants.UPDATE_ENGINE_LOG)
|
|
self.run('cat %s' % client_constants.UPDATE_ENGINE_LOG)
|
|
|
|
|
|
def _get_board_from_afe(self):
|
|
"""Retrieve this host's board from its labels in the AFE.
|
|
|
|
Looks for a host label of the form "board:<board>", and
|
|
returns the "<board>" part of the label. `None` is returned
|
|
if there is not a single, unique label matching the pattern.
|
|
|
|
@returns board from label, or `None`.
|
|
"""
|
|
return server_utils.get_board_from_afe(self.hostname, self._AFE)
|
|
|
|
|
|
def get_build(self):
|
|
"""Retrieve the current build for this Host from the AFE.
|
|
|
|
Looks through this host's labels in the AFE to determine its build.
|
|
This method is replaced by afe_utils.get_build. It's kept here to
|
|
maintain backwards compatibility for test control files in older CrOS
|
|
builds (R48, R49 etc.) still call host.get_build, e.g.,
|
|
`provision_AutoUpdate.double`.
|
|
TODO(sbasi): Once R50 falls into release branch, this method can be
|
|
removed.
|
|
|
|
@returns The current build or None if it could not find it or if there
|
|
were multiple build labels assigned to this host.
|
|
"""
|
|
return afe_utils.get_build(self)
|
|
|
|
|
|
def _install_repair(self):
|
|
"""Attempt to repair this host using the update-engine.
|
|
|
|
If the host is up, try installing the DUT with a stable
|
|
"repair" version of Chrome OS as defined in afe_stable_versions table.
|
|
If the table is not setup, global_config value under
|
|
CROS.stable_cros_version will be used instead.
|
|
|
|
@raises AutoservRepairMethodNA if the DUT is not reachable.
|
|
@raises ChromiumOSError if the install failed for some reason.
|
|
|
|
"""
|
|
if not self.is_up():
|
|
raise error.AutoservRepairMethodNA('DUT unreachable for install.')
|
|
logging.info('Attempting to reimage machine to repair image.')
|
|
try:
|
|
afe_utils.machine_install_and_update_labels(self, repair=True)
|
|
except autoupdater.ChromiumOSError as e:
|
|
logging.exception(e)
|
|
logging.info('Repair via install failed.')
|
|
raise
|
|
|
|
|
|
def _install_repair_with_powerwash(self):
|
|
"""Attempt to powerwash first then repair this host using update-engine.
|
|
|
|
update-engine may fail due to a bad image. In such case, powerwash
|
|
may help to cleanup the DUT for update-engine to work again.
|
|
|
|
@raises AutoservRepairMethodNA if the DUT is not reachable.
|
|
@raises ChromiumOSError if the install failed for some reason.
|
|
|
|
"""
|
|
if not self.is_up():
|
|
raise error.AutoservRepairMethodNA('DUT unreachable for install.')
|
|
|
|
logging.info('Attempting to powerwash the DUT.')
|
|
self.run('echo "fast safe" > '
|
|
'/mnt/stateful_partition/factory_install_reset')
|
|
self.reboot(timeout=self.POWERWASH_BOOT_TIMEOUT, wait=True)
|
|
if not self.is_up():
|
|
logging.error('Powerwash failed. DUT did not come back after '
|
|
'reboot.')
|
|
raise error.AutoservRepairFailure(
|
|
'DUT failed to boot from powerwash after %d seconds' %
|
|
self.POWERWASH_BOOT_TIMEOUT)
|
|
|
|
logging.info('Powerwash succeeded.')
|
|
self._install_repair()
|
|
|
|
|
|
def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT,
|
|
install_timeout=INSTALL_TIMEOUT):
|
|
"""
|
|
Re-install the OS on the DUT by:
|
|
1) installing a test image on a USB storage device attached to the Servo
|
|
board,
|
|
2) booting that image in recovery mode, and then
|
|
3) installing the image with chromeos-install.
|
|
|
|
@param image_url: If specified use as the url to install on the DUT.
|
|
otherwise boot the currently staged image on the USB stick.
|
|
@param usb_boot_timeout: The usb_boot_timeout to use during reimage.
|
|
Factory images need a longer usb_boot_timeout than regular
|
|
cros images.
|
|
@param install_timeout: The timeout to use when installing the chromeos
|
|
image. Factory images need a longer install_timeout.
|
|
|
|
@raises AutoservError if the image fails to boot.
|
|
|
|
"""
|
|
usb_boot_timer_key = ('servo_install.usb_boot_timeout_%s'
|
|
% usb_boot_timeout)
|
|
logging.info('Downloading image to USB, then booting from it. Usb boot '
|
|
'timeout = %s', usb_boot_timeout)
|
|
timer = autotest_stats.Timer(usb_boot_timer_key)
|
|
timer.start()
|
|
self.servo.install_recovery_image(image_url)
|
|
if not self.wait_up(timeout=usb_boot_timeout):
|
|
raise error.AutoservRepairFailure(
|
|
'DUT failed to boot from USB after %d seconds' %
|
|
usb_boot_timeout)
|
|
timer.stop()
|
|
|
|
# The new chromeos-tpm-recovery has been merged since R44-7073.0.0.
|
|
# In old CrOS images, this command fails. Skip the error.
|
|
logging.info('Resetting the TPM status')
|
|
try:
|
|
self.run('chromeos-tpm-recovery')
|
|
except error.AutoservRunError:
|
|
logging.warn('chromeos-tpm-recovery is too old.')
|
|
|
|
|
|
install_timer_key = ('servo_install.install_timeout_%s'
|
|
% install_timeout)
|
|
timer = autotest_stats.Timer(install_timer_key)
|
|
timer.start()
|
|
logging.info('Installing image through chromeos-install.')
|
|
self.run('chromeos-install --yes', timeout=install_timeout)
|
|
self.halt()
|
|
timer.stop()
|
|
|
|
logging.info('Power cycling DUT through servo.')
|
|
self.servo.get_power_state_controller().power_off()
|
|
self.servo.switch_usbkey('off')
|
|
# N.B. The Servo API requires that we use power_on() here
|
|
# for two reasons:
|
|
# 1) After turning on a DUT in recovery mode, you must turn
|
|
# it off and then on with power_on() once more to
|
|
# disable recovery mode (this is a Parrot specific
|
|
# requirement).
|
|
# 2) After power_off(), the only way to turn on is with
|
|
# power_on() (this is a Storm specific requirement).
|
|
self.servo.get_power_state_controller().power_on()
|
|
|
|
logging.info('Waiting for DUT to come back up.')
|
|
if not self.wait_up(timeout=self.BOOT_TIMEOUT):
|
|
raise error.AutoservError('DUT failed to reboot installed '
|
|
'test image after %d seconds' %
|
|
self.BOOT_TIMEOUT)
|
|
|
|
|
|
def _setup_servo(self):
|
|
"""Try to force to create servo object if it's not set up yet.
|
|
"""
|
|
if self.servo:
|
|
return
|
|
|
|
try:
|
|
# Setting servo_args to {} will force it to create the servo_host
|
|
# object if possible.
|
|
self._servo_host = servo_host.create_servo_host(
|
|
dut=self.hostname, servo_args={})
|
|
if self._servo_host:
|
|
self.servo = self._servo_host.get_servo()
|
|
else:
|
|
logging.error('Failed to create servo_host object.')
|
|
except Exception as e:
|
|
logging.error('Failed to create servo object: %s', e)
|
|
|
|
|
|
def _servo_repair_reinstall(self):
|
|
"""Reinstall the DUT utilizing servo and a test image.
|
|
|
|
Re-install the OS on the DUT by:
|
|
1) installing a test image on a USB storage device attached to the Servo
|
|
board,
|
|
2) booting that image in recovery mode,
|
|
3) resetting the TPM status, and then
|
|
4) installing the image with chromeos-install.
|
|
|
|
@raises AutoservRepairMethodNA if the device does not have servo
|
|
support.
|
|
|
|
"""
|
|
if not self.servo:
|
|
raise error.AutoservRepairMethodNA('Repair Reinstall NA: '
|
|
'DUT has no servo support.')
|
|
|
|
logging.info('Attempting to recovery servo enabled device with '
|
|
'servo_repair_reinstall')
|
|
|
|
image_url = self.stage_image_for_servo()
|
|
self.servo_install(image_url)
|
|
|
|
|
|
def _is_firmware_repair_supported(self):
|
|
"""Check if the firmware repair is supported.
|
|
|
|
The firmware repair is only applicable to DUTs in pools listed in
|
|
global config CROS/pools_support_firmware_repair.
|
|
|
|
@return: True if it is supported; otherwise False.
|
|
"""
|
|
logging.info('Checking if host %s can be repaired with firmware '
|
|
'repair.', self.hostname)
|
|
pools = server_utils.get_labels_from_afe(self.hostname, 'pool:',
|
|
self._AFE)
|
|
pools_support_firmware_repair = CONFIG.get_config_value('CROS',
|
|
'pools_support_firmware_repair', type=str).split(',')
|
|
|
|
return (pools and pools_support_firmware_repair and
|
|
set(pools).intersection(set(pools_support_firmware_repair)))
|
|
|
|
|
|
def _firmware_repair(self):
|
|
"""Reinstall the firmware image using servo.
|
|
|
|
This repair function attempts to install the stable firmware specified
|
|
by the stable firmware version.
|
|
Then reset the DUT and try to verify it. If verify fails, it will try to
|
|
install the CrOS image using servo.
|
|
"""
|
|
if not self._is_firmware_repair_supported():
|
|
logging.info('Host is not in pools that support firmware repair.')
|
|
raise error.AutoservRepairMethodNA(
|
|
'Firmware repair is not applicable to host %s.' %
|
|
self.hostname)
|
|
|
|
# To repair a DUT connected to a moblab, try to create a servo object if
|
|
# it was failed to be created earlier as there may be a servo_host host
|
|
# attribute for this host.
|
|
if utils.is_moblab():
|
|
self._setup_servo()
|
|
|
|
if not self.servo:
|
|
raise error.AutoservRepairMethodNA('Repair Reinstall NA: '
|
|
'DUT has no servo support.')
|
|
|
|
logging.info('Attempting to recovery servo enabled device with '
|
|
'firmware_repair.')
|
|
self.firmware_install()
|
|
|
|
logging.info('Firmware repaired. Check if the DUT can boot. If not, '
|
|
'reinstall the CrOS using servo.')
|
|
try:
|
|
self.verify()
|
|
except Exception as e:
|
|
logging.warn('Failed to verify DUT, error: %s. Will try to repair '
|
|
'the DUT with servo_repair_reinstall.', e)
|
|
self._servo_repair_reinstall()
|
|
|
|
|
|
def _servo_repair_power(self):
|
|
"""Attempt to repair DUT using an attached Servo.
|
|
|
|
Attempt to power cycle the DUT via cold_reset.
|
|
|
|
@raises AutoservRepairMethodNA if the device does not have servo
|
|
support.
|
|
@raises AutoservRepairFailure if the repair fails for any reason.
|
|
"""
|
|
if not self.servo:
|
|
raise error.AutoservRepairMethodNA('Repair Power NA: '
|
|
'DUT has no servo support.')
|
|
|
|
logging.info('Attempting to recover servo enabled device by '
|
|
'powering cycling with cold reset.')
|
|
self.servo.get_power_state_controller().reset()
|
|
if self.wait_up(self.BOOT_TIMEOUT):
|
|
return
|
|
|
|
raise error.AutoservRepairFailure('DUT did not boot after long_press.')
|
|
|
|
|
|
def _powercycle_to_repair(self):
|
|
"""Utilize the RPM Infrastructure to bring the host back up.
|
|
|
|
If the host is not up/repaired after the first powercycle we utilize
|
|
auto fallback to the last good install by powercycling and rebooting the
|
|
host 6 times.
|
|
|
|
@raises AutoservRepairMethodNA if the device does not support remote
|
|
power.
|
|
@raises AutoservRepairFailure if the repair fails for any reason.
|
|
|
|
"""
|
|
if not self.has_power():
|
|
raise error.AutoservRepairMethodNA('Device does not support power.')
|
|
|
|
logging.info('Attempting repair via RPM powercycle.')
|
|
failed_cycles = 0
|
|
self.power_cycle()
|
|
while not self.wait_up(timeout=self.BOOT_TIMEOUT):
|
|
failed_cycles += 1
|
|
if failed_cycles >= self._MAX_POWER_CYCLE_ATTEMPTS:
|
|
raise error.AutoservRepairFailure(
|
|
'Powercycled host %s %d times; device did not come back'
|
|
' online.' % (self.hostname, failed_cycles))
|
|
self.power_cycle()
|
|
if failed_cycles == 0:
|
|
logging.info('Powercycling was successful first time.')
|
|
else:
|
|
logging.info('Powercycling was successful after %d failures.',
|
|
failed_cycles)
|
|
|
|
|
|
def _reboot_repair(self):
|
|
"""SSH to this host and reboot."""
|
|
if not self.is_up(self._CHECK_HOST_UP_TIMEOUT_SECS):
|
|
raise error.AutoservRepairMethodNA('DUT unreachable for reboot.')
|
|
logging.info('Attempting repair via SSH reboot.')
|
|
self.reboot(timeout=self.BOOT_TIMEOUT, wait=True)
|
|
|
|
|
|
def check_device(self):
|
|
"""Check if a device is ssh-able, and if so, clean and verify it.
|
|
|
|
@raise AutoservSSHTimeout: If the ssh ping times out.
|
|
@raise AutoservSshPermissionDeniedError: If ssh ping fails due to
|
|
permissions.
|
|
@raise AutoservSshPingHostError: For other AutoservRunErrors during
|
|
ssh_ping.
|
|
@raises AutoservError: As appropriate, during cleanup and verify.
|
|
"""
|
|
self.ssh_ping()
|
|
self.cleanup()
|
|
self.verify()
|
|
|
|
|
|
def confirm_servo(self):
|
|
"""Confirm servo is initialized and verified.
|
|
|
|
@raise AutoservError: If servo is not initialized and verified.
|
|
"""
|
|
if self.servo and self._servo_host.required_by_test:
|
|
return
|
|
|
|
# Force to re-create the servo object to make sure servo is verified.
|
|
logging.debug('Rebuilding the servo object.')
|
|
self.servo = None
|
|
self._servo_host = None
|
|
self._setup_servo()
|
|
if not self.servo:
|
|
raise error.AutoservError('Failed to create servo object.')
|
|
|
|
|
|
def _is_last_provision_failed(self):
|
|
"""Checks if the last provision job failed.
|
|
|
|
@return: True if there exists file /var/tmp/provision_failed, which
|
|
indicates the last provision job failed.
|
|
False if the file does not exist or the dut can't be reached.
|
|
"""
|
|
try:
|
|
result = self.run('test -f %s' % PROVISION_FAILED,
|
|
ignore_status=True, timeout=5)
|
|
return result.exit_status == 0
|
|
except (error.AutoservRunError, error.AutoservSSHTimeout):
|
|
# Default to False, for repair to try all repair method if the dut
|
|
# can't be reached.
|
|
return False
|
|
|
|
|
|
def repair(self):
|
|
"""Attempt to get the DUT to pass `self.verify()`.
|
|
|
|
This overrides the base class function for repair; it does
|
|
not call back to the parent class, but instead offers a
|
|
simplified implementation based on the capabilities in the
|
|
Chrome OS test lab.
|
|
|
|
It first verifies and repairs servo if it is a DUT in CrOS
|
|
lab and a servo is attached.
|
|
|
|
This escalates in order through the following procedures and verifies
|
|
the status using `self.check_device()` after each of them. This is done
|
|
until both the repair and the veryfing step succeed.
|
|
|
|
Escalation order of repair procedures from less intrusive to
|
|
more intrusive repairs:
|
|
1. SSH to the DUT and reboot.
|
|
2. If there's a servo for the DUT, try to power the DUT off and
|
|
on.
|
|
3. If the DUT can be power-cycled via RPM, try to repair
|
|
by power-cycling.
|
|
4. Try to re-install to a known stable image using
|
|
auto-update.
|
|
5. If there's a servo for the DUT, try to re-install via
|
|
the servo.
|
|
|
|
As with the parent method, the last operation performed on
|
|
the DUT must be to call `self.check_device()`; If that call fails the
|
|
exception it raises is passed back to the caller.
|
|
|
|
@raises AutoservRepairTotalFailure if the repair process fails to
|
|
fix the DUT.
|
|
@raises ServoHostRepairTotalFailure if the repair process fails to
|
|
fix the servo host if one is attached to the DUT.
|
|
@raises AutoservSshPermissionDeniedError if it is unable
|
|
to ssh to the servo host due to permission error.
|
|
|
|
"""
|
|
# Caution: Deleting shards relies on repair to always reboot the DUT.
|
|
|
|
# To repair a DUT connected to a moblab, try to create a servo object if
|
|
# it was failed to be created earlier as there may be a servo_host host
|
|
# attribute for this host.
|
|
if utils.is_moblab():
|
|
self._setup_servo()
|
|
|
|
if self._servo_host and not self.servo:
|
|
try:
|
|
self._servo_host.repair()
|
|
except Exception as e:
|
|
logging.error('Could not create a healthy servo: %s', e)
|
|
self.servo = self._servo_host.get_servo()
|
|
|
|
self.try_collect_crashlogs()
|
|
|
|
# TODO(scottz): This should use something similar to label_decorator,
|
|
# but needs to be populated in order so DUTs are repaired with the
|
|
# least amount of effort.
|
|
force_powerwash = self._is_last_provision_failed()
|
|
if force_powerwash:
|
|
logging.info('Last provision failed, try powerwash first.')
|
|
autotest_stats.Counter(
|
|
'repair_force_powerwash.TOTAL').increment()
|
|
repair_funcs = [self._firmware_repair,
|
|
self._install_repair_with_powerwash,
|
|
self._servo_repair_reinstall]
|
|
else:
|
|
repair_funcs = [self._reboot_repair,
|
|
self._servo_repair_power,
|
|
self._firmware_repair,
|
|
self._powercycle_to_repair,
|
|
self._install_repair,
|
|
self._install_repair_with_powerwash,
|
|
self._servo_repair_reinstall]
|
|
errors = []
|
|
board = self._get_board_from_afe()
|
|
for repair_func in repair_funcs:
|
|
try:
|
|
repair_func()
|
|
self.try_collect_crashlogs()
|
|
self.check_device()
|
|
autotest_stats.Counter(
|
|
'%s.SUCCEEDED' % repair_func.__name__).increment()
|
|
if board:
|
|
autotest_stats.Counter(
|
|
'%s.%s.SUCCEEDED' % (repair_func.__name__,
|
|
board)).increment()
|
|
if force_powerwash:
|
|
autotest_stats.Counter(
|
|
'repair_force_powerwash.SUCCEEDED').increment()
|
|
return
|
|
except error.AutoservRepairMethodNA as e:
|
|
autotest_stats.Counter(
|
|
'%s.RepairNA' % repair_func.__name__).increment()
|
|
if board:
|
|
autotest_stats.Counter(
|
|
'%s.%s.RepairNA' % (repair_func.__name__,
|
|
board)).increment()
|
|
logging.warning('Repair function NA: %s', e)
|
|
errors.append(str(e))
|
|
except Exception as e:
|
|
autotest_stats.Counter(
|
|
'%s.FAILED' % repair_func.__name__).increment()
|
|
if board:
|
|
autotest_stats.Counter(
|
|
'%s.%s.FAILED' % (repair_func.__name__,
|
|
board)).increment()
|
|
logging.warning('Failed to repair device: %s', e)
|
|
errors.append(str(e))
|
|
|
|
if force_powerwash:
|
|
autotest_stats.Counter(
|
|
'repair_force_powerwash.FAILED').increment()
|
|
autotest_stats.Counter('Full_Repair_Failed').increment()
|
|
if board:
|
|
autotest_stats.Counter(
|
|
'Full_Repair_Failed.%s' % board).increment()
|
|
raise error.AutoservRepairTotalFailure(
|
|
'All attempts at repairing the device failed:\n%s' %
|
|
'\n'.join(errors))
|
|
|
|
|
|
def try_collect_crashlogs(self, check_host_up=True):
|
|
"""
|
|
Check if a host is up and logs need to be collected from the host,
|
|
if yes, collect them.
|
|
|
|
@param check_host_up: Flag for checking host is up. Default is True.
|
|
"""
|
|
try:
|
|
crash_job = self._need_crash_logs()
|
|
if crash_job:
|
|
logging.debug('%s: Job %s was crashed', self._CRASHLOGS_PREFIX,
|
|
crash_job)
|
|
if not check_host_up or self.is_up(
|
|
self._CHECK_HOST_UP_TIMEOUT_SECS):
|
|
self._collect_crashlogs(crash_job)
|
|
logging.debug('%s: Completed collecting logs for the '
|
|
'crashed job %s', self._CRASHLOGS_PREFIX,
|
|
crash_job)
|
|
except Exception as e:
|
|
# Exception should not result in repair failure.
|
|
# Therefore, suppress all exceptions here.
|
|
logging.error('%s: Failed while trying to collect crash-logs: %s',
|
|
self._CRASHLOGS_PREFIX, e)
|
|
|
|
|
|
def _need_crash_logs(self):
|
|
"""Get the value of need_crash_logs attribute of this host.
|
|
|
|
@return: Value string of need_crash_logs attribute
|
|
None if there is no need_crash_logs attribute
|
|
"""
|
|
attrs = self._AFE.get_host_attribute(constants.CRASHLOGS_HOST_ATTRIBUTE,
|
|
hostname=self.hostname)
|
|
assert len(attrs) < 2
|
|
return attrs[0].value if attrs else None
|
|
|
|
|
|
def _collect_crashlogs(self, job_id):
|
|
"""Grab logs from the host where a job was crashed.
|
|
|
|
First, check if PRIOR_LOGS_DIR exists in the host.
|
|
If yes, collect them.
|
|
Otherwise, check if a lab-machine marker (_LAB_MACHINE_FILE) exists
|
|
in the host.
|
|
If yes, the host was repaired automatically, and we collect normal
|
|
system logs.
|
|
|
|
@param job_id: Id of the job that was crashed.
|
|
"""
|
|
crashlogs_dir = crashcollect.get_crashinfo_dir(self,
|
|
constants.CRASHLOGS_DEST_DIR_PREFIX)
|
|
flag_prior_logs = False
|
|
|
|
if self.path_exists(client_constants.PRIOR_LOGS_DIR):
|
|
flag_prior_logs = True
|
|
self._collect_prior_logs(crashlogs_dir)
|
|
elif self.path_exists(self._LAB_MACHINE_FILE):
|
|
self._collect_system_logs(crashlogs_dir)
|
|
else:
|
|
logging.warning('%s: Host was manually re-installed without '
|
|
'--lab_preserve_log option. Skip collecting '
|
|
'crash-logs.', self._CRASHLOGS_PREFIX)
|
|
|
|
# We make crash collection be one-time effort.
|
|
# _collect_prior_logs() and _collect_system_logs() will not throw
|
|
# any exception, and following codes will be executed even when
|
|
# those methods fail.
|
|
# _collect_crashlogs() is called only when the host is up (refer
|
|
# to try_collect_crashlogs()). We assume _collect_prior_logs() and
|
|
# _collect_system_logs() fail rarely when the host is up.
|
|
# In addition, it is not clear how many times we should try crash
|
|
# collection again while not triggering next repair unnecessarily.
|
|
# Threfore, we try crash collection one time.
|
|
|
|
# Create a marker file as soon as log collection is done.
|
|
# Leave the job id to this marker for gs_offloader to consume.
|
|
marker_file = os.path.join(crashlogs_dir, constants.CRASHLOGS_MARKER)
|
|
with open(marker_file, 'a') as f:
|
|
f.write('%s\n' % job_id)
|
|
|
|
# Remove need_crash_logs attribute
|
|
logging.debug('%s: Remove attribute need_crash_logs from host %s',
|
|
self._CRASHLOGS_PREFIX, self.hostname)
|
|
self._AFE.set_host_attribute(constants.CRASHLOGS_HOST_ATTRIBUTE,
|
|
None, hostname=self.hostname)
|
|
|
|
if flag_prior_logs:
|
|
logging.debug('%s: Remove %s from host %s', self._CRASHLOGS_PREFIX,
|
|
client_constants.PRIOR_LOGS_DIR, self.hostname)
|
|
self.run('rm -rf %s; sync' % client_constants.PRIOR_LOGS_DIR)
|
|
# Wait for a few seconds to make sure the prior command is
|
|
# done deep through storage.
|
|
time.sleep(self._SAFE_WAIT_SECS)
|
|
|
|
|
|
def _collect_prior_logs(self, crashlogs_dir):
|
|
"""Grab prior logs that were stashed before re-installing a host.
|
|
|
|
@param crashlogs_dir: Directory path where crash-logs are stored.
|
|
"""
|
|
logging.debug('%s: Found %s, collecting them...',
|
|
self._CRASHLOGS_PREFIX, client_constants.PRIOR_LOGS_DIR)
|
|
try:
|
|
self.collect_logs(client_constants.PRIOR_LOGS_DIR,
|
|
crashlogs_dir, False)
|
|
logging.debug('%s: %s is collected',
|
|
self._CRASHLOGS_PREFIX, client_constants.PRIOR_LOGS_DIR)
|
|
except Exception as e:
|
|
logging.error('%s: Failed to collect %s: %s',
|
|
self._CRASHLOGS_PREFIX, client_constants.PRIOR_LOGS_DIR,
|
|
e)
|
|
|
|
|
|
def _collect_system_logs(self, crashlogs_dir):
|
|
"""Grab normal system logs from a host.
|
|
|
|
@param crashlogs_dir: Directory path where crash-logs are stored.
|
|
"""
|
|
logging.debug('%s: Found %s, collecting system logs...',
|
|
self._CRASHLOGS_PREFIX, self._LAB_MACHINE_FILE)
|
|
sources = server_utils.parse_simple_config(self._LOGS_TO_COLLECT_FILE)
|
|
for src in sources:
|
|
try:
|
|
if self.path_exists(src):
|
|
logging.debug('%s: Collecting %s...',
|
|
self._CRASHLOGS_PREFIX, src)
|
|
dest = server_utils.concat_path_except_last(
|
|
crashlogs_dir, src)
|
|
self.collect_logs(src, dest, False)
|
|
logging.debug('%s: %s is collected',
|
|
self._CRASHLOGS_PREFIX, src)
|
|
except Exception as e:
|
|
logging.error('%s: Failed to collect %s: %s',
|
|
self._CRASHLOGS_PREFIX, src, e)
|
|
|
|
|
|
def close(self):
|
|
super(CrosHost, self).close()
|
|
|
|
|
|
def get_power_supply_info(self):
|
|
"""Get the output of power_supply_info.
|
|
|
|
power_supply_info outputs the info of each power supply, e.g.,
|
|
Device: Line Power
|
|
online: no
|
|
type: Mains
|
|
voltage (V): 0
|
|
current (A): 0
|
|
Device: Battery
|
|
state: Discharging
|
|
percentage: 95.9276
|
|
technology: Li-ion
|
|
|
|
Above output shows two devices, Line Power and Battery, with details of
|
|
each device listed. This function parses the output into a dictionary,
|
|
with key being the device name, and value being a dictionary of details
|
|
of the device info.
|
|
|
|
@return: The dictionary of power_supply_info, e.g.,
|
|
{'Line Power': {'online': 'yes', 'type': 'main'},
|
|
'Battery': {'vendor': 'xyz', 'percentage': '100'}}
|
|
@raise error.AutoservRunError if power_supply_info tool is not found in
|
|
the DUT. Caller should handle this error to avoid false failure
|
|
on verification.
|
|
"""
|
|
result = self.run('power_supply_info').stdout.strip()
|
|
info = {}
|
|
device_name = None
|
|
device_info = {}
|
|
for line in result.split('\n'):
|
|
pair = [v.strip() for v in line.split(':')]
|
|
if len(pair) != 2:
|
|
continue
|
|
if pair[0] == 'Device':
|
|
if device_name:
|
|
info[device_name] = device_info
|
|
device_name = pair[1]
|
|
device_info = {}
|
|
else:
|
|
device_info[pair[0]] = pair[1]
|
|
if device_name and not device_name in info:
|
|
info[device_name] = device_info
|
|
return info
|
|
|
|
|
|
def get_battery_percentage(self):
|
|
"""Get the battery percentage.
|
|
|
|
@return: The percentage of battery level, value range from 0-100. Return
|
|
None if the battery info cannot be retrieved.
|
|
"""
|
|
try:
|
|
info = self.get_power_supply_info()
|
|
logging.info(info)
|
|
return float(info['Battery']['percentage'])
|
|
except (KeyError, ValueError, error.AutoservRunError):
|
|
return None
|
|
|
|
|
|
def is_ac_connected(self):
|
|
"""Check if the dut has power adapter connected and charging.
|
|
|
|
@return: True if power adapter is connected and charging.
|
|
"""
|
|
try:
|
|
info = self.get_power_supply_info()
|
|
return info['Line Power']['online'] == 'yes'
|
|
except (KeyError, error.AutoservRunError):
|
|
return None
|
|
|
|
|
|
def _cleanup_poweron(self):
|
|
"""Special cleanup method to make sure hosts always get power back."""
|
|
afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
|
|
hosts = afe.get_hosts(hostname=self.hostname)
|
|
if not hosts or not (self._RPM_OUTLET_CHANGED in
|
|
hosts[0].attributes):
|
|
return
|
|
logging.debug('This host has recently interacted with the RPM'
|
|
' Infrastructure. Ensuring power is on.')
|
|
try:
|
|
self.power_on()
|
|
afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None,
|
|
hostname=self.hostname)
|
|
except rpm_client.RemotePowerException:
|
|
logging.error('Failed to turn Power On for this host after '
|
|
'cleanup through the RPM Infrastructure.')
|
|
autotest_es.post(
|
|
type_str='RPM_poweron_failure',
|
|
metadata={'hostname': self.hostname})
|
|
|
|
battery_percentage = self.get_battery_percentage()
|
|
if battery_percentage and battery_percentage < 50:
|
|
raise
|
|
elif self.is_ac_connected():
|
|
logging.info('The device has power adapter connected and '
|
|
'charging. No need to try to turn RPM on '
|
|
'again.')
|
|
afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None,
|
|
hostname=self.hostname)
|
|
logging.info('Battery level is now at %s%%. The device may '
|
|
'still have enough power to run test, so no '
|
|
'exception will be raised.', battery_percentage)
|
|
|
|
|
|
def _is_factory_image(self):
|
|
"""Checks if the image on the DUT is a factory image.
|
|
|
|
@return: True if the image on the DUT is a factory image.
|
|
False otherwise.
|
|
"""
|
|
result = self.run('[ -f /root/.factory_test ]', ignore_status=True)
|
|
return result.exit_status == 0
|
|
|
|
|
|
def _restart_ui(self):
|
|
"""Restart the Chrome UI.
|
|
|
|
@raises: FactoryImageCheckerException for factory images, since
|
|
we cannot attempt to restart ui on them.
|
|
error.AutoservRunError for any other type of error that
|
|
occurs while restarting ui.
|
|
"""
|
|
if self._is_factory_image():
|
|
raise FactoryImageCheckerException('Cannot restart ui on factory '
|
|
'images')
|
|
|
|
# TODO(jrbarnette): The command to stop/start the ui job
|
|
# should live inside cros_ui, too. However that would seem
|
|
# to imply interface changes to the existing start()/restart()
|
|
# functions, which is a bridge too far (for now).
|
|
prompt = cros_ui.get_chrome_session_ident(self)
|
|
self.run('stop ui; start ui')
|
|
cros_ui.wait_for_chrome_ready(prompt, self)
|
|
|
|
|
|
def get_release_version(self):
|
|
"""Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release.
|
|
|
|
@returns The version string in lsb-release, under attribute
|
|
CHROMEOS_RELEASE_VERSION.
|
|
"""
|
|
lsb_release_content = self.run(
|
|
'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
|
|
return lsbrelease_utils.get_chromeos_release_version(
|
|
lsb_release_content=lsb_release_content)
|
|
|
|
|
|
def verify_cros_version_label(self):
|
|
""" Make sure host's cros-version label match the actual image in dut.
|
|
|
|
Remove any cros-version: label that doesn't match that installed in
|
|
the dut.
|
|
|
|
@param raise_error: Set to True to raise exception if any mismatch found
|
|
|
|
@raise error.AutoservError: If any mismatch between cros-version label
|
|
and the build installed in dut is found.
|
|
"""
|
|
labels = self._AFE.get_labels(
|
|
name__startswith=ds_constants.VERSION_PREFIX,
|
|
host__hostname=self.hostname)
|
|
mismatch_found = False
|
|
if labels:
|
|
# Get CHROMEOS_RELEASE_VERSION from lsb-release, e.g., 6908.0.0.
|
|
# Note that it's different from cros-version label, which has
|
|
# builder and branch info, e.g.,
|
|
# cros-version:peppy-release/R43-6908.0.0
|
|
release_version = self.get_release_version()
|
|
host_list = [self.hostname]
|
|
for label in labels:
|
|
# Remove any cros-version label that does not match
|
|
# release_version.
|
|
build_version = label.name[len(ds_constants.VERSION_PREFIX):]
|
|
if not utils.version_match(build_version, release_version):
|
|
logging.warn('cros-version label "%s" does not match '
|
|
'release version %s. Removing the label.',
|
|
label.name, release_version)
|
|
label.remove_hosts(hosts=host_list)
|
|
mismatch_found = True
|
|
if mismatch_found:
|
|
autotest_es.post(use_http=True,
|
|
type_str='cros_version_label_mismatch',
|
|
metadata={'hostname': self.hostname})
|
|
raise error.AutoservError('The host has wrong cros-version label.')
|
|
|
|
|
|
def verify_tpm_status(self):
|
|
""" Verify the host's TPM is in a good state.
|
|
|
|
@raise error.AutoservError: If state is not good.
|
|
"""
|
|
# This cryptohome command emits status information in JSON format. It
|
|
# looks something like this:
|
|
# {
|
|
# "installattrs": {
|
|
# "first_install": false,
|
|
# "initialized": true,
|
|
# "invalid": false,
|
|
# "lockbox_index": 536870916,
|
|
# "lockbox_nvram_version": 2,
|
|
# "secure": true,
|
|
# "size": 0,
|
|
# "version": 1
|
|
# },
|
|
# "mounts": [ {
|
|
# "enterprise": false,
|
|
# "keysets": [ {
|
|
# "current": true,
|
|
# "index": 0,
|
|
# "last_activity": 1330111359,
|
|
# "ok": true,
|
|
# "scrypt": true,
|
|
# "tpm": false
|
|
# } ],
|
|
# "mounted": true,
|
|
# "owner": "dbb3dd34edb181245130e136be51fa08478d3909"
|
|
# } ],
|
|
# "tpm": {
|
|
# "being_owned": false,
|
|
# "can_connect": true,
|
|
# "can_decrypt": false,
|
|
# "can_encrypt": false,
|
|
# "can_load_srk": true,
|
|
# "can_load_srk_pubkey": true,
|
|
# "enabled": true,
|
|
# "has_context": true,
|
|
# "has_cryptohome_key": false,
|
|
# "has_key_handle": false,
|
|
# "last_error": 0,
|
|
# "owned": true
|
|
# }
|
|
# }
|
|
output = self.run('cryptohome --action=status').stdout.strip()
|
|
try:
|
|
status = json.loads(output)
|
|
except ValueError:
|
|
logging.error('TPM_VERIFY: Cryptohome did not return valid status.')
|
|
return
|
|
try:
|
|
tpm = status['tpm']
|
|
if (not tpm['enabled'] or not tpm['can_connect'] or
|
|
(tpm['owned'] and not tpm['can_load_srk']) or
|
|
(tpm['can_load_srk'] and not tpm['can_load_srk_pubkey'])):
|
|
logging.error('TPM_VERIFY: The host TPM is in a bad state.')
|
|
raise error.AutoservError('The host TPM is in a bad state.')
|
|
else:
|
|
logging.debug('TPM_VERIFY: The host TPM is in a good state.')
|
|
except KeyError:
|
|
logging.error('TPM_VERIFY: Cryptohome did not return valid status.')
|
|
|
|
|
|
def verify_firmware_status(self):
|
|
"""Verify the host's firmware is in a good state.
|
|
|
|
@raise error.AutoservError: If state is not good.
|
|
"""
|
|
if self._is_firmware_repair_supported():
|
|
try:
|
|
# Read the AP firmware and dump the sections we are interested.
|
|
cmd = ('mkdir /tmp/verify_firmware; '
|
|
'cd /tmp/verify_firmware; '
|
|
'for section in VBLOCK_A VBLOCK_B FW_MAIN_A FW_MAIN_B; '
|
|
'do flashrom -r image.bin -i $section:$section; '
|
|
'done')
|
|
self.run(cmd)
|
|
|
|
# Verify the firmware blocks A and B.
|
|
cmd = ('vbutil_firmware --verify /tmp/verify_firmware/VBLOCK_%c'
|
|
' --signpubkey /usr/share/vboot/devkeys/root_key.vbpubk'
|
|
' --fv /tmp/verify_firmware/FW_MAIN_%c')
|
|
for c in ('A', 'B'):
|
|
rv = self.run(cmd % (c, c), ignore_status=True)
|
|
if rv.exit_status:
|
|
raise error.AutoservError(
|
|
'Firmware %c is in a bad state.' % c)
|
|
finally:
|
|
# Remove the tempoary files.
|
|
self.run('rm -rf /tmp/verify_firmware')
|
|
else:
|
|
logging.info('Do not care about firmware status when the host '
|
|
'is not in pools that support firmware repair.')
|
|
|
|
|
|
def verify_filesystem_write_status(self):
|
|
"""Verify the DUT's filesystem is read/writable
|
|
|
|
@raise error.AutoservError: if filesystem is not writable.
|
|
"""
|
|
# try to create & delete a file
|
|
filename = "/mnt/stateful_partition/test.txt"
|
|
cmd = 'touch %s && rm %s' % (filename, filename)
|
|
rv = self.run(command=cmd, ignore_status=True)
|
|
|
|
if rv.exit_status == 1:
|
|
raise error.AutoservError('DUT filesystem is read-only.')
|
|
|
|
|
|
def cleanup(self):
|
|
self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE)
|
|
try:
|
|
self._restart_ui()
|
|
except (error.AutotestRunError, error.AutoservRunError,
|
|
FactoryImageCheckerException):
|
|
logging.warning('Unable to restart ui, rebooting device.')
|
|
# Since restarting the UI fails fall back to normal Autotest
|
|
# cleanup routines, i.e. reboot the machine.
|
|
super(CrosHost, self).cleanup()
|
|
# Check if the rpm outlet was manipulated.
|
|
if self.has_power():
|
|
self._cleanup_poweron()
|
|
self.verify_cros_version_label()
|
|
|
|
|
|
def reboot(self, **dargs):
|
|
"""
|
|
This function reboots the site host. The more generic
|
|
RemoteHost.reboot() performs sync and sleeps for 5
|
|
seconds. This is not necessary for Chrome OS devices as the
|
|
sync should be finished in a short time during the reboot
|
|
command.
|
|
"""
|
|
if 'reboot_cmd' not in dargs:
|
|
reboot_timeout = dargs.get('reboot_timeout', 10)
|
|
dargs['reboot_cmd'] = ('sleep 1; '
|
|
'reboot & sleep %d; '
|
|
'reboot -f' % reboot_timeout)
|
|
# Enable fastsync to avoid running extra sync commands before reboot.
|
|
if 'fastsync' not in dargs:
|
|
dargs['fastsync'] = True
|
|
|
|
# For purposes of logging reboot times:
|
|
# Get the board name i.e. 'daisy_spring'
|
|
board_fullname = self.get_board()
|
|
|
|
# Strip the prefix and add it to dargs.
|
|
dargs['board'] = board_fullname[board_fullname.find(':')+1:]
|
|
super(CrosHost, self).reboot(**dargs)
|
|
|
|
|
|
def suspend(self, **dargs):
|
|
"""
|
|
This function suspends the site host.
|
|
"""
|
|
suspend_time = dargs.get('suspend_time', 60)
|
|
dargs['timeout'] = suspend_time
|
|
if 'suspend_cmd' not in dargs:
|
|
dargs['suspend_cmd'] = ' && '.join([
|
|
'echo 0 > /sys/class/rtc/rtc0/wakealarm',
|
|
'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time,
|
|
'powerd_dbus_suspend --delay=0'])
|
|
super(CrosHost, self).suspend(**dargs)
|
|
|
|
|
|
def upstart_status(self, service_name):
|
|
"""Check the status of an upstart init script.
|
|
|
|
@param service_name: Service to look up.
|
|
|
|
@returns True if the service is running, False otherwise.
|
|
"""
|
|
return self.run('status %s | grep start/running' %
|
|
service_name).stdout.strip() != ''
|
|
|
|
|
|
def verify_software(self):
|
|
"""Verify working software on a Chrome OS system.
|
|
|
|
Tests for the following conditions:
|
|
1. All conditions tested by the parent version of this
|
|
function.
|
|
2. Sufficient space in /mnt/stateful_partition.
|
|
3. Sufficient space in /mnt/stateful_partition/encrypted.
|
|
4. update_engine answers a simple status request over DBus.
|
|
|
|
"""
|
|
# Check if a job was crashed on this host.
|
|
# If yes, avoid verification until crash-logs are collected.
|
|
if self._need_crash_logs():
|
|
raise error.AutoservCrashLogCollectRequired(
|
|
'Need to collect crash-logs before verification')
|
|
|
|
super(CrosHost, self).verify_software()
|
|
default_kilo_inodes_required = CONFIG.get_config_value(
|
|
'SERVER', 'kilo_inodes_required', type=int, default=100)
|
|
board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
|
|
kilo_inodes_required = CONFIG.get_config_value(
|
|
'SERVER', 'kilo_inodes_required_%s' % board,
|
|
type=int, default=default_kilo_inodes_required)
|
|
self.check_inodes('/mnt/stateful_partition', kilo_inodes_required)
|
|
self.check_diskspace(
|
|
'/mnt/stateful_partition',
|
|
CONFIG.get_config_value(
|
|
'SERVER', 'gb_diskspace_required', type=float,
|
|
default=20.0))
|
|
encrypted_stateful_path = '/mnt/stateful_partition/encrypted'
|
|
# Not all targets build with encrypted stateful support.
|
|
if self.path_exists(encrypted_stateful_path):
|
|
self.check_diskspace(
|
|
encrypted_stateful_path,
|
|
CONFIG.get_config_value(
|
|
'SERVER', 'gb_encrypted_diskspace_required', type=float,
|
|
default=0.1))
|
|
|
|
if not self.upstart_status('system-services'):
|
|
raise error.AutoservError('Chrome failed to reach login. '
|
|
'System services not running.')
|
|
|
|
# Factory images don't run update engine,
|
|
# goofy controls dbus on these DUTs.
|
|
if not self._is_factory_image():
|
|
self.run('update_engine_client --status')
|
|
# Makes sure python is present, loads and can use built in functions.
|
|
# We have seen cases where importing cPickle fails with undefined
|
|
# symbols in cPickle.so.
|
|
self.run('python -c "import cPickle"')
|
|
|
|
self.verify_cros_version_label()
|
|
|
|
self.verify_tpm_status()
|
|
|
|
self.verify_firmware_status()
|
|
|
|
self.verify_filesystem_write_status()
|
|
|
|
|
|
def verify_hardware(self):
|
|
"""Verify hardware system of a Chrome OS system.
|
|
|
|
Check following hardware conditions:
|
|
1. Battery level.
|
|
2. Is power adapter connected.
|
|
"""
|
|
logging.info('Battery percentage: %s', self.get_battery_percentage())
|
|
if self.is_ac_connected() is None:
|
|
logging.info('Can not determine if the device has power adapter '
|
|
'connected.')
|
|
else:
|
|
logging.info('Device %s power adapter connected and charging.',
|
|
'has' if self.is_ac_connected() else 'does not have')
|
|
|
|
|
|
def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
|
|
connect_timeout=None, alive_interval=None):
|
|
"""Override default make_ssh_command to use options tuned for Chrome OS.
|
|
|
|
Tuning changes:
|
|
- ConnectTimeout=30; maximum of 30 seconds allowed for an SSH
|
|
connection failure. Consistency with remote_access.sh.
|
|
|
|
- ServerAliveInterval=900; which causes SSH to ping connection every
|
|
900 seconds. In conjunction with ServerAliveCountMax ensures
|
|
that if the connection dies, Autotest will bail out.
|
|
Originally tried 60 secs, but saw frequent job ABORTS where
|
|
the test completed successfully. Later increased from 180 seconds to
|
|
900 seconds to account for tests where the DUT is suspended for
|
|
longer periods of time.
|
|
|
|
- ServerAliveCountMax=3; consistency with remote_access.sh.
|
|
|
|
- ConnectAttempts=4; reduce flakiness in connection errors;
|
|
consistency with remote_access.sh.
|
|
|
|
- UserKnownHostsFile=/dev/null; we don't care about the keys.
|
|
Host keys change with every new installation, don't waste
|
|
memory/space saving them.
|
|
|
|
- SSH protocol forced to 2; needed for ServerAliveInterval.
|
|
|
|
@param user User name to use for the ssh connection.
|
|
@param port Port on the target host to use for ssh connection.
|
|
@param opts Additional options to the ssh command.
|
|
@param hosts_file Ignored.
|
|
@param connect_timeout Ignored.
|
|
@param alive_interval Ignored.
|
|
"""
|
|
base_command = ('/usr/bin/ssh -a -x %s %s %s'
|
|
' -o StrictHostKeyChecking=no'
|
|
' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
|
|
' -o ConnectTimeout=30 -o ServerAliveInterval=900'
|
|
' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
|
|
' -o Protocol=2 -l %s -p %d')
|
|
return base_command % (self._ssh_verbosity_flag, self._ssh_options,
|
|
opts, user, port)
|
|
def syslog(self, message, tag='autotest'):
|
|
"""Logs a message to syslog on host.
|
|
|
|
@param message String message to log into syslog
|
|
@param tag String tag prefix for syslog
|
|
|
|
"""
|
|
self.run('logger -t "%s" "%s"' % (tag, message))
|
|
|
|
|
|
def _ping_check_status(self, status):
|
|
"""Ping the host once, and return whether it has a given status.
|
|
|
|
@param status Check the ping status against this value.
|
|
@return True iff `status` and the result of ping are the same
|
|
(i.e. both True or both False).
|
|
|
|
"""
|
|
ping_val = utils.ping(self.hostname, tries=1, deadline=1)
|
|
return not (status ^ (ping_val == 0))
|
|
|
|
def _ping_wait_for_status(self, status, timeout):
|
|
"""Wait for the host to have a given status (UP or DOWN).
|
|
|
|
Status is checked by polling. Polling will not last longer
|
|
than the number of seconds in `timeout`. The polling
|
|
interval will be long enough that only approximately
|
|
_PING_WAIT_COUNT polling cycles will be executed, subject
|
|
to a maximum interval of about one minute.
|
|
|
|
@param status Waiting will stop immediately if `ping` of the
|
|
host returns this status.
|
|
@param timeout Poll for at most this many seconds.
|
|
@return True iff the host status from `ping` matched the
|
|
requested status at the time of return.
|
|
|
|
"""
|
|
# _ping_check_status() takes about 1 second, hence the
|
|
# "- 1" in the formula below.
|
|
poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1
|
|
end_time = time.time() + timeout
|
|
while time.time() <= end_time:
|
|
if self._ping_check_status(status):
|
|
return True
|
|
if poll_interval > 0:
|
|
time.sleep(poll_interval)
|
|
|
|
# The last thing we did was sleep(poll_interval), so it may
|
|
# have been too long since the last `ping`. Check one more
|
|
# time, just to be sure.
|
|
return self._ping_check_status(status)
|
|
|
|
def ping_wait_up(self, timeout):
|
|
"""Wait for the host to respond to `ping`.
|
|
|
|
N.B. This method is not a reliable substitute for
|
|
`wait_up()`, because a host that responds to ping will not
|
|
necessarily respond to ssh. This method should only be used
|
|
if the target DUT can be considered functional even if it
|
|
can't be reached via ssh.
|
|
|
|
@param timeout Minimum time to allow before declaring the
|
|
host to be non-responsive.
|
|
@return True iff the host answered to ping before the timeout.
|
|
|
|
"""
|
|
return self._ping_wait_for_status(self._PING_STATUS_UP, timeout)
|
|
|
|
def ping_wait_down(self, timeout):
|
|
"""Wait until the host no longer responds to `ping`.
|
|
|
|
This function can be used as a slightly faster version of
|
|
`wait_down()`, by avoiding potentially long ssh timeouts.
|
|
|
|
@param timeout Minimum time to allow for the host to become
|
|
non-responsive.
|
|
@return True iff the host quit answering ping before the
|
|
timeout.
|
|
|
|
"""
|
|
return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout)
|
|
|
|
def test_wait_for_sleep(self, sleep_timeout=None):
|
|
"""Wait for the client to enter low-power sleep mode.
|
|
|
|
The test for "is asleep" can't distinguish a system that is
|
|
powered off; to confirm that the unit was asleep, it is
|
|
necessary to force resume, and then call
|
|
`test_wait_for_resume()`.
|
|
|
|
This function is expected to be called from a test as part
|
|
of a sequence like the following:
|
|
|
|
~~~~~~~~
|
|
boot_id = host.get_boot_id()
|
|
# trigger sleep on the host
|
|
host.test_wait_for_sleep()
|
|
# trigger resume on the host
|
|
host.test_wait_for_resume(boot_id)
|
|
~~~~~~~~
|
|
|
|
@param sleep_timeout time limit in seconds to allow the host sleep.
|
|
|
|
@exception TestFail The host did not go to sleep within
|
|
the allowed time.
|
|
"""
|
|
if sleep_timeout is None:
|
|
sleep_timeout = self.SLEEP_TIMEOUT
|
|
|
|
if not self.ping_wait_down(timeout=sleep_timeout):
|
|
raise error.TestFail(
|
|
'client failed to sleep after %d seconds' % sleep_timeout)
|
|
|
|
|
|
def test_wait_for_resume(self, old_boot_id, resume_timeout=None):
|
|
"""Wait for the client to resume from low-power sleep mode.
|
|
|
|
The `old_boot_id` parameter should be the value from
|
|
`get_boot_id()` obtained prior to entering sleep mode. A
|
|
`TestFail` exception is raised if the boot id changes.
|
|
|
|
See @ref test_wait_for_sleep for more on this function's
|
|
usage.
|
|
|
|
@param old_boot_id A boot id value obtained before the
|
|
target host went to sleep.
|
|
@param resume_timeout time limit in seconds to allow the host up.
|
|
|
|
@exception TestFail The host did not respond within the
|
|
allowed time.
|
|
@exception TestFail The host responded, but the boot id test
|
|
indicated a reboot rather than a sleep
|
|
cycle.
|
|
"""
|
|
if resume_timeout is None:
|
|
resume_timeout = self.RESUME_TIMEOUT
|
|
|
|
if not self.wait_up(timeout=resume_timeout):
|
|
raise error.TestFail(
|
|
'client failed to resume from sleep after %d seconds' %
|
|
resume_timeout)
|
|
else:
|
|
new_boot_id = self.get_boot_id()
|
|
if new_boot_id != old_boot_id:
|
|
logging.error('client rebooted (old boot %s, new boot %s)',
|
|
old_boot_id, new_boot_id)
|
|
raise error.TestFail(
|
|
'client rebooted, but sleep was expected')
|
|
|
|
|
|
def test_wait_for_shutdown(self, shutdown_timeout=None):
|
|
"""Wait for the client to shut down.
|
|
|
|
The test for "has shut down" can't distinguish a system that
|
|
is merely asleep; to confirm that the unit was down, it is
|
|
necessary to force boot, and then call test_wait_for_boot().
|
|
|
|
This function is expected to be called from a test as part
|
|
of a sequence like the following:
|
|
|
|
~~~~~~~~
|
|
boot_id = host.get_boot_id()
|
|
# trigger shutdown on the host
|
|
host.test_wait_for_shutdown()
|
|
# trigger boot on the host
|
|
host.test_wait_for_boot(boot_id)
|
|
~~~~~~~~
|
|
|
|
@param shutdown_timeout time limit in seconds to allow the host down.
|
|
@exception TestFail The host did not shut down within the
|
|
allowed time.
|
|
"""
|
|
if shutdown_timeout is None:
|
|
shutdown_timeout = self.SHUTDOWN_TIMEOUT
|
|
|
|
if not self.ping_wait_down(timeout=shutdown_timeout):
|
|
raise error.TestFail(
|
|
'client failed to shut down after %d seconds' %
|
|
shutdown_timeout)
|
|
|
|
|
|
def test_wait_for_boot(self, old_boot_id=None):
|
|
"""Wait for the client to boot from cold power.
|
|
|
|
The `old_boot_id` parameter should be the value from
|
|
`get_boot_id()` obtained prior to shutting down. A
|
|
`TestFail` exception is raised if the boot id does not
|
|
change. The boot id test is omitted if `old_boot_id` is not
|
|
specified.
|
|
|
|
See @ref test_wait_for_shutdown for more on this function's
|
|
usage.
|
|
|
|
@param old_boot_id A boot id value obtained before the
|
|
shut down.
|
|
|
|
@exception TestFail The host did not respond within the
|
|
allowed time.
|
|
@exception TestFail The host responded, but the boot id test
|
|
indicated that there was no reboot.
|
|
"""
|
|
if not self.wait_up(timeout=self.REBOOT_TIMEOUT):
|
|
raise error.TestFail(
|
|
'client failed to reboot after %d seconds' %
|
|
self.REBOOT_TIMEOUT)
|
|
elif old_boot_id:
|
|
if self.get_boot_id() == old_boot_id:
|
|
logging.error('client not rebooted (boot %s)',
|
|
old_boot_id)
|
|
raise error.TestFail(
|
|
'client is back up, but did not reboot')
|
|
|
|
|
|
@staticmethod
|
|
def check_for_rpm_support(hostname):
|
|
"""For a given hostname, return whether or not it is powered by an RPM.
|
|
|
|
@param hostname: hostname to check for rpm support.
|
|
|
|
@return None if this host does not follows the defined naming format
|
|
for RPM powered DUT's in the lab. If it does follow the format,
|
|
it returns a regular expression MatchObject instead.
|
|
"""
|
|
return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname)
|
|
|
|
|
|
def has_power(self):
|
|
"""For this host, return whether or not it is powered by an RPM.
|
|
|
|
@return True if this host is in the CROS lab and follows the defined
|
|
naming format.
|
|
"""
|
|
return CrosHost.check_for_rpm_support(self.hostname)
|
|
|
|
|
|
def _set_power(self, state, power_method):
|
|
"""Sets the power to the host via RPM, Servo or manual.
|
|
|
|
@param state Specifies which power state to set to DUT
|
|
@param power_method Specifies which method of power control to
|
|
use. By default "RPM" will be used. Valid values
|
|
are the strings "RPM", "manual", "servoj10".
|
|
|
|
"""
|
|
ACCEPTABLE_STATES = ['ON', 'OFF']
|
|
|
|
if state.upper() not in ACCEPTABLE_STATES:
|
|
raise error.TestError('State must be one of: %s.'
|
|
% (ACCEPTABLE_STATES,))
|
|
|
|
if power_method == self.POWER_CONTROL_SERVO:
|
|
logging.info('Setting servo port J10 to %s', state)
|
|
self.servo.set('prtctl3_pwren', state.lower())
|
|
time.sleep(self._USB_POWER_TIMEOUT)
|
|
elif power_method == self.POWER_CONTROL_MANUAL:
|
|
logging.info('You have %d seconds to set the AC power to %s.',
|
|
self._POWER_CYCLE_TIMEOUT, state)
|
|
time.sleep(self._POWER_CYCLE_TIMEOUT)
|
|
else:
|
|
if not self.has_power():
|
|
raise error.TestFail('DUT does not have RPM connected.')
|
|
afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10)
|
|
afe.set_host_attribute(self._RPM_OUTLET_CHANGED, True,
|
|
hostname=self.hostname)
|
|
rpm_client.set_power(self.hostname, state.upper(), timeout_mins=5)
|
|
|
|
|
|
def power_off(self, power_method=POWER_CONTROL_RPM):
|
|
"""Turn off power to this host via RPM, Servo or manual.
|
|
|
|
@param power_method Specifies which method of power control to
|
|
use. By default "RPM" will be used. Valid values
|
|
are the strings "RPM", "manual", "servoj10".
|
|
|
|
"""
|
|
self._set_power('OFF', power_method)
|
|
|
|
|
|
def power_on(self, power_method=POWER_CONTROL_RPM):
|
|
"""Turn on power to this host via RPM, Servo or manual.
|
|
|
|
@param power_method Specifies which method of power control to
|
|
use. By default "RPM" will be used. Valid values
|
|
are the strings "RPM", "manual", "servoj10".
|
|
|
|
"""
|
|
self._set_power('ON', power_method)
|
|
|
|
|
|
def power_cycle(self, power_method=POWER_CONTROL_RPM):
|
|
"""Cycle power to this host by turning it OFF, then ON.
|
|
|
|
@param power_method Specifies which method of power control to
|
|
use. By default "RPM" will be used. Valid values
|
|
are the strings "RPM", "manual", "servoj10".
|
|
|
|
"""
|
|
if power_method in (self.POWER_CONTROL_SERVO,
|
|
self.POWER_CONTROL_MANUAL):
|
|
self.power_off(power_method=power_method)
|
|
time.sleep(self._POWER_CYCLE_TIMEOUT)
|
|
self.power_on(power_method=power_method)
|
|
else:
|
|
rpm_client.set_power(self.hostname, 'CYCLE')
|
|
|
|
|
|
def get_platform(self):
|
|
"""Determine the correct platform label for this host.
|
|
|
|
@returns a string representing this host's platform.
|
|
"""
|
|
crossystem = utils.Crossystem(self)
|
|
crossystem.init()
|
|
# Extract fwid value and use the leading part as the platform id.
|
|
# fwid generally follow the format of {platform}.{firmware version}
|
|
# Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z
|
|
platform = crossystem.fwid().split('.')[0].lower()
|
|
# Newer platforms start with 'Google_' while the older ones do not.
|
|
return platform.replace('google_', '')
|
|
|
|
|
|
def get_architecture(self):
|
|
"""Determine the correct architecture label for this host.
|
|
|
|
@returns a string representing this host's architecture.
|
|
"""
|
|
crossystem = utils.Crossystem(self)
|
|
crossystem.init()
|
|
return crossystem.arch()
|
|
|
|
|
|
def get_chrome_version(self):
|
|
"""Gets the Chrome version number and milestone as strings.
|
|
|
|
Invokes "chrome --version" to get the version number and milestone.
|
|
|
|
@return A tuple (chrome_ver, milestone) where "chrome_ver" is the
|
|
current Chrome version number as a string (in the form "W.X.Y.Z")
|
|
and "milestone" is the first component of the version number
|
|
(the "W" from "W.X.Y.Z"). If the version number cannot be parsed
|
|
in the "W.X.Y.Z" format, the "chrome_ver" will be the full output
|
|
of "chrome --version" and the milestone will be the empty string.
|
|
|
|
"""
|
|
version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout
|
|
return utils.parse_chrome_version(version_string)
|
|
|
|
|
|
@label_decorator()
|
|
def get_board(self):
|
|
"""Determine the correct board label for this host.
|
|
|
|
@returns a string representing this host's board.
|
|
"""
|
|
release_info = utils.parse_cmd_output('cat /etc/lsb-release',
|
|
run_method=self.run)
|
|
return (ds_constants.BOARD_PREFIX +
|
|
release_info['CHROMEOS_RELEASE_BOARD'])
|
|
|
|
|
|
@label_decorator('lightsensor')
|
|
def has_lightsensor(self):
|
|
"""Determine the correct board label for this host.
|
|
|
|
@returns the string 'lightsensor' if this host has a lightsensor or
|
|
None if it does not.
|
|
"""
|
|
search_cmd = "find -L %s -maxdepth 4 | egrep '%s'" % (
|
|
self._LIGHTSENSOR_SEARCH_DIR, '|'.join(self._LIGHTSENSOR_FILES))
|
|
try:
|
|
# Run the search cmd following the symlinks. Stderr_tee is set to
|
|
# None as there can be a symlink loop, but the command will still
|
|
# execute correctly with a few messages printed to stderr.
|
|
self.run(search_cmd, stdout_tee=None, stderr_tee=None)
|
|
return 'lightsensor'
|
|
except error.AutoservRunError:
|
|
# egrep exited with a return code of 1 meaning none of the possible
|
|
# lightsensor files existed.
|
|
return None
|
|
|
|
|
|
@label_decorator('bluetooth')
|
|
def has_bluetooth(self):
|
|
"""Determine the correct board label for this host.
|
|
|
|
@returns the string 'bluetooth' if this host has bluetooth or
|
|
None if it does not.
|
|
"""
|
|
try:
|
|
self.run('test -d /sys/class/bluetooth/hci0')
|
|
# test exited with a return code of 0.
|
|
return 'bluetooth'
|
|
except error.AutoservRunError:
|
|
# test exited with a return code 1 meaning the directory did not
|
|
# exist.
|
|
return None
|
|
|
|
|
|
@label_decorator('ec')
|
|
def get_ec(self):
|
|
"""
|
|
Determine the type of EC on this host.
|
|
|
|
@returns a string representing this host's embedded controller type.
|
|
At present, it only returns "ec:cros", for Chrome OS ECs. Other types
|
|
of EC (or none) don't return any strings, since no tests depend on
|
|
those.
|
|
"""
|
|
cmd = 'mosys ec info'
|
|
# The output should look like these, so that the last field should
|
|
# match our EC version scheme:
|
|
#
|
|
# stm | stm32f100 | snow_v1.3.139-375eb9f
|
|
# ti | Unknown-10de | peppy_v1.5.114-5d52788
|
|
#
|
|
# Non-Chrome OS ECs will look like these:
|
|
#
|
|
# ENE | KB932 | 00BE107A00
|
|
# ite | it8518 | 3.08
|
|
#
|
|
# And some systems don't have ECs at all (Lumpy, for example).
|
|
regexp = r'^.*\|\s*(\S+_v\d+\.\d+\.\d+-[0-9a-f]+)\s*$'
|
|
|
|
ecinfo = self.run(command=cmd, ignore_status=True)
|
|
if ecinfo.exit_status == 0:
|
|
res = re.search(regexp, ecinfo.stdout)
|
|
if res:
|
|
logging.info("EC version is %s", res.groups()[0])
|
|
return 'ec:cros'
|
|
logging.info("%s got: %s", cmd, ecinfo.stdout)
|
|
# Has an EC, but it's not a Chrome OS EC
|
|
return None
|
|
logging.info("%s exited with status %d", cmd, ecinfo.exit_status)
|
|
# No EC present
|
|
return None
|
|
|
|
|
|
@label_decorator('accels')
|
|
def get_accels(self):
|
|
"""
|
|
Determine the type of accelerometers on this host.
|
|
|
|
@returns a string representing this host's accelerometer type.
|
|
At present, it only returns "accel:cros-ec", for accelerometers
|
|
attached to a Chrome OS EC, or none, if no accelerometers.
|
|
"""
|
|
# Check to make sure we have ectool
|
|
rv = self.run('which ectool', ignore_status=True)
|
|
if rv.exit_status:
|
|
logging.info("No ectool cmd found, assuming no EC accelerometers")
|
|
return None
|
|
|
|
# Check that the EC supports the motionsense command
|
|
rv = self.run('ectool motionsense', ignore_status=True)
|
|
if rv.exit_status:
|
|
logging.info("EC does not support motionsense command "
|
|
"assuming no EC accelerometers")
|
|
return None
|
|
|
|
# Check that EC motion sensors are active
|
|
active = self.run('ectool motionsense active').stdout.split('\n')
|
|
if active[0] == "0":
|
|
logging.info("Motion sense inactive, assuming no EC accelerometers")
|
|
return None
|
|
|
|
logging.info("EC accelerometers found")
|
|
return 'accel:cros-ec'
|
|
|
|
|
|
@label_decorator('chameleon')
|
|
def has_chameleon(self):
|
|
"""Determine if a Chameleon connected to this host.
|
|
|
|
@returns a list containing two strings ('chameleon' and
|
|
'chameleon:' + label, e.g. 'chameleon:hdmi') if this host
|
|
has a Chameleon or None if it has not.
|
|
"""
|
|
if self._chameleon_host:
|
|
return ['chameleon', 'chameleon:' + self.chameleon.get_label()]
|
|
else:
|
|
return None
|
|
|
|
|
|
@label_decorator('audio_loopback_dongle')
|
|
def has_loopback_dongle(self):
|
|
"""Determine if an audio loopback dongle is plugged to this host.
|
|
|
|
@returns 'audio_loopback_dongle' when there is an audio loopback dongle
|
|
plugged to this host.
|
|
None when there is no audio loopback dongle
|
|
plugged to this host.
|
|
"""
|
|
nodes_info = self.run(command=cras_utils.get_cras_nodes_cmd(),
|
|
ignore_status=True).stdout
|
|
if (cras_utils.node_type_is_plugged('HEADPHONE', nodes_info) and
|
|
cras_utils.node_type_is_plugged('MIC', nodes_info)):
|
|
return 'audio_loopback_dongle'
|
|
else:
|
|
return None
|
|
|
|
|
|
@label_decorator('power_supply')
|
|
def get_power_supply(self):
|
|
"""
|
|
Determine what type of power supply the host has
|
|
|
|
@returns a string representing this host's power supply.
|
|
'power:battery' when the device has a battery intended for
|
|
extended use
|
|
'power:AC_primary' when the device has a battery not intended
|
|
for extended use (for moving the machine, etc)
|
|
'power:AC_only' when the device has no battery at all.
|
|
"""
|
|
psu = self.run(command='mosys psu type', ignore_status=True)
|
|
if psu.exit_status:
|
|
# The psu command for mosys is not included for all platforms. The
|
|
# assumption is that the device will have a battery if the command
|
|
# is not found.
|
|
return 'power:battery'
|
|
|
|
psu_str = psu.stdout.strip()
|
|
if psu_str == 'unknown':
|
|
return None
|
|
|
|
return 'power:%s' % psu_str
|
|
|
|
|
|
@label_decorator('storage')
|
|
def get_storage(self):
|
|
"""
|
|
Determine the type of boot device for this host.
|
|
|
|
Determine if the internal device is SCSI or dw_mmc device.
|
|
Then check that it is SSD or HDD or eMMC or something else.
|
|
|
|
@returns a string representing this host's internal device type.
|
|
'storage:ssd' when internal device is solid state drive
|
|
'storage:hdd' when internal device is hard disk drive
|
|
'storage:mmc' when internal device is mmc drive
|
|
None When internal device is something else or
|
|
when we are unable to determine the type
|
|
"""
|
|
# The output should be /dev/mmcblk* for SD/eMMC or /dev/sd* for scsi
|
|
rootdev_cmd = ' '.join(['. /usr/sbin/write_gpt.sh;',
|
|
'. /usr/share/misc/chromeos-common.sh;',
|
|
'load_base_vars;',
|
|
'get_fixed_dst_drive'])
|
|
rootdev = self.run(command=rootdev_cmd, ignore_status=True)
|
|
if rootdev.exit_status:
|
|
logging.info("Fail to run %s", rootdev_cmd)
|
|
return None
|
|
rootdev_str = rootdev.stdout.strip()
|
|
|
|
if not rootdev_str:
|
|
return None
|
|
|
|
rootdev_base = os.path.basename(rootdev_str)
|
|
|
|
mmc_pattern = '/dev/mmcblk[0-9]'
|
|
if re.match(mmc_pattern, rootdev_str):
|
|
# Use type to determine if the internal device is eMMC or somthing
|
|
# else. We can assume that MMC is always an internal device.
|
|
type_cmd = 'cat /sys/block/%s/device/type' % rootdev_base
|
|
type = self.run(command=type_cmd, ignore_status=True)
|
|
if type.exit_status:
|
|
logging.info("Fail to run %s", type_cmd)
|
|
return None
|
|
type_str = type.stdout.strip()
|
|
|
|
if type_str == 'MMC':
|
|
return 'storage:mmc'
|
|
|
|
scsi_pattern = '/dev/sd[a-z]+'
|
|
if re.match(scsi_pattern, rootdev.stdout):
|
|
# Read symlink for /sys/block/sd* to determine if the internal
|
|
# device is connected via ata or usb.
|
|
link_cmd = 'readlink /sys/block/%s' % rootdev_base
|
|
link = self.run(command=link_cmd, ignore_status=True)
|
|
if link.exit_status:
|
|
logging.info("Fail to run %s", link_cmd)
|
|
return None
|
|
link_str = link.stdout.strip()
|
|
if 'usb' in link_str:
|
|
return None
|
|
|
|
# Read rotation to determine if the internal device is ssd or hdd.
|
|
rotate_cmd = str('cat /sys/block/%s/queue/rotational'
|
|
% rootdev_base)
|
|
rotate = self.run(command=rotate_cmd, ignore_status=True)
|
|
if rotate.exit_status:
|
|
logging.info("Fail to run %s", rotate_cmd)
|
|
return None
|
|
rotate_str = rotate.stdout.strip()
|
|
|
|
rotate_dict = {'0':'storage:ssd', '1':'storage:hdd'}
|
|
return rotate_dict.get(rotate_str)
|
|
|
|
# All other internal device / error case will always fall here
|
|
return None
|
|
|
|
|
|
@label_decorator('servo')
|
|
def get_servo(self):
|
|
"""Determine if the host has a servo attached.
|
|
|
|
If the host has a working servo attached, it should have a servo label.
|
|
|
|
@return: string 'servo' if the host has servo attached. Otherwise,
|
|
returns None.
|
|
"""
|
|
return 'servo' if self._servo_host else None
|
|
|
|
|
|
@label_decorator('video_labels')
|
|
def get_video_labels(self):
|
|
"""Run /usr/local/bin/avtest_label_detect to get a list of video labels.
|
|
|
|
Sample output of avtest_label_detect:
|
|
Detected label: hw_video_acc_vp8
|
|
Detected label: webcam
|
|
|
|
@return: A list of labels detected by tool avtest_label_detect.
|
|
"""
|
|
try:
|
|
result = self.run('/usr/local/bin/avtest_label_detect').stdout
|
|
return re.findall('^Detected label: (\w+)$', result, re.M)
|
|
except error.AutoservRunError:
|
|
# The tool is not installed.
|
|
return []
|
|
|
|
|
|
@label_decorator('video_glitch_detection')
|
|
def is_video_glitch_detection_supported(self):
|
|
""" Determine if a board under test is supported for video glitch
|
|
detection tests.
|
|
|
|
@return: 'video_glitch_detection' if board is supported, None otherwise.
|
|
"""
|
|
board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
|
|
|
|
if board in video_test_constants.SUPPORTED_BOARDS:
|
|
return 'video_glitch_detection'
|
|
|
|
return None
|
|
|
|
|
|
@label_decorator('touch_labels')
|
|
def get_touch(self):
|
|
"""
|
|
Determine whether board under test has a touchpad or touchscreen.
|
|
|
|
@return: A list of some combination of 'touchscreen' and 'touchpad',
|
|
depending on what is present on the device.
|
|
|
|
"""
|
|
labels = []
|
|
looking_for = ['touchpad', 'touchscreen']
|
|
player = input_playback.InputPlayback()
|
|
input_events = self.run('ls /dev/input/event*').stdout.strip().split()
|
|
filename = '/tmp/touch_labels'
|
|
for event in input_events:
|
|
self.run('evtest %s > %s' % (event, filename), timeout=1,
|
|
ignore_timeout=True)
|
|
properties = self.run('cat %s' % filename).stdout
|
|
input_type = player._determine_input_type(properties)
|
|
if input_type in looking_for:
|
|
labels.append(input_type)
|
|
looking_for.remove(input_type)
|
|
if len(looking_for) == 0:
|
|
break
|
|
self.run('rm %s' % filename)
|
|
|
|
return labels
|
|
|
|
|
|
@label_decorator('internal_display')
|
|
def has_internal_display(self):
|
|
"""Determine if the device under test is equipped with an internal
|
|
display.
|
|
|
|
@return: 'internal_display' if one is present; None otherwise.
|
|
"""
|
|
from autotest_lib.client.cros.graphics import graphics_utils
|
|
from autotest_lib.client.common_lib import utils as common_utils
|
|
|
|
def __system_output(cmd):
|
|
return self.run(cmd).stdout
|
|
|
|
def __read_file(remote_path):
|
|
return self.run('cat %s' % remote_path).stdout
|
|
|
|
# Hijack the necessary client functions so that we can take advantage
|
|
# of the client lib here.
|
|
# FIXME: find a less hacky way than this
|
|
original_system_output = utils.system_output
|
|
original_read_file = common_utils.read_file
|
|
utils.system_output = __system_output
|
|
common_utils.read_file = __read_file
|
|
try:
|
|
return ('internal_display' if graphics_utils.has_internal_display()
|
|
else None)
|
|
finally:
|
|
utils.system_output = original_system_output
|
|
common_utils.read_file = original_read_file
|
|
|
|
|
|
@label_decorator('lucidsleep')
|
|
def has_lucid_sleep_support(self):
|
|
"""Determine if the device under test has support for lucid sleep.
|
|
|
|
@return 'lucidsleep' if this board supports lucid sleep; None otherwise
|
|
"""
|
|
board = self.get_board().replace(ds_constants.BOARD_PREFIX, '')
|
|
return 'lucidsleep' if board in LUCID_SLEEP_BOARDS else None
|
|
|
|
|
|
def is_boot_from_usb(self):
|
|
"""Check if DUT is boot from USB.
|
|
|
|
@return: True if DUT is boot from usb.
|
|
"""
|
|
device = self.run('rootdev -s -d').stdout.strip()
|
|
removable = int(self.run('cat /sys/block/%s/removable' %
|
|
os.path.basename(device)).stdout.strip())
|
|
return removable == 1
|
|
|
|
|
|
def read_from_meminfo(self, key):
|
|
"""Return the memory info from /proc/meminfo
|
|
|
|
@param key: meminfo requested
|
|
|
|
@return the memory value as a string
|
|
|
|
"""
|
|
meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip()
|
|
logging.debug('%s', meminfo)
|
|
return int(re.search(r'\d+', meminfo).group(0))
|
|
|
|
|
|
def get_board_type(self):
|
|
"""
|
|
Get the DUT's device type from /etc/lsb-release.
|
|
DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more.
|
|
|
|
@return value of DEVICETYPE param from lsb-release.
|
|
"""
|
|
device_type = self.run('grep DEVICETYPE /etc/lsb-release',
|
|
ignore_status=True).stdout
|
|
if device_type:
|
|
return device_type.split('=')[-1].strip()
|
|
return ''
|
|
|
|
|
|
def get_os_type(self):
|
|
return 'cros'
|
|
|
|
|
|
def enable_adb_testing(self):
|
|
"""Mark this host as an adb tester."""
|
|
self.run('touch %s' % constants.ANDROID_TESTER_FILEFLAG)
|