blob: c04576426fb771d82cccd39ace44c669bdc9e9a6 [file] [log] [blame]
# Lint as: python2, python3
# 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.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import os
import re
import sys
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 hosts
from autotest_lib.client.common_lib import lsbrelease_utils
from autotest_lib.client.common_lib import utils as common_utils
from autotest_lib.client.common_lib.cros import cros_config
from autotest_lib.client.common_lib.cros import dev_server
from autotest_lib.client.common_lib.cros import retry
from autotest_lib.client.cros import constants as client_constants
from autotest_lib.client.cros import cros_ui
from autotest_lib.server import afe_utils
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.device_health_profile import device_health_profile
from autotest_lib.server.cros.device_health_profile import profile_constants
from autotest_lib.server.cros.servo import pdtester
from autotest_lib.server.hosts import abstract_ssh
from autotest_lib.server.hosts import base_label
from autotest_lib.server.hosts import chameleon_host
from autotest_lib.server.hosts import cros_constants
from autotest_lib.server.hosts import cros_label
from autotest_lib.server.hosts import cros_repair
from autotest_lib.server.hosts import pdtester_host
from autotest_lib.server.hosts import servo_host
from autotest_lib.server.hosts import servo_constants
from autotest_lib.site_utils.rpm_control_system import rpm_client
from autotest_lib.site_utils.admin_audit import constants as audit_const
from autotest_lib.site_utils.admin_audit import verifiers as audit_verify
from six.moves import zip
# In case cros_host is being ran via SSP on an older Moblab version with an
# older chromite version.
try:
from chromite.lib import metrics
except ImportError:
metrics = utils.metrics_mock
CONFIG = global_config.global_config
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
_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)
USE_FSFREEZE = CONFIG.get_config_value(
'CROS', 'enable_fs_freeze', type=bool, default=False)
# 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.
# _CHANGE_SERVO_ROLE_TIMEOUT: Time to allow DUT regain network connection
# since changing servo role will reset USB state
# and causes temporary ethernet drop.
_USB_POWER_TIMEOUT = 5
_POWER_CYCLE_TIMEOUT = 10
_CHANGE_SERVO_ROLE_TIMEOUT = 180
_RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)'
'-host(\d+)')
# 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: Used in power_off/on/cycle() methods, default for all
# DUTs except those with servo_v4 CCD.
# POWER_CONTROL_CCD: Used in power_off/on/cycle() methods, default for all
# DUTs with servo_v4 CCD.
# 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_CCD = 'CCD'
POWER_CONTROL_SERVO = 'servoj10'
POWER_CONTROL_MANUAL = 'manual'
POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
POWER_CONTROL_CCD,
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)
# Regular expression for extracting EC version string
_EC_REGEX = '(%s_\w*[-\.]\w*[-\.]\w*[-\.]\w*)'
# Regular expression for extracting BIOS version string
_BIOS_REGEX = '(%s\.\w*\.\w*\.\w*)'
# Command to update firmware located on DUT
_FW_UPDATE_CMD = 'chromeos-firmwareupdate --mode=recovery %s'
@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 && '
'! grep -q moblab /etc/lsb-release && '
'! grep -q labstation /etc/lsb-release',
ignore_status=True, timeout=timeout)
if result.exit_status == 0:
lsb_release_content = host.run(
'grep CHROMEOS_RELEASE_BOARD /etc/lsb-release',
timeout=timeout).stdout
return not (
lsbrelease_utils.is_jetstream(
lsb_release_content=lsb_release_content) or
lsbrelease_utils.is_gce_board(
lsb_release_content=lsb_release_content))
except (error.AutoservRunError, error.AutoservSSHTimeout):
return False
return False
@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.
"""
chameleon_args = {key: args_dict[key]
for key in ('chameleon_host', 'chameleon_port')
if key in args_dict}
if 'chameleon_ssh_port' in args_dict:
chameleon_args['port'] = int(args_dict['chameleon_ssh_port'])
return chameleon_args
@staticmethod
def get_btpeer_arguments(args_dict):
"""Extract btpeer options from `args_dict` and return the result.
This is used to parse details of Bluetooth peer.
Recommended usage:
~~~~~~~~
args_dict = utils.args_to_dict(args)
btpeer_args = hosts.CrosHost.get_btpeer_arguments(args_dict)
host = hosts.create_host(machine, btpeer_args=btpeer_args)
~~~~~~~~
@param args_dict: Dictionary from which to extract the btpeer
arguments.
"""
if 'btpeer_host_list' in args_dict:
result = []
for btpeer in args_dict['btpeer_host_list'].split(','):
result.append({key: value for key,value in
zip(('btpeer_host','btpeer_port'),
btpeer.split(':'))})
return result
else:
return {key: args_dict[key]
for key in ('btpeer_host', 'btpeer_port', 'btpeer_ssh_port')
if key in args_dict}
@staticmethod
def get_pdtester_arguments(args_dict):
"""Extract chameleon options from `args_dict` and return the result.
Recommended usage:
~~~~~~~~
args_dict = utils.args_to_dict(args)
pdtester_args = hosts.CrosHost.get_pdtester_arguments(args_dict)
host = hosts.create_host(machine, pdtester_args=pdtester_args)
~~~~~~~~
@param args_dict Dictionary from which to extract the pdtester
arguments.
"""
return {key: args_dict[key]
for key in ('pdtester_host', 'pdtester_port')
if key in args_dict}
@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.
"""
servo_attrs = (servo_constants.SERVO_HOST_ATTR,
servo_constants.SERVO_PORT_ATTR,
servo_constants.SERVO_SERIAL_ATTR,
servo_constants.SERVO_BOARD_ATTR,
servo_constants.SERVO_MODEL_ATTR)
servo_args = {key: args_dict[key]
for key in servo_attrs
if key in args_dict}
return (
None
if servo_constants.SERVO_HOST_ATTR in servo_args
and not servo_args[servo_constants.SERVO_HOST_ATTR]
else servo_args)
def _initialize(self, hostname, chameleon_args=None, servo_args=None,
pdtester_args=None, try_lab_servo=False,
try_servo_repair=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: When true, indicates that an attempt should
be made to create a ServoHost for a DUT in
the test lab, even if not required by
`servo_args`. See servo_host for details.
@param try_servo_repair: If a servo host is created, check it
with `repair()` rather than `verify()`.
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._repair_strategy = cros_repair.create_cros_repair_strategy()
# hold special dut_state for repair process
self._device_repair_state = None
self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS)
# 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
_servo_host, servo_state = servo_host.create_servo_host(
dut=self,
servo_args=servo_args,
try_lab_servo=try_lab_servo,
try_servo_repair=try_servo_repair,
dut_host_info=self.host_info_store.get())
self.set_servo_host(_servo_host, servo_state)
self.health_profile = None
self._default_power_method = None
# TODO(waihong): Do the simplication on Chameleon too.
self._chameleon_host = chameleon_host.create_chameleon_host(
dut=self.hostname,
chameleon_args=chameleon_args)
if self._chameleon_host:
self.chameleon = self._chameleon_host.create_chameleon_board()
else:
self.chameleon = None
# Bluetooth peers will be populated by the test if needed
self._btpeer_host_list = []
self.btpeer_list = []
self.btpeer = None
# Add pdtester host if pdtester args were added on command line
self._pdtester_host = pdtester_host.create_pdtester_host(
pdtester_args, self._servo_host)
if self._pdtester_host:
self.pdtester_servo = self._pdtester_host.get_servo()
logging.info('pdtester_servo: %r', self.pdtester_servo)
# Create the pdtester object used to access the ec uart
self.pdtester = pdtester.PDTester(self.pdtester_servo,
self._pdtester_host.get_servod_server_proxy())
else:
self.pdtester = None
def initialize_btpeer(self, btpeer_args=[]):
""" Initialize the Bluetooth peers
Initialize Bluetooth peer devices given in the arguments. Bluetooth peer
is chameleon host on Raspberry Pi.
@param btpeer_args: A dictionary that contains args for creating
a ChameleonHost. See chameleon_host for details.
"""
logging.debug('Attempting to initialize bluetooth peers if available')
try:
if type(btpeer_args) is list:
btpeer_args_list = btpeer_args
else:
btpeer_args_list = [btpeer_args]
self._btpeer_host_list = chameleon_host.create_btpeer_host(
dut=self.hostname, btpeer_args_list=btpeer_args_list)
logging.debug('Bluetooth peer hosts are %s',
self._btpeer_host_list)
self.btpeer_list = [_host.create_chameleon_board() for _host in
self._btpeer_host_list if _host is not None]
if len(self.btpeer_list) > 0:
self.btpeer = self.btpeer_list[0]
logging.debug('After initialize_btpeer btpeer_list %s '
'btpeer_host_list is %s and btpeer is %s',
self.btpeer_list, self._btpeer_host_list,
self.btpeer)
except Exception as e:
logging.error('Exception %s in initialize_btpeer', str(e))
def get_cros_repair_image_name(self):
"""Get latest stable cros image name from AFE.
Use the board name from the info store. Should that fail, try to
retrieve the board name from the host's installed image itself.
@returns: current stable cros image name for this host.
"""
info = self.host_info_store.get()
if not info.board:
logging.warn('No board label value found. Trying to infer '
'from the host itself.')
try:
info.labels.append(self.get_board())
except (error.AutoservRunError, error.AutoservSSHTimeout) as e:
logging.error('Also failed to get the board name from the DUT '
'itself. %s.', str(e))
raise error.AutoservError('Cannot determine board of the DUT'
' while getting repair image name.')
return afe_utils.get_stable_cros_image_name_v2(info)
def host_version_prefix(self, image):
"""Return version label prefix.
In case the CrOS provisioning version is something other than the
standard CrOS version e.g. CrOS TH version, this function will
find the prefix from provision.py.
@param image: The image name to find its version prefix.
@returns: A prefix string for the image type.
"""
return provision.get_version_label_prefix(image)
def stage_build_to_usb(self, build):
"""Stage the current ChromeOS image on the USB stick connected to the
servo.
@param build: The build to download and send to USB.
"""
if not self.servo:
raise error.TestError('Host %s does not have servo.' %
self.hostname)
_, update_url = self.stage_image_for_servo(build)
try:
self.servo.image_to_servo_usb(update_url)
finally:
# servo.image_to_servo_usb turned the DUT off, so turn it back on
logging.debug('Turn DUT power back on.')
self.servo.get_power_state_controller().power_on()
logging.debug('ChromeOS image %s is staged on the USB stick.',
build)
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.
"""
info = self.host_info_store.get()
job_repo_url = info.attributes.get(ds_constants.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('.', '_'),
}
monarch_fields = {
'board': board,
'build_type': build_type,
'branch': branch,
'dev_server': devserver,
}
metrics.Counter(
'chromeos/autotest/provision/verify_url'
).increment(fields=monarch_fields)
metrics.SecondsDistribution(
'chromeos/autotest/provision/verify_url_duration'
).add(stage_time, fields=monarch_fields)
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.
@raise: error.AutoservError if fail to locate the build to test with, or
fail to stage server-side package.
"""
# If enable_drone_in_restricted_subnet is False, do not set hostname
# in devserver.resolve call, so a devserver in non-restricted subnet
# is picked to stage autotest server package for drone to download.
hostname = self.hostname
if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
hostname = None
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, hostname)
else:
info = self.host_info_store.get()
job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '')
if job_repo_url:
devserver_url, image_name = (
tools.get_devserver_build_from_package_url(job_repo_url))
# If enable_drone_in_restricted_subnet is True, use the
# existing devserver. Otherwise, resolve a new one in
# non-restricted subnet.
if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET:
ds = dev_server.ImageServer(devserver_url)
else:
ds = dev_server.ImageServer.resolve(image_name)
elif info.build is not None:
ds = dev_server.ImageServer.resolve(info.build, hostname)
image_name = info.build
else:
raise error.AutoservError(
'Failed to stage server-side package. The host has '
'no job_repo_url attribute or cros-version label.')
# 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:
raise error.AutoservError(
'Build %s is older than %s. Server side packaging is '
'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP))
ds.stage_artifacts(image_name, ['autotest_server_package'])
return '%s/static/%s/%s' % (ds.url(), image_name,
'autotest_server_package.tar.bz2')
def stage_image_for_servo(self, image_name=None, artifact='test_image'):
"""Stage a build on a devserver and return the update_url.
@param image_name: a name like lumpy-release/R27-3837.0.0
@param artifact: a string like 'test_image'. Requests
appropriate image to be staged.
@returns a tuple of (image_name, URL) like
(lumpy-release/R27-3837.0.0,
http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0)
"""
if not image_name:
image_name = self.get_cros_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, [artifact])
if artifact == 'test_image':
return image_name, devserver.get_test_image_url(image_name)
elif artifact == 'recovery_image':
return image_name, devserver.get_recovery_image_url(image_name)
else:
raise error.AutoservError("Bad artifact!")
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 prepare_for_update(self):
"""Prepares the DUT for an update.
Subclasses may override this to perform any special actions
required before updating.
"""
pass
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.
"""
info = self.host_info_store.get()
info.clear_version_labels(provision.FW_RW_VERSION_PREFIX)
if not rw_only:
info.clear_version_labels(provision.FW_RO_VERSION_PREFIX)
self.host_info_store.commit(info)
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.
"""
info = self.host_info_store.get()
info.set_version_label(provision.FW_RW_VERSION_PREFIX, build)
if not rw_only:
info.set_version_label(provision.FW_RO_VERSION_PREFIX, build)
self.host_info_store.commit(info)
def get_latest_release_version(self, platform, ref_board=None):
"""Search for the latest package release version from the image archive,
and return it.
@param platform: platform name, a.k.a. board or model
@param ref_board: reference board name, a.k.a. baseboard, parent
@return 'firmware-{platform}-{branch}-firmwarebranch/{release-version}/'
'{platform}'
or None if LATEST release file does not exist.
"""
platforms = [ platform ]
# Search the image path in reference board archive as well.
# For example, bob has its binary image under its reference board (gru)
# image archive.
if ref_board:
platforms.append(ref_board)
for board in platforms:
# Read 'LATEST-1.0.0' file
branch_dir = provision.FW_BRANCH_GLOB % board
latest_file = os.path.join(provision.CROS_IMAGE_ARCHIVE, branch_dir,
'LATEST-1.0.0')
try:
# The result could be one or more.
result = utils.system_output('gsutil ls -d ' + latest_file)
candidates = re.findall('gs://.*', result)
# Found the directory candidates. No need to check the other
# board name cadidates. Let's break the loop.
break
except error.CmdError:
# It doesn't exist. Let's move on to the next item.
pass
else:
logging.error('No LATEST release info is available.')
return None
for cand_dir in candidates:
result = utils.system_output('gsutil cat ' + cand_dir)
release_path = cand_dir.replace('LATEST-1.0.0', result)
release_path = os.path.join(release_path, platform)
try:
# Check if release_path does exist.
release = utils.system_output('gsutil ls -d ' + release_path)
# Now 'release' has a full directory path: e.g.
# gs://chromeos-image-archive/firmware-octopus-11297.B-
# firmwarebranch/RNone-1.0.0-b4395530/octopus/
# Remove "gs://chromeos-image-archive".
release = release.replace(provision.CROS_IMAGE_ARCHIVE, '')
# Remove CROS_IMAGE_ARCHIVE and any surrounding '/'s.
return release.strip('/')
except error.CmdError:
# The directory might not exist. Let's try next candidate.
pass
else:
raise error.AutoservError('Cannot find the latest firmware')
@staticmethod
def get_version_from_image(image, version_regex):
"""Get version string from binary image using regular expression.
@param image: Binary image to search
@param version_regex: Regular expression to search for
@return Version string
@raises TestFail if no version string is found in image
"""
with open(image, 'rb') as f:
image_data = f.read()
match = re.findall(version_regex,
image_data.decode('ISO-8859-1', errors='ignore'))
if match:
return match[0]
else:
raise error.TestFail('Failed to read version from %s.' % image)
def firmware_install(self, build, rw_only=False, dest=None,
local_tarball=None, verify_version=False,
try_scp=False, install_ec=True, install_bios=True,
board_as=None):
"""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.
@param dest: Directory to store the firmware in.
@param local_tarball: Path to local firmware image for installing
without devserver.
@param verify_version: True to verify EC and BIOS versions after
programming firmware, default is False.
@param try_scp: False to always program using servo, true to try copying
the firmware and programming from the DUT.
@param install_ec: True to install EC FW, and False to skip it.
@param install_bios: True to install BIOS, and False to skip it.
@param board_as: A board name to force to use.
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)
# Get the DUT board name from AFE.
info = self.host_info_store.get()
board = info.board
model = info.model
if board is None or board == '':
board = self.servo.get_board()
# if board_as argument is passed, then use it instead of the original
# board name.
if board_as:
board = board_as
if model is None or model == '':
try:
model = self.get_platform()
except Exception as e:
logging.warn('Dut is unresponsive: %s', str(e))
# If local firmware path not provided fetch it from the dev server
tmpd = None
if not local_tarball:
logging.info('Will install firmware from build %s.', build)
try:
ds = dev_server.ImageServer.resolve(build, self.hostname)
ds.stage_artifacts(build, ['firmware'])
if not dest:
tmpd = autotemp.tempdir(unique_id='fwimage')
dest = tmpd.name
# Download firmware image
fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
local_tarball = os.path.join(dest, os.path.basename(fwurl))
ds.download_file(fwurl, local_tarball)
except Exception as e:
raise error.TestError('Failed to download firmware package: %s'
% str(e))
ec_image = None
if install_ec:
# Extract EC image from tarball
logging.info('Extracting EC image.')
ec_image = self.servo.extract_ec_image(board, model, local_tarball)
logging.info('Extracted: %s', ec_image)
bios_image = None
if install_bios:
# Extract BIOS image from tarball
logging.info('Extracting BIOS image.')
bios_image = self.servo.extract_bios_image(board, model,
local_tarball)
logging.info('Extracted: %s', bios_image)
if not bios_image and not ec_image:
raise error.TestError('No firmware installation was processed.')
# Clear firmware version labels
self._clear_fw_version_labels(rw_only)
# Install firmware from local tarball
try:
# Check if copying to DUT is enabled and DUT is available
if try_scp and self.is_up():
# DUT is available, make temp firmware directory to store images
logging.info('Making temp folder.')
dest_folder = '/tmp/firmware'
self.run('mkdir -p ' + dest_folder)
fw_cmd = self._FW_UPDATE_CMD % ('--wp=1' if rw_only else '')
if bios_image:
# Send BIOS firmware image to DUT
logging.info('Sending BIOS firmware.')
dest_bios_path = os.path.join(dest_folder,
os.path.basename(bios_image))
self.send_file(bios_image, dest_bios_path)
# Initialize firmware update command for BIOS image
fw_cmd += ' -i %s' % dest_bios_path
# Send EC firmware image to DUT when EC image was found
if ec_image:
logging.info('Sending EC firmware.')
dest_ec_path = os.path.join(dest_folder,
os.path.basename(ec_image))
self.send_file(ec_image, dest_ec_path)
# Add EC image to firmware update command
fw_cmd += ' -e %s' % dest_ec_path
# Make sure command is allowed to finish even if ssh fails.
fw_cmd = "trap '' SIGHUP; %s" % fw_cmd
# Update firmware on DUT
logging.info('Updating firmware.')
try:
self.run(fw_cmd, options="-o LogLevel=verbose")
except error.AutoservRunError as e:
if e.result_obj.exit_status != 255:
raise
elif ec_image:
logging.warn("DUT network dropped during update"
" (often caused by EC resetting USB)")
else:
logging.error("DUT network dropped during update"
" (unexpected, since no EC image)")
raise
else:
# Host is not available, program firmware using servo
if ec_image:
self.servo.program_ec(ec_image, rw_only)
if bios_image:
self.servo.program_bios(bios_image, rw_only)
if utils.host_is_in_lab_zone(self.hostname):
self._add_fw_version_label(build, rw_only)
# Reboot and wait for DUT after installing firmware
logging.info('Rebooting DUT.')
self.servo.get_power_state_controller().reset()
time.sleep(self.servo.BOOT_DELAY)
self.test_wait_for_boot()
# When enabled verify EC and BIOS firmware version after programming
if verify_version:
# Check programmed EC firmware when EC image was found
if ec_image:
logging.info('Checking EC firmware version.')
dest_ec_version = self.get_ec_version()
ec_version_prefix = dest_ec_version.split('_', 1)[0]
ec_regex = self._EC_REGEX % ec_version_prefix
image_ec_version = self.get_version_from_image(ec_image,
ec_regex)
if dest_ec_version != image_ec_version:
raise error.TestFail(
'Failed to update EC firmware, version %s '
'(expected %s)' % (dest_ec_version,
image_ec_version))
if bios_image:
# Check programmed BIOS firmware against expected version
logging.info('Checking BIOS firmware version.')
dest_bios_version = self.get_firmware_version()
bios_version_prefix = dest_bios_version.split('.', 1)[0]
bios_regex = self._BIOS_REGEX % bios_version_prefix
image_bios_version = self.get_version_from_image(bios_image,
bios_regex)
if dest_bios_version != image_bios_version:
raise error.TestFail(
'Failed to update BIOS, version %s '
'(expected %s)' % (dest_bios_version,
image_bios_version))
finally:
if tmpd:
tmpd.clean()
def servo_install(self,
image_url=None,
usb_boot_timeout=USB_BOOT_TIMEOUT,
install_timeout=INSTALL_TIMEOUT,
is_repair=False):
"""
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.
@param is_repair: Indicates if the method is called from a repair task.
@raises AutoservError if the image fails to boot.
"""
if image_url:
logging.info('Downloading image to USB, then booting from it.'
' Usb boot timeout = %s', usb_boot_timeout)
else:
logging.info('Booting from USB directly. Usb boot timeout = %s',
usb_boot_timeout)
metrics_field = {'download': bool(image_url)}
metrics.Counter(
'chromeos/autotest/provision/servo_install/download_image'
).increment(fields=metrics_field)
with metrics.SecondsTimer(
'chromeos/autotest/provision/servo_install/boot_duration'):
need_snk = self.require_snk_mode_in_recovery()
self.servo.install_recovery_image(image_url, snk_mode=need_snk)
if not self.wait_up(timeout=usb_boot_timeout):
if need_snk:
# Attempt to restore servo_v4 role to 'src' mode.
self.servo.set_servo_v4_role('src')
raise hosts.AutoservRepairError(
'DUT failed to boot from USB after %d seconds' %
usb_boot_timeout, 'failed_to_boot_pre_install')
# 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.')
with metrics.SecondsTimer(
'chromeos/autotest/provision/servo_install/install_duration'):
logging.info('Installing image through chromeos-install.')
try:
self.run('chromeos-install --yes',timeout=install_timeout)
self.halt()
except Exception as e:
storage_errors = [
'No space left on device',
'I/O error when trying to write primary GPT',
'Input/output error while writing out',
'cannot read GPT header',
'can not determine destination device',
'wrong fs type',
'bad superblock on',
]
has_error = [msg for msg in storage_errors if(msg in str(e))]
if has_error:
info = self.host_info_store.get()
info.set_version_label(
audit_const.DUT_STORAGE_STATE_PREFIX,
audit_const.HW_STATE_NEED_REPLACEMENT)
self.host_info_store.commit(info)
self.set_device_repair_state(
cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT)
logging.debug(
'Fail install image from USB; Storage error; %s', e)
raise error.AutoservError(
'Failed to install image from USB due to a suspect '
'disk failure, DUT storage state changed to '
'need_replacement, please check debug log '
'for details.')
else:
if is_repair:
# DUT will be marked for replacement if storage is bad.
audit_verify.VerifyDutStorage(self).verify()
logging.debug('Fail install image from USB; %s', e)
raise error.AutoservError(
'Failed to install image from USB due to unexpected '
'error, please check debug log for details.')
finally:
# We need reset the DUT no matter re-install success or not,
# as we don't want leave the DUT in boot from usb state.
logging.info('Power cycling DUT through servo.')
self.servo.get_power_state_controller().power_off()
self.servo.switch_usbkey('off')
if need_snk:
# Attempt to restore servo_v4 role to 'src' mode.
self.servo.set_servo_v4_role('src')
# 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 hosts.AutoservRepairError('DUT failed to reboot installed '
'test image after %d seconds' %
self.BOOT_TIMEOUT,
'failed_to_boot_post_install')
def set_servo_host(self, host, servo_state=None):
"""Set our servo host member, and associated servo.
@param host Our new `ServoHost`.
"""
self._servo_host = host
self.servo_pwr_supported = None
if self._servo_host is not None:
self.servo = self._servo_host.get_servo()
servo_state = self._servo_host.get_servo_state()
self._set_smart_usbhub_label(self._servo_host.smart_usbhub)
try:
self.servo_pwr_supported = self.servo.has_control('power_state')
except Exception as e:
logging.debug(
"Could not get servo power state due to {}".format(e))
else:
self.servo = None
self.servo_pwr_supported = False
self.set_servo_type()
self.set_servo_state(servo_state)
self._set_servo_topology()
def repair_servo(self):
"""
Confirm that servo is initialized and verified.
If the servo object is missing, attempt to repair the servo
host. Repair failures are passed back to the caller.
@raise AutoservError: If there is no servo host for this CrOS
host.
"""
if self.servo:
return
if not self._servo_host:
raise error.AutoservError('No servo host for %s.' %
self.hostname)
try:
self._servo_host.repair()
except:
raise
finally:
self.set_servo_host(self._servo_host)
def set_servo_type(self):
"""Set servo info labels to dut host_info"""
if not self.servo:
logging.debug('Servo is not initialized to get servo_type.')
return
servo_type = self.servo.get_servo_type()
if not servo_type:
logging.debug('Cannot collect servo_type from servo'
' by `dut-control servo_type`! Please file a bug'
' and inform infra team as we are not expected '
' to reach this point.')
return
host_info = self.host_info_store.get()
prefix = servo_constants.SERVO_TYPE_LABEL_PREFIX
old_type = host_info.get_label_value(prefix)
if old_type == servo_type:
# do not need update
return
host_info.set_version_label(prefix, servo_type)
self.host_info_store.commit(host_info)
logging.info('ServoHost: servo_type updated to %s '
'(previous: %s)', servo_type, old_type)
def set_servo_state(self, servo_state):
"""Set servo info labels to dut host_info"""
if servo_state is not None:
host_info = self.host_info_store.get()
servo_state_prefix = servo_constants.SERVO_STATE_LABEL_PREFIX
old_state = host_info.get_label_value(servo_state_prefix)
if old_state == servo_state:
# do not need update
return
host_info.set_version_label(servo_state_prefix, servo_state)
self.host_info_store.commit(host_info)
logging.info('ServoHost: servo_state updated to %s (previous: %s)',
servo_state, old_state)
def get_servo_state(self):
host_info = self.host_info_store.get()
servo_state_prefix = servo_constants.SERVO_STATE_LABEL_PREFIX
return host_info.get_label_value(servo_state_prefix)
def get_servo_usb_state(self):
"""Get the label value indicating the health of the USB drive.
@return: The label value if defined, otherwise '' (empty string).
@rtype: str
"""
host_info = self.host_info_store.get()
servo_usb_state_prefix = audit_const.SERVO_USB_STATE_PREFIX
return host_info.get_label_value(servo_usb_state_prefix)
def is_servo_usb_usable(self):
"""Check if the servo USB storage device is usable for FAFT.
@return: False if the label indicates a state that will break FAFT.
True if state is okay, or if state is not defined.
@rtype: bool
"""
usb_state = self.get_servo_usb_state()
return usb_state in ('', audit_const.HW_STATE_ACCEPTABLE,
audit_const.HW_STATE_NORMAL,
audit_const.HW_STATE_UNKNOWN)
def _set_smart_usbhub_label(self, smart_usbhub_detected):
if smart_usbhub_detected is None:
# skip the label update here as this indicate we wasn't able
# to confirm usbhub type.
return
host_info = self.host_info_store.get()
if (smart_usbhub_detected ==
(servo_constants.SMART_USBHUB_LABEL in host_info.labels)):
# skip label update if current label match the truth.
return
if smart_usbhub_detected:
logging.info('Adding %s label to host %s',
servo_constants.SMART_USBHUB_LABEL,
self.hostname)
host_info.labels.append(servo_constants.SMART_USBHUB_LABEL)
else:
logging.info('Removing %s label from host %s',
servo_constants.SMART_USBHUB_LABEL,
self.hostname)
host_info.labels.remove(servo_constants.SMART_USBHUB_LABEL)
self.host_info_store.commit(host_info)
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 relies on
`self._repair_strategy` to coordinate the verification and
repair steps needed to get the DUT working.
"""
message = 'Beginning repair for host %s board %s model %s'
info = self.host_info_store.get()
message %= (self.hostname, info.board, info.model)
self.record('INFO', None, None, message)
profile_state = profile_constants.DUT_STATE_READY
# Initialize bluetooth peers
self.initialize_btpeer()
try:
self._repair_strategy.repair(self)
except hosts.AutoservVerifyDependencyError as e:
# We don't want flag a DUT as failed if only non-critical
# verifier(s) failed during the repair.
if e.is_critical():
profile_state = profile_constants.DUT_STATE_REPAIR_FAILED
self.try_set_device_needs_manual_repair()
raise
finally:
self.set_health_profile_dut_state(profile_state)
def get_verifier_state(self, tag):
"""Return the state of servo verifier.
@returns: bool or None
"""
return self._repair_strategy.verifier_is_good(tag)
def close(self):
"""Close connection."""
super(CrosHost, self).close()
if self._chameleon_host:
self._chameleon_host.close()
if self.health_profile:
try:
self.health_profile.close()
except Exception as e:
logging.warning(
'Failed to finalize device health profile; %s', e)
if self._servo_host:
self._servo_host.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 get_battery_state(self):
"""Get the battery charging state.
@return: A string representing the battery charging state. It can be
'Charging', 'Fully charged', or 'Discharging'.
"""
try:
info = self.get_power_supply_info()
logging.info(info)
return info['Battery']['state']
except (KeyError, ValueError, error.AutoservRunError):
return None
def get_battery_display_percentage(self):
"""Get the battery display percentage.
@return: The display 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']['display 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."""
info = self.host_info_store.get()
if self._RPM_OUTLET_CHANGED not in info.attributes:
return
logging.debug('This host has recently interacted with the RPM'
' Infrastructure. Ensuring power is on.')
try:
self.power_on()
self._remove_rpm_changed_tag()
except rpm_client.RemotePowerException:
logging.error('Failed to turn Power On for this host after '
'cleanup through the RPM Infrastructure.')
battery_percentage = self.get_battery_percentage()
if (battery_percentage and
battery_percentage < cros_repair.MIN_BATTERY_LEVEL):
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.')
self._remove_rpm_changed_tag()
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 _remove_rpm_changed_tag(self):
info = self.host_info_store.get()
del info.attributes[self._RPM_OUTLET_CHANGED]
self.host_info_store.commit(info)
def _add_rpm_changed_tag(self):
info = self.host_info_store.get()
info.attributes[self._RPM_OUTLET_CHANGED] = 'true'
self.host_info_store.commit(info)
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 _start_powerd_if_needed(self):
"""Start powerd if it isn't already running."""
self.run('start powerd', ignore_status=True)
def _get_lsb_release_content(self):
"""Return the content of lsb-release file of host."""
return self.run(
'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip()
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.
"""
return lsbrelease_utils.get_chromeos_release_version(
lsb_release_content=self._get_lsb_release_content())
def get_release_builder_path(self):
"""Get the value of CHROMEOS_RELEASE_BUILDER_PATH from lsb-release.
@returns The version string in lsb-release, under attribute
CHROMEOS_RELEASE_BUILDER_PATH.
"""
return lsbrelease_utils.get_chromeos_release_builder_path(
lsb_release_content=self._get_lsb_release_content())
def get_chromeos_release_milestone(self):
"""Get the value of attribute CHROMEOS_RELEASE_BUILD_TYPE
from lsb-release.
@returns The version string in lsb-release, under attribute
CHROMEOS_RELEASE_BUILD_TYPE.
"""
return lsbrelease_utils.get_chromeos_release_milestone(
lsb_release_content=self._get_lsb_release_content())
def verify_cros_version_label(self):
"""Verify if host's cros-version label match the actual image in dut.
@returns True if the label match with image in dut, otherwise False
"""
os_from_host = self.get_release_builder_path()
info = self.host_info_store.get()
os_from_label = info.get_label_value(self.VERSION_PREFIX)
if not os_from_label:
logging.debug('No existing %s label detected', self.VERSION_PREFIX)
return True
# known cases where the version label will not match the
# original CHROMEOS_RELEASE_BUILDER_PATH setting:
# * Tests for the `arc-presubmit` append "-cheetsth" to the label.
if os_from_label.endswith(provision.CHEETS_SUFFIX):
logging.debug('%s label with %s suffix detected, this suffix will'
' be ignored when comparing label.',
self.VERSION_PREFIX, provision.CHEETS_SUFFIX)
os_from_label = os_from_label[:-len(provision.CHEETS_SUFFIX)]
logging.debug('OS version from host: %s; OS verision cached in '
'label: %s', os_from_host, os_from_label)
return os_from_label == os_from_host
def cleanup_services(self):
"""Reinitializes the device for cleanup.
Subclasses may override this to customize the cleanup method.
To indicate failure of the reset, the implementation may raise
any of:
error.AutoservRunError
error.AutotestRunError
FactoryImageCheckerException
@raises error.AutoservRunError
@raises error.AutotestRunError
@raises error.FactoryImageCheckerException
"""
self._restart_ui()
self._start_powerd_if_needed()
def cleanup(self, reboot_cmd=None):
"""Cleanup state on device.
@param reboot_cmd: command to use to reboot device
@return nothing
"""
self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE)
try:
self.cleanup_services()
except (error.AutotestRunError, error.AutoservRunError,
FactoryImageCheckerException):
logging.warning('Unable to restart ui.')
# cleanup routines, i.e. reboot the machine.
super(CrosHost, self).cleanup(reboot_cmd=reboot_cmd)
# Check if the rpm outlet was manipulated.
if self.has_power():
self._cleanup_poweron()
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 dargs.get('reboot_cmd') is None:
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
dargs['board'] = self.host_info_store.get().board
# Record who called us
orig = sys._getframe(1).f_code
metric_fields = {'board' : dargs['board'],
'dut_host_name' : self.hostname,
'success' : True}
metric_debug_fields = {'board' : dargs['board'],
'caller' : "%s:%s" % (orig.co_filename,
orig.co_name),
'success' : True,
'error' : ''}
t0 = time.time()
try:
logging.info("reboot cmd: %s", dargs.get('reboot_cmd'))
super(CrosHost, self).reboot(**dargs)
except Exception as e:
metric_fields['success'] = False
metric_debug_fields['success'] = False
metric_debug_fields['error'] = type(e).__name__
raise
finally:
duration = int(time.time() - t0)
metrics.Counter(
'chromeos/autotest/autoserv/reboot_count').increment(
fields=metric_fields)
metrics.Counter(
'chromeos/autotest/autoserv/reboot_debug').increment(
fields=metric_debug_fields)
metrics.SecondsDistribution(
'chromeos/autotest/autoserv/reboot_duration').add(
duration, fields=metric_fields)
def suspend(self, suspend_time=60, delay_seconds=0,
suspend_cmd=None, allow_early_resume=False):
"""
This function suspends the site host.
@param suspend_time: How long to suspend as integer seconds.
@param suspend_cmd: Suspend command to execute.
@param allow_early_resume: If False and if device resumes before
|suspend_time|, throw an error.
@exception AutoservSuspendError Host resumed earlier than
|suspend_time|.
"""
if suspend_cmd is None:
suspend_cmd = ' && '.join([
'echo 0 > /sys/class/rtc/rtc0/wakealarm',
'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time,
'powerd_dbus_suspend --delay=%d' % delay_seconds])
super(CrosHost, self).suspend(suspend_time, suspend_cmd,
allow_early_resume);
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 'start/running' in self.run('status %s' % service_name,
ignore_status=True).stdout
def upstart_stop(self, service_name):
"""Stops an upstart job if it's running.
@param service_name: Service to stop
@returns True if service has been stopped or was already stopped
False otherwise.
"""
if not self.upstart_status(service_name):
return True
result = self.run('stop %s' % service_name, ignore_status=True)
if result.exit_status != 0:
return False
return True
def upstart_restart(self, service_name):
"""Restarts (or starts) an upstart job.
@param service_name: Service to start/restart
@returns True if service has been started/restarted, False otherwise.
"""
cmd = 'start'
if self.upstart_status(service_name):
cmd = 'restart'
cmd = cmd + ' %s' % service_name
result = self.run(cmd)
if result.exit_status != 0:
return False
return True
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.
"""
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))
self.wait_for_system_services()
# 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')
@retry.retry(error.AutoservError, timeout_min=5, delay_sec=10)
def wait_for_system_services(self):
"""Waits for system-services to be running.
Sometimes, update_engine will take a while to update firmware, so we
should give this some time to finish. See crbug.com/765686#c38 for
details.
"""
if not self.upstart_status('system-services'):
raise error.AutoservError('Chrome failed to reach login. '
'System services not running.')
def verify(self):
"""Verify Chrome OS system is in good state."""
message = 'Beginning verify for host %s board %s model %s'
info = self.host_info_store.get()
message %= (self.hostname, info.board, info.model)
self.record('INFO', None, None, message)
try:
self._repair_strategy.verify(self)
except hosts.AutoservVerifyDependencyError as e:
# We don't want flag a DUT as failed if only non-critical
# verifier(s) failed during the repair.
if e.is_critical():
raise
def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None,
connect_timeout=None, alive_interval=None,
alive_count_max=None, connection_attempts=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.
@param alive_count_max Ignored.
@param connection_attempts Ignored.
"""
options = ' '.join([opts, '-o Protocol=2'])
return super(CrosHost, self).make_ssh_command(
user=user, port=port, opts=options, hosts_file='/dev/null',
connect_timeout=30, alive_interval=900, alive_count_max=3,
connection_attempts=4)
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,
timeout=2,
ignore_timeout=True)
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.
# FIXME: if the ping command errors then _ping_check_status()
# returns instantly. If timeout is also smaller than twice
# _PING_WAIT_COUNT then the while loop below forks many
# thousands of ping commands (see /tmp/test_that_results_XXXXX/
# /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one
# CPU core for 60 seconds.
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 _is_host_port_forwarded(self):
"""Checks if the dut is connected over port forwarding.
N.B. This method does not detect all situations where port forwarding is
occurring. Namely, running autotest on the dut may result in a
false-positive, and port forwarding using a different machine on the
same network will be a false-negative.
@return True if the dut is connected over port forwarding
False otherwise
"""
is_localhost = self.hostname in ['localhost', '127.0.0.1']
is_forwarded = is_localhost and not self.is_default_port
if is_forwarded:
logging.info('Detected DUT connected by port forwarding')
return is_forwarded
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 the dut is accessed over SSH port-forwarding, `ping` is not useful
# for detecting the dut is down since a ping to localhost will always
# succeed. In this case, fall back to wait_down() which uses SSH.
if self._is_host_port_forwarded():
success = self.wait_down(timeout=sleep_timeout)
else:
success = self.ping_wait_down(timeout=sleep_timeout)
if not success:
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 self._is_host_port_forwarded():
success = self.wait_down(timeout=shutdown_timeout)
else:
success = self.ping_wait_down(timeout=shutdown_timeout)
if not success:
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, CCD, 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" or "CCD" will be used based
on servo type. Valid values from
POWER_CONTROL_VALID_ARGS, or None to use default.
"""
ACCEPTABLE_STATES = ['ON', 'OFF']
if not power_method:
power_method = self.get_default_power_method()
state = state.upper()
if state 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)
elif power_method == self.POWER_CONTROL_CCD:
servo_role = 'src' if state == 'ON' else 'snk'
logging.info('servo ccd power pass through detected,'
' changing servo_role to %s.', servo_role)
self.servo.set_servo_v4_role(servo_role)
if not self.ping_wait_up(timeout=self._CHANGE_SERVO_ROLE_TIMEOUT):
# Make sure we don't leave DUT with no power(servo_role=snk)
# when DUT is not pingable, as we raise a exception here
# that may break a power cycle in the middle.
self.servo.set_servo_v4_role('src')
raise error.AutoservError(
'DUT failed to regain network connection after %d seconds.'
% self._CHANGE_SERVO_ROLE_TIMEOUT)
else:
if not self.has_power():
raise error.TestFail('DUT does not have RPM connected.')
self._add_rpm_changed_tag()
rpm_client.set_power(self, state, timeout_mins=5)
def power_off(self, power_method=None):
"""Turn off power to this host via RPM, CCD, Servo or manual.
@param power_method Specifies which method of power control to
use. By default "RPM" or "CCD" will be used based
on servo type. Valid values from
POWER_CONTROL_VALID_ARGS, or None to use default.
"""
self._sync_if_up()
self._set_power('OFF', power_method)
def _check_supported(self):
"""Throw an error if dts mode control is not supported."""
if not self.servo_pwr_supported:
raise error.TestFail('power_state controls not supported')
def _sync_if_up(self):
"""Run sync on the DUT and wait for completion if the DUT is up.
Additionally, try to sync and ignore status if its not up.
Useful prior to reboots to ensure files are written to disc.
"""
if self.is_up_fast():
self.run("sync")
return
# If it is not up, attempt to sync in the rare event the DUT is up but
# doesn't respond to a ping. Ignore any errors.
try:
self.run("sync", ignore_status=True, timeout=1)
except Exception:
pass
def power_off_via_servo(self):
"""Force the DUT to power off.
The DUT is guaranteed to be off at the end of this call,
regardless of its previous state, provided that there is
working EC and boot firmware. There is no requirement for
working OS software.
"""
self._check_supported()
self._sync_if_up()
self.servo.set_nocheck('power_state', 'off')
def power_on_via_servo(self, rec_mode='on'):
"""Force the DUT to power on.
Prior to calling this function, the DUT must be powered off,
e.g. with a call to `power_off()`.
At power on, recovery mode is set as specified by the
corresponding argument. When booting with recovery mode on, it
is the caller's responsibility to unplug/plug in a bootable
external storage device.
If the DUT requires a delay after powering on but before
processing inputs such as USB stick insertion, the delay is
handled by this method; the caller is not responsible for such
delays.
@param rec_mode Setting of recovery mode to be applied at
power on. default: REC_OFF aka 'off'
"""
self._check_supported()
self.servo.set_nocheck('power_state', rec_mode)
def reset_via_servo(self):
"""Force the DUT to reset.
The DUT is guaranteed to be on at the end of this call,
regardless of its previous state, provided that there is
working OS software. This also guarantees that the EC has
been restarted.
"""
self._check_supported()
self._sync_if_up()
self.servo.set_nocheck('power_state', 'reset')
def power_on(self, power_method=None):
"""Turn on power to this host via RPM, CCD, Servo or manual.
@param power_method Specifies which method of power control to
use. By default "RPM" or "CCD" will be used based
on servo type. Valid values from
POWER_CONTROL_VALID_ARGS, or None to use default.
"""
self._set_power('ON', power_method)
def power_cycle(self, power_method=None):
"""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" or "CCD" will be used based
on servo type. Valid values from
POWER_CONTROL_VALID_ARGS, or None to use default.
"""
if not power_method:
power_method = self.get_default_power_method()
if power_method in (self.POWER_CONTROL_SERVO,
self.POWER_CONTROL_MANUAL,
self.POWER_CONTROL_CCD):
self.power_off(power_method=power_method)
time.sleep(self._POWER_CYCLE_TIMEOUT)
self.power_on(power_method=power_method)
else:
self._add_rpm_changed_tag()
rpm_client.set_power(self, 'CYCLE')
def get_platform_from_fwid(self):
"""Determine the platform from the crossystem fwid.
@returns a string representing this host's platform.
"""
# Look at the firmware for non-unibuild cases or if cros_config fails.
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_platform(self):
"""Determine the correct platform label for this host.
@returns a string representing this host's platform.
"""
release_info = utils.parse_cmd_output('cat /etc/lsb-release',
run_method=self.run)
platform = ''
if release_info.get('CHROMEOS_RELEASE_UNIBUILD') == '1':
platform = self.get_model_from_cros_config()
return platform if platform else self.get_platform_from_fwid()
def get_model_from_cros_config(self):
"""Get the host model from cros_config command.
@returns a string representing this host's model.
"""
return cros_config.call_cros_config_get_output('/ name',
self.run, ignore_status=True)
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)
def get_ec_version(self):
"""Get the ec version as strings.
@returns a string representing this host's ec version.
"""
command = 'mosys ec info -s fw_version'
result = self.run(command, ignore_status=True)
if result.exit_status != 0:
return ''
return result.stdout.strip()
def get_firmware_version(self):
"""Get the firmware version as strings.
@returns a string representing this host's firmware version.
"""
crossystem = utils.Crossystem(self)
crossystem.init()
return crossystem.fwid()
def get_hardware_revision(self):
"""Get the hardware revision as strings.
@returns a string representing this host's hardware revision.
"""
command = 'mosys platform version'
result = self.run(command, ignore_status=True)
if result.exit_status != 0:
return ''
return result.stdout.strip()
def get_kernel_version(self):
"""Get the kernel version as strings.
@returns a string representing this host's kernel version.
"""
return self.run('uname -r').stdout.strip()
def get_cpu_name(self):
"""Get the cpu name as strings.
@returns a string representing this host's cpu name.
"""
# Try get cpu name from device tree first
if self.path_exists('/proc/device-tree/compatible'):
command = ' | '.join(
["sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible",
'tail -1'])
return self.run(command).stdout.strip().replace(',', ' ')
# Get cpu name from uname -p
command = 'uname -p'
ret = self.run(command).stdout.strip()
# 'uname -p' return variant of unknown or amd64 or x86_64 or i686
# Try get cpu name from /proc/cpuinfo instead
if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE):
command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1"
self = self.run(command).stdout.strip()
# Remove bloat from CPU name, for example
# Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz -> Intel Core i5-7Y57
# Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz -> Intel Xeon E5-2690 v4
# AMD A10-7850K APU with Radeon(TM) R7 Graphics -> AMD A10-7850K
# AMD GX-212JC SOC with Radeon(TM) R2E Graphics -> AMD GX-212JC
trim_re = r' (@|processor|apu|soc|radeon).*|\(.*?\)| cpu'
return re.sub(trim_re, '', ret, flags=re.IGNORECASE)
def get_screen_resolution(self):
"""Get the screen(s) resolution as strings.
In case of more than 1 monitor, return resolution for each monitor
separate with plus sign.
@returns a string representing this host's screen(s) resolution.
"""
command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done'
ret = self.run(command, ignore_status=True)
# We might have Chromebox without a screen
if ret.exit_status != 0:
return ''
return ret.stdout.strip().replace('\n', '+')
def get_mem_total_gb(self):
"""Get total memory available in the system in GiB (2^20).
@returns an integer representing total memory
"""
mem_total_kb = self.read_from_meminfo('MemTotal')
kb_in_gb = float(2 ** 20)
return int(round(mem_total_kb / kb_in_gb))
def get_disk_size_gb(self):
"""Get size of disk in GB (10^9)
@returns an integer representing size of disk, 0 in Error Case
"""
command = 'grep $(rootdev -s -d | cut -f3 -d/)$ /proc/partitions'
result = self.run(command, ignore_status=True)
if result.exit_status != 0:
return 0
_, _, block, _ = re.split(r' +', result.stdout.strip())
byte_per_block = 1024.0
disk_kb_in_gb = 1e9
return int(int(block) * byte_per_block / disk_kb_in_gb + 0.5)
def get_battery_size(self):
"""Get size of battery in Watt-hour via sysfs
This method assumes that battery support voltage_min_design and
charge_full_design sysfs.
@returns a float representing Battery size, 0 if error.
"""
# sysfs report data in micro scale
battery_scale = 1e6
command = 'cat /sys/class/power_supply/*/voltage_min_design'
result = self.run(command, ignore_status=True)
if result.exit_status != 0:
return 0
voltage = float(result.stdout.strip()) / battery_scale
command = 'cat /sys/class/power_supply/*/charge_full_design'
result = self.run(command, ignore_status=True)
if result.exit_status != 0:
return 0
amphereHour = float(result.stdout.strip()) / battery_scale
return voltage * amphereHour
def get_low_battery_shutdown_percent(self):
"""Get the percent-based low-battery shutdown threshold.
@returns a float representing low-battery shutdown percent, 0 if error.
"""
ret = 0.0
try:
command = 'check_powerd_config --low_battery_shutdown_percent'
ret = float(self.run(command).stdout)
except error.CmdError:
logging.debug("Can't run %s", command)
except ValueError:
logging.debug("Didn't get number from %s", command)
return ret
def has_hammer(self):
"""Check whether DUT has hammer device or not.
@returns boolean whether device has hammer or not
"""
command = 'grep Hammer /sys/bus/usb/devices/*/product'
return self.run(command, ignore_status=True).exit_status == 0
def is_chrome_switch_present(self, switch):
"""Returns True if the specified switch was provided to Chrome.
@param switch The chrome switch to search for.
"""
command = 'pgrep -x -f -c "/opt/google/chrome/chrome.*%s.*"' % switch
return self.run(command, ignore_status=True).exit_status == 0
def oobe_triggers_update(self):
"""Returns True if this host has an OOBE flow during which
it will perform an update check and perhaps an update.
One example of such a flow is Hands-Off Zero-Touch Enrollment.
As more such flows are developed, code handling them needs
to be added here.
@return Boolean indicating whether this host's OOBE triggers an update.
"""
return self.is_chrome_switch_present(
'--enterprise-enable-zero-touch-enrollment=hands-off')
# TODO(kevcheng): change this to just return the board without the
# 'board:' prefix and fix up all the callers. Also look into removing the
# need for this method.
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'])
def get_channel(self):
"""Determine the correct channel label for this host.
@returns: a string represeting this host's build channel.
(stable, dev, beta). None on fail.
"""
return lsbrelease_utils.get_chromeos_channel(
lsb_release_content=self._get_lsb_release_content())
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
def has_battery(self):
"""Determine if DUT has a battery.
Returns:
Boolean, False if known not to have battery, True otherwise.
"""
rv = True
power_supply = self.get_power_supply()
if power_supply == 'power:battery':
_NO_BATTERY_BOARD_TYPE = ['CHROMEBOX', 'CHROMEBIT', 'CHROMEBASE']
board_type = self.get_board_type()
if board_type in _NO_BATTERY_BOARD_TYPE:
logging.warn('Do NOT believe type %s has battery. '
'See debug for mosys details', board_type)
psu = utils.system_output('mosys -vvvv psu type',
ignore_status=True)
logging.debug(psu)
rv = False
elif power_supply == 'power:AC_only':
rv = False
return rv
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
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
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 is_boot_from_external_device(self):
"""Check if DUT is boot from external storage.
@return: True if DUT is boot from external storage.
"""
boot_device = self.run('rootdev -s -d', ignore_status=True,
timeout=60).stdout.strip()
if not boot_device:
logging.debug('Boot storage not detected on the host.')
return False
main_storage_cmd = ('. /usr/sbin/write_gpt.sh;'
' . /usr/share/misc/chromeos-common.sh;'
' load_base_vars; get_fixed_dst_drive')
main_storage = self.run(main_storage_cmd,
ignore_status=True,
timeout=60).stdout.strip()
if not main_storage:
logging.debug('Main storage not detected on the host.')
return False
if boot_device == main_storage:
logging.debug('Device booted from main storage.')
return False
logging.debug('Device booted from external storage storage.')
return True
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_cpu_arch(self):
"""Returns CPU arch of the device.
@return CPU architecture of the DUT.
"""
# Add CPUs by following logic in client/bin/utils.py.
if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo",
ignore_status=True).stdout:
return 'x86_64'
if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo",
ignore_status=True).stdout:
return 'arm'
return 'i386'
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_arc_version(self):
"""Return ARC version installed on the DUT.
@returns ARC version as string if the CrOS build has ARC, else None.
"""
arc_version = self.run('grep CHROMEOS_ARC_VERSION /etc/lsb-release',
ignore_status=True).stdout
if arc_version:
return arc_version.split('=')[-1].strip()
return None
def get_os_type(self):
return 'cros'
def get_labels(self):
"""Return the detected labels on the host."""
return self.labels.get_labels(self)
def get_default_power_method(self):
"""
Get the default power method for power_on/off/cycle() methods.
@return POWER_CONTROL_RPM or POWER_CONTROL_CCD
"""
if not self._default_power_method:
self._default_power_method = self.POWER_CONTROL_RPM
if self.servo and self.servo.supports_built_in_pd_control():
self._default_power_method = self.POWER_CONTROL_CCD
else:
logging.debug('Either servo is unitialized or the servo '
'setup does not support pd controls. Falling '
'back to default RPM method.')
return self._default_power_method
def find_usb_devices(self, idVendor, idProduct):
"""
Get usb device sysfs name for specific device.
@param idVendor Vendor ID to search in sysfs directory.
@param idProduct Product ID to search in sysfs directory.
@return Usb node names in /sys/bus/usb/drivers/usb/ that match.
"""
# Look for matching file and cut at position 7 to get dir name.
grep_cmd = 'grep {} /sys/bus/usb/drivers/usb/*/{} | cut -f 7 -d /'
vendor_cmd = grep_cmd.format(idVendor, 'idVendor')
product_cmd = grep_cmd.format(idProduct, 'idProduct')
# Use uniq -d to print duplicate line from both command
cmd = 'sort <({}) <({}) | uniq -d'.format(vendor_cmd, product_cmd)
return self.run(cmd, ignore_status=True).stdout.strip().split('\n')
def bind_usb_device(self, usb_node):
"""
Bind usb device
@param usb_node Node name in /sys/bus/usb/drivers/usb/
"""
cmd = 'echo {} > /sys/bus/usb/drivers/usb/bind'.format(usb_node)
self.run(cmd, ignore_status=True)
def unbind_usb_device(self, usb_node):
"""
Unbind usb device
@param usb_node Node name in /sys/bus/usb/drivers/usb/
"""
cmd = 'echo {} > /sys/bus/usb/drivers/usb/unbind'.format(usb_node)
self.run(cmd, ignore_status=True)
def get_wlan_ip(self):
"""
Get ip address of wlan interface.
@return ip address of wlan or empty string if wlan is not connected.
"""
cmds = [
'iw dev', # List wlan physical device
'grep Interface', # Grep only interface name
'cut -f 2 -d" "', # Cut the name part
'xargs ifconfig', # Feed it to ifconfig to get ip
'grep -oE "inet [0-9.]+"', # Grep only ipv4
'cut -f 2 -d " "' # Cut the ip part
]
return self.run(' | '.join(cmds), ignore_status=True).stdout.strip()
def connect_to_wifi(self, ssid, passphrase=None, security=None):
"""
Connect to wifi network
@param ssid SSID of the wifi network.
@param passphrase Passphrase of the wifi network. None if not existed.
@param security Security of the wifi network. Default to "psk" if
passphase is given without security. Possible values
are "none", "psk", "802_1x".
@return True if succeed, False if not.
"""
cmd = '/usr/local/autotest/cros/scripts/wifi connect ' + ssid
if passphrase:
cmd += ' ' + passphrase
if security:
cmd += ' ' + security
return self.run(cmd, ignore_status=True).exit_status == 0
def get_device_repair_state(self):
"""Get device repair state"""
return self._device_repair_state
def set_device_repair_state(self, state, resultdir=None):
"""Set device repair state.
The special device state will be written to the 'dut_state.repair'
file in result directory. The file will be read by Lucifer. The
file will not be created if result directory not specified.
@params state: The new state for the device.
@params resultdir: The path to result directory. If path not provided
will be attempt to get retrieve it from job
if present.
"""
resultdir = resultdir or getattr(self.job, 'resultdir', '')
if resultdir:
target = os.path.join(resultdir, 'dut_state.repair')
common_utils.open_write_close(target, state)
logging.info('Set device state as %s. '
'Created dut_state.repair file.', state)
else:
logging.debug('Cannot write the device state due missing info '
'about result dir.')
self._device_repair_state = state
def set_device_needs_replacement(self, resultdir=None):
"""Set device as required replacement.
@params resultdir: The path to result directory. If path not provided
will be attempt to get retrieve it from job
if present.
"""
self.set_device_repair_state(
cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT,
resultdir=resultdir)
def try_set_device_needs_manual_repair(self):
"""Check if device require manual attention to be fixed.
The state 'needs_manual_repair' can be set when auto repair cannot
fix the device due hardware or cable issues.
"""
# ignore the logic if state present
# state can be set by any cros repair actions
if self.get_device_repair_state() or not self._repair_strategy:
return
dut_ssh_verifier = self._repair_strategy.verifier_is_good('ssh')
if dut_ssh_verifier == hosts.VERIFY_SUCCESS:
# DUT us sshable and we still have many options to repair it.
return
# when we cannot ssh to the DUT and we have hardware or set-up
# issues with servo then we need request manual repair for this DUT.
servo_state_required_manual_fix = [
servo_constants.SERVO_STATE_DUT_NOT_CONNECTED,
servo_constants.SERVO_STATE_NEED_REPLACEMENT,
]
if self.get_servo_state() in servo_state_required_manual_fix:
logging.info(
'DUT required manual repair because it is not sshable'
' and possible have setup issue with Servo. Please verify'
' all connections and present of devices.')
self.set_device_repair_state(
cros_constants.DEVICE_STATE_NEEDS_MANUAL_REPAIR)
def is_file_system_writable(self, testdirs=None):
"""Check is the file systems are writable.
The standard linux response to certain unexpected file system errors
(including hardware errors in block devices) is to change the file
system status to read-only. This checks that that hasn't happened.
@param testdirs: List of directories to check. If no data provided
then '/mnt/stateful_partition' and '/var/tmp'
directories will be checked.
@returns boolean whether file-system writable.
"""
def _check_dir(testdir):
# check if we can create a file
filename = os.path.join(testdir, 'writable_my_test_file')
command = 'touch %s && rm %s' % (filename, filename)
rv = self.run(command=command,
timeout=30,
ignore_status=True)
is_writable = rv.exit_status == 0
if not is_writable:
logging.info('Cannot create a file in "%s"!'
' Probably the FS is read-only', testdir)
logging.info("FileSystem is not writable!")
return False
return True
if not testdirs or len(testdirs) == 0:
# N.B. Order matters here: Encrypted stateful is loop-mounted
# from a file in unencrypted stateful, so we don't test for
# errors in encrypted stateful if unencrypted fails.
testdirs = ['/mnt/stateful_partition', '/var/tmp']
for dir in testdirs:
# loop will be stopped if any directory fill fail the check
try:
if not _check_dir(dir):
return False
except Exception as e:
# here expected only timeout error, all other will
# be catch by 'ignore_status=True'
logging.debug('Fail to check %s to write in it', dir)
return False
return True
def blocking_sync(self, freeze_for_reset=False):
"""Sync root device and internal device, via script.
The actual calls end up logged by the run() call, since they're printed
to stdout/stderr in the script.
@param freeze_for_reset: if True, prepare for reset by blocking writes
(only if enable_fs_sync_fsfreeze=True)
"""
if freeze_for_reset and self.USE_FSFREEZE:
logging.info('Blocking sync and freeze')
elif freeze_for_reset:
logging.info('Blocking sync for reset')
else:
logging.info('Blocking sync')
# client/bin is installed on the DUT as /usr/local/autotest/bin
sync_cmd = '/usr/local/autotest/bin/fs_sync.py'
if freeze_for_reset and self.USE_FSFREEZE:
sync_cmd += ' --freeze'
return self.run(sync_cmd)
def setup_device_health_profile(self):
"""Setup device health profile for repair/provision task to consume.
"""
if self.health_profile:
logging.info('Device health profile has already been initialized.')
if not self._servo_host:
logging.info('Servohost is not instantiated, skip device'
' health profile setup...')
return
# Also skip setup health profile if it's a task runs locally.
if self._servo_host.is_localhost():
logging.info('Servohost is a localhost, skip device'
' health profile setup...')
return
try:
self.health_profile = device_health_profile.DeviceHealthProfile(
self, self._servo_host)
except Exception as e:
logging.warning('Failed to setup device health profile; %s', e)
def set_health_profile_dut_state(self, state):
if not self.health_profile:
logging.debug('Device health profile is not initialized, skip'
' set dut state.')
return
reset_counters = state in profile_constants.STATES_NEED_RESET_COUNTER
self.health_profile.update_dut_state(state, reset_counters)
def require_snk_mode_in_recovery(self):
"""Check whether we need to switch servo_v4 role to snk when
booting into recovery mode. (See crbug.com/1129165)
"""
info = self.host_info_store.get()
if info.get_label_value('power') != 'battery':
logging.info(
'%s does not has battery, snk mode is not needed'
' for recovery.', self.hostname)
return False
if not self.servo.supports_built_in_pd_control():
logging.info('Power delivery is not supported on this servo, snk'
' mode is not needed for recovery.')
return False
try:
#TODO(xianuowang@) move MIN_BATTERY_LEVEL to cros_constant
battery_percent = self.servo.get('battery_charge_percent')
if battery_percent < cros_repair.MIN_BATTERY_LEVEL:
logging.info(
'Current battery level %s%% below %s%% threshold, we'
' will attempt to boot host in recovery mode without'
' changing servo to snk mode. Please note the host may'
' not able to see usb drive in recovery mode later due'
' to servo not in snk mode.', battery_percent,
cros_repair.MIN_BATTERY_LEVEL)
return False
except Exception as e:
logging.info(
'Unexpected error occurred when getting'
' battery_charge_percent from servo; %s', str(e))
return False
return True
def _set_servo_topology(self):
"""Set servo-topology info to the host-info."""
logging.debug('Try to save servo topology to host-info.')
if not self._servo_host:
logging.info('Servo host is not initilized.')
return
if not self._servo_host.is_servo_topology_supported():
logging.info('Servo-topology is not supported.')
return
servo_topology = self._servo_host.get_topology()
if not servo_topology or servo_topology.is_empty():
logging.info('Servo topology is empty')
return
servo_topology.save(self.host_info_store)