blob: 95b69ac46c8e26fdcb56767c568bc26ef43185b5 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import 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.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.servo import plankton
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_label
from autotest_lib.server.hosts import cros_repair
from autotest_lib.server.hosts import plankton_host
from autotest_lib.server.hosts import servo_host
from autotest_lib.site_utils.rpm_control_system import rpm_client
# 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)
# REBOOT_TIMEOUT: How long to wait for a reboot.
#
# We have a long timeout to ensure we don't flakily fail due to other
# issues. Shorter timeouts are vetted in platform_RebootAfterUpdate.
# TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not
# return from reboot' bug is solved.
REBOOT_TIMEOUT = 480
# _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF.
# _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle.
_USB_POWER_TIMEOUT = 5
_POWER_CYCLE_TIMEOUT = 10
_RPM_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: Passed as default arg for power_off/on/cycle() methods.
# POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods.
# POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods.
POWER_CONTROL_RPM = 'RPM'
POWER_CONTROL_SERVO = 'servoj10'
POWER_CONTROL_MANUAL = 'manual'
POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM,
POWER_CONTROL_SERVO,
POWER_CONTROL_MANUAL)
_RPM_OUTLET_CHANGED = 'outlet_changed'
# URL pattern to download firmware image.
_FW_IMAGE_URL_PATTERN = CONFIG.get_config_value(
'CROS', 'firmware_url_pattern', type=str)
@staticmethod
def check_host(host, timeout=10):
"""
Check if the given host is a chrome-os host.
@param host: An ssh host representing a device.
@param timeout: The timeout for the run command.
@return: True if the host device is chromeos.
"""
try:
result = host.run(
'grep -q CHROMEOS /etc/lsb-release && '
'! test -f /mnt/stateful_partition/.android_tester && '
'! grep -q moblab /etc/lsb-release',
ignore_status=True, timeout=timeout)
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.
"""
return {key: args_dict[key]
for key in ('chameleon_host', 'chameleon_port')
if key in args_dict}
@staticmethod
def get_plankton_arguments(args_dict):
"""Extract chameleon options from `args_dict` and return the result.
Recommended usage:
~~~~~~~~
args_dict = utils.args_to_dict(args)
plankton_args = hosts.CrosHost.get_plankton_arguments(args_dict)
host = hosts.create_host(machine, plankton_args=plankton_args)
~~~~~~~~
@param args_dict Dictionary from which to extract the plankton
arguments.
"""
return {key: args_dict[key]
for key in ('plankton_host', 'plankton_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_host.SERVO_HOST_ATTR,
servo_host.SERVO_PORT_ATTR,
servo_host.SERVO_BOARD_ATTR,
servo_host.SERVO_MODEL_ATTR)
servo_args = {key: args_dict[key]
for key in servo_attrs
if key in args_dict}
return (
None
if servo_host.SERVO_HOST_ATTR in servo_args
and not servo_args[servo_host.SERVO_HOST_ATTR]
else servo_args)
def _initialize(self, hostname, chameleon_args=None, servo_args=None,
plankton_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()
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
self.set_servo_host(
servo_host.create_servo_host(
dut=self, servo_args=servo_args,
try_lab_servo=try_lab_servo,
try_servo_repair=try_servo_repair))
# TODO(waihong): Do the simplication on Chameleon too.
self._chameleon_host = chameleon_host.create_chameleon_host(
dut=self.hostname, chameleon_args=chameleon_args)
# Add plankton host if plankton args were added on command line
self._plankton_host = plankton_host.create_plankton_host(plankton_args)
if self._chameleon_host:
self.chameleon = self._chameleon_host.create_chameleon_board()
else:
self.chameleon = None
if self._plankton_host:
self.plankton_servo = self._plankton_host.get_servo()
logging.info('plankton_servo: %r', self.plankton_servo)
# Create the plankton object used to access the ec uart
self.plankton = plankton.Plankton(self.plankton_servo,
self._plankton_host.get_servod_server_proxy())
else:
self.plankton = None
def get_cros_repair_image_name(self):
info = self.host_info_store.get()
if not info.board:
raise error.AutoservError('Cannot obtain repair image name. '
'No board label value found')
return afe_utils.get_stable_cros_image_name(info.board)
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 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,
# TODO(akeshet): To be consistent with most other metrics,
# consider changing the following field to be named
# 'milestone'.
'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_report_url attribute or 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 an update URL like:
http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0
"""
if not image_name:
image_name = self.get_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 devserver.get_test_image_url(image_name)
elif artifact == 'recovery_image':
return 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.
"""
labels = self._AFE.get_labels(
name__startswith=provision.FW_RW_VERSION_PREFIX,
host__hostname=self.hostname)
if not rw_only:
labels = labels + self._AFE.get_labels(
name__startswith=provision.FW_RO_VERSION_PREFIX,
host__hostname=self.hostname)
for label in labels:
label.remove_hosts(hosts=[self.hostname])
def _add_fw_version_label(self, build, rw_only):
"""Add firmware version label to the machine.
@param build: Build of firmware.
@param rw_only: True to only add fwrw_version; otherwise, add both
fwro_version and fwrw_version.
"""
fw_label = provision.fwrw_version_to_label(build)
self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
if not rw_only:
fw_label = provision.fwro_version_to_label(build)
self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname])
def firmware_install(self, build=None, rw_only=False):
"""Install firmware to the DUT.
Use stateful update if the DUT is already running the same build.
Stateful update does not update kernel and tends to run much faster
than a full reimage. If the DUT is running a different build, or it
failed to do a stateful update, full update, including kernel update,
will be applied to the DUT.
Once a host enters firmware_install its fw[ro|rw]_version label will
be removed. After the firmware is updated successfully, a new
fw[ro|rw]_version label will be added to the host.
@param build: The build version to which we want to provision the
firmware of the machine,
e.g. 'link-firmware/R22-2695.1.144'.
@param rw_only: True to only install firmware to its RW portions. Keep
the RO portions unchanged.
TODO(dshi): After bug 381718 is fixed, update here with corresponding
exceptions that could be raised.
"""
if not self.servo:
raise error.TestError('Host %s does not have servo.' %
self.hostname)
# Get the DUT board name from AFE.
info = self.host_info_store.get()
board = info.board
if board is None or board == '':
board = self.servo.get_board()
# If build is not set, try to install firmware from stable CrOS.
if not build:
build = afe_utils.get_stable_faft_version(board)
if not build:
raise error.TestError(
'Failed to find stable firmware build for %s.',
self.hostname)
logging.info('Will install firmware from build %s.', build)
ds = dev_server.ImageServer.resolve(build, self.hostname)
ds.stage_artifacts(build, ['firmware'])
tmpd = autotemp.tempdir(unique_id='fwimage')
try:
fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build)
local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl))
ds.download_file(fwurl, local_tarball)
self._clear_fw_version_labels(rw_only)
self.servo.program_firmware(board, local_tarball, rw_only)
if utils.host_is_in_lab_zone(self.hostname):
self._add_fw_version_label(build, rw_only)
finally:
tmpd.clean()
def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT,
install_timeout=INSTALL_TIMEOUT):
"""
Re-install the OS on the DUT by:
1) installing a test image on a USB storage device attached to the Servo
board,
2) booting that image in recovery mode, and then
3) installing the image with chromeos-install.
@param image_url: If specified use as the url to install on the DUT.
otherwise boot the currently staged image on the USB stick.
@param usb_boot_timeout: The usb_boot_timeout to use during reimage.
Factory images need a longer usb_boot_timeout than regular
cros images.
@param install_timeout: The timeout to use when installing the chromeos
image. Factory images need a longer install_timeout.
@raises AutoservError if the image fails to boot.
"""
logging.info('Downloading image to USB, then booting from it. Usb boot '
'timeout = %s', usb_boot_timeout)
with metrics.SecondsTimer(
'chromeos/autotest/provision/servo_install/boot_duration'):
self.servo.install_recovery_image(image_url)
if not self.wait_up(timeout=usb_boot_timeout):
raise hosts.AutoservRepairError(
'DUT failed to boot from USB after %d seconds' %
usb_boot_timeout, 'failed_to_reboot')
# 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.')
self.run('chromeos-install --yes', timeout=install_timeout)
self.halt()
logging.info('Power cycling DUT through servo.')
self.servo.get_power_state_controller().power_off()
self.servo.switch_usbkey('off')
# N.B. The Servo API requires that we use power_on() here
# for two reasons:
# 1) After turning on a DUT in recovery mode, you must turn
# it off and then on with power_on() once more to
# disable recovery mode (this is a Parrot specific
# requirement).
# 2) After power_off(), the only way to turn on is with
# power_on() (this is a Storm specific requirement).
self.servo.get_power_state_controller().power_on()
logging.info('Waiting for DUT to come back up.')
if not self.wait_up(timeout=self.BOOT_TIMEOUT):
raise error.AutoservError('DUT failed to reboot installed '
'test image after %d seconds' %
self.BOOT_TIMEOUT)
def set_servo_host(self, host):
"""Set our servo host member, and associated servo.
@param host Our new `ServoHost`.
"""
self._servo_host = host
if self._servo_host is not None:
self.servo = self._servo_host.get_servo()
else:
self.servo = None
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)
self._servo_host.repair()
self.servo = self._servo_host.get_servo()
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)
self._repair_strategy.repair(self)
def close(self):
"""Close connection."""
super(CrosHost, self).close()
if self._chameleon_host:
self._chameleon_host.close()
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 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 < 50:
raise
elif self.is_ac_connected():
logging.info('The device has power adapter connected and '
'charging. No need to try to turn RPM on '
'again.')
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):
""" Make sure host's cros-version label match the actual image in dut.
Remove any cros-version: label that doesn't match that installed in
the dut.
@param raise_error: Set to True to raise exception if any mismatch found
@raise error.AutoservError: If any mismatch between cros-version label
and the build installed in dut is found.
"""
labels = self._AFE.get_labels(
name__startswith=ds_constants.VERSION_PREFIX,
host__hostname=self.hostname)
mismatch_found = False
if labels:
# Ask the DUT for its canonical image name. This will be in
# a form like this: kevin-release/R66-10405.0.0
release_builder_path = self.get_release_builder_path()
host_list = [self.hostname]
for label in labels:
# Remove any cros-version label that does not match
# the DUT's installed image.
#
# TODO(jrbarnette): We make exceptions for certain
# known cases where the version label will not match the
# original CHROMEOS_RELEASE_BUILDER_PATH setting:
# * Tests for the `arc-presubmit` pool append
# "-cheetsth" to the label.
# * Moblab use cases based on `cros stage` store images
# under a name with the string "-custom" embedded.
# It's not reliable to match such an image name to the
# label.
label_version = label.name[len(ds_constants.VERSION_PREFIX):]
if '-custom' in label_version:
continue
if label_version.endswith('-cheetsth'):
label_version = label_version[:-len('-cheetsth')]
if label_version != release_builder_path:
logging.warn(
'cros-version label "%s" does not match '
'release_builder_path %s. Removing the label.',
label.name, release_builder_path)
label.remove_hosts(hosts=host_list)
mismatch_found = True
if mismatch_found:
raise error.AutoservError('The host has wrong cros-version label.')
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):
"""Cleanup state on device."""
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, rebooting device.')
# Since restarting the UI fails fall back to normal Autotest
# cleanup routines, i.e. reboot the machine.
super(CrosHost, self).cleanup()
# Check if the rpm outlet was manipulated.
if self.has_power():
self._cleanup_poweron()
self.verify_cros_version_label()
def reboot(self, **dargs):
"""
This function reboots the site host. The more generic
RemoteHost.reboot() performs sync and sleeps for 5
seconds. This is not necessary for Chrome OS devices as the
sync should be finished in a short time during the reboot
command.
"""
if 'reboot_cmd' not in dargs:
reboot_timeout = dargs.get('reboot_timeout', 10)
dargs['reboot_cmd'] = ('sleep 1; '
'reboot & sleep %d; '
'reboot -f' % reboot_timeout)
# Enable fastsync to avoid running extra sync commands before reboot.
if 'fastsync' not in dargs:
dargs['fastsync'] = True
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:
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,
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=0'])
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')
self.verify_cros_version_label()
@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)
self._repair_strategy.verify(self)
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)
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 test_wait_for_sleep(self, sleep_timeout=None):
"""Wait for the client to enter low-power sleep mode.
The test for "is asleep" can't distinguish a system that is
powered off; to confirm that the unit was asleep, it is
necessary to force resume, and then call
`test_wait_for_resume()`.
This function is expected to be called from a test as part
of a sequence like the following:
~~~~~~~~
boot_id = host.get_boot_id()
# trigger sleep on the host
host.test_wait_for_sleep()
# trigger resume on the host
host.test_wait_for_resume(boot_id)
~~~~~~~~
@param sleep_timeout time limit in seconds to allow the host sleep.
@exception TestFail The host did not go to sleep within
the allowed time.
"""
if sleep_timeout is None:
sleep_timeout = self.SLEEP_TIMEOUT
if not self.ping_wait_down(timeout=sleep_timeout):
raise error.TestFail(
'client failed to sleep after %d seconds' % sleep_timeout)
def test_wait_for_resume(self, old_boot_id, resume_timeout=None):
"""Wait for the client to resume from low-power sleep mode.
The `old_boot_id` parameter should be the value from
`get_boot_id()` obtained prior to entering sleep mode. A
`TestFail` exception is raised if the boot id changes.
See @ref test_wait_for_sleep for more on this function's
usage.
@param old_boot_id A boot id value obtained before the
target host went to sleep.
@param resume_timeout time limit in seconds to allow the host up.
@exception TestFail The host did not respond within the
allowed time.
@exception TestFail The host responded, but the boot id test
indicated a reboot rather than a sleep
cycle.
"""
if resume_timeout is None:
resume_timeout = self.RESUME_TIMEOUT
if not self.wait_up(timeout=resume_timeout):
raise error.TestFail(
'client failed to resume from sleep after %d seconds' %
resume_timeout)
else:
new_boot_id = self.get_boot_id()
if new_boot_id != old_boot_id:
logging.error('client rebooted (old boot %s, new boot %s)',
old_boot_id, new_boot_id)
raise error.TestFail(
'client rebooted, but sleep was expected')
def test_wait_for_shutdown(self, shutdown_timeout=None):
"""Wait for the client to shut down.
The test for "has shut down" can't distinguish a system that
is merely asleep; to confirm that the unit was down, it is
necessary to force boot, and then call test_wait_for_boot().
This function is expected to be called from a test as part
of a sequence like the following:
~~~~~~~~
boot_id = host.get_boot_id()
# trigger shutdown on the host
host.test_wait_for_shutdown()
# trigger boot on the host
host.test_wait_for_boot(boot_id)
~~~~~~~~
@param shutdown_timeout time limit in seconds to allow the host down.
@exception TestFail The host did not shut down within the
allowed time.
"""
if shutdown_timeout is None:
shutdown_timeout = self.SHUTDOWN_TIMEOUT
if not self.ping_wait_down(timeout=shutdown_timeout):
raise error.TestFail(
'client failed to shut down after %d seconds' %
shutdown_timeout)
def test_wait_for_boot(self, old_boot_id=None):
"""Wait for the client to boot from cold power.
The `old_boot_id` parameter should be the value from
`get_boot_id()` obtained prior to shutting down. A
`TestFail` exception is raised if the boot id does not
change. The boot id test is omitted if `old_boot_id` is not
specified.
See @ref test_wait_for_shutdown for more on this function's
usage.
@param old_boot_id A boot id value obtained before the
shut down.
@exception TestFail The host did not respond within the
allowed time.
@exception TestFail The host responded, but the boot id test
indicated that there was no reboot.
"""
if not self.wait_up(timeout=self.REBOOT_TIMEOUT):
raise error.TestFail(
'client failed to reboot after %d seconds' %
self.REBOOT_TIMEOUT)
elif old_boot_id:
if self.get_boot_id() == old_boot_id:
logging.error('client not rebooted (boot %s)',
old_boot_id)
raise error.TestFail(
'client is back up, but did not reboot')
@staticmethod
def check_for_rpm_support(hostname):
"""For a given hostname, return whether or not it is powered by an RPM.
@param hostname: hostname to check for rpm support.
@return None if this host does not follows the defined naming format
for RPM powered DUT's in the lab. If it does follow the format,
it returns a regular expression MatchObject instead.
"""
return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname)
def has_power(self):
"""For this host, return whether or not it is powered by an RPM.
@return True if this host is in the CROS lab and follows the defined
naming format.
"""
return CrosHost.check_for_rpm_support(self.hostname)
def _set_power(self, state, power_method):
"""Sets the power to the host via RPM, Servo or manual.
@param state Specifies which power state to set to DUT
@param power_method Specifies which method of power control to
use. By default "RPM" will be used. Valid values
are the strings "RPM", "manual", "servoj10".
"""
ACCEPTABLE_STATES = ['ON', 'OFF']
if state.upper() not in ACCEPTABLE_STATES:
raise error.TestError('State must be one of: %s.'
% (ACCEPTABLE_STATES,))
if power_method == self.POWER_CONTROL_SERVO:
logging.info('Setting servo port J10 to %s', state)
self.servo.set('prtctl3_pwren', state.lower())
time.sleep(self._USB_POWER_TIMEOUT)
elif power_method == self.POWER_CONTROL_MANUAL:
logging.info('You have %d seconds to set the AC power to %s.',
self._POWER_CYCLE_TIMEOUT, state)
time.sleep(self._POWER_CYCLE_TIMEOUT)
else:
if not self.has_power():
raise error.TestFail('DUT does not have RPM connected.')
self._add_rpm_changed_tag()
rpm_client.set_power(self, state.upper(), timeout_mins=5)
def power_off(self, power_method=POWER_CONTROL_RPM):
"""Turn off power to this host via RPM, Servo or manual.
@param power_method Specifies which method of power control to
use. By default "RPM" will be used. Valid values
are the strings "RPM", "manual", "servoj10".
"""
self._set_power('OFF', power_method)
def power_on(self, power_method=POWER_CONTROL_RPM):
"""Turn on power to this host via RPM, Servo or manual.
@param power_method Specifies which method of power control to
use. By default "RPM" will be used. Valid values
are the strings "RPM", "manual", "servoj10".
"""
self._set_power('ON', power_method)
def power_cycle(self, power_method=POWER_CONTROL_RPM):
"""Cycle power to this host by turning it OFF, then ON.
@param power_method Specifies which method of power control to
use. By default "RPM" will be used. Valid values
are the strings "RPM", "manual", "servoj10".
"""
if power_method in (self.POWER_CONTROL_SERVO,
self.POWER_CONTROL_MANUAL):
self.power_off(power_method=power_method)
time.sleep(self._POWER_CYCLE_TIMEOUT)
self.power_on(power_method=power_method)
else:
self._add_rpm_changed_tag()
rpm_client.set_power(self, 'CYCLE')
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)
unibuild = release_info.get('CHROMEOS_RELEASE_UNIBUILD') == '1'
platform = ''
if unibuild:
cmd = 'mosys platform model'
result = self.run(command=cmd, ignore_status=True)
if result.exit_status == 0:
platform = result.stdout.strip()
if not platform:
# Look at the firmware for non-unibuild cases or if mosys 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.
platform = platform.replace('google_', '')
return platform
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 = self.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 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)