You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3164 lines
127 KiB
3164 lines
127 KiB
# 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 six
|
|
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(','):
|
|
# IPv6 addresses including a port number should be enclosed in
|
|
# square brackets.
|
|
delimiter = ']:' if re.search(r':.*:', btpeer) else ':'
|
|
result.append({key: value for key,value in
|
|
zip(('btpeer_host','btpeer_port'),
|
|
btpeer.strip('[]').split(delimiter))})
|
|
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
|
|
self.health_profile = None
|
|
self._default_power_method = None
|
|
dut_health_profile = device_health_profile.DeviceHealthProfile(
|
|
hostname=self.hostname,
|
|
host_info=self.host_info_store.get(),
|
|
result_dir=self.get_result_dir())
|
|
|
|
# TODO(otabek@): remove when b/171414073 closed
|
|
pingable_before_servo = self.is_up_fast(count=3)
|
|
if pingable_before_servo:
|
|
logging.info('DUT is pingable before init Servo.')
|
|
_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(),
|
|
dut_health_profile=dut_health_profile)
|
|
if dut_health_profile.is_loaded():
|
|
logging.info('Device health profile loaded.')
|
|
# The device profile is located in the servo_host which make it
|
|
# dependency. If profile is not loaded yet then we do not have it
|
|
# TODO(otabek@) persist device provide out of servo-host.
|
|
self.health_profile = dut_health_profile
|
|
self.set_servo_host(_servo_host, servo_state)
|
|
|
|
# TODO(otabek@): remove when b/171414073 closed
|
|
# Introduced to collect cases when servo made DUT not sshable
|
|
pingable_after_servo = self.is_up_fast(count=3)
|
|
if pingable_after_servo:
|
|
logging.info('DUT is pingable after init Servo.')
|
|
elif pingable_before_servo:
|
|
logging.info('DUT was pingable before init Servo but not now')
|
|
if servo_args and self._servo_host and self._servo_host.hostname:
|
|
# collect stats only for tests.
|
|
dut_ping_servo_init_data = {
|
|
'host': self.hostname,
|
|
'servo_host': self._servo_host.hostname,
|
|
}
|
|
metrics.Counter('chromeos/autotest/dut_ping_servo_init2'
|
|
).increment(fields=dut_ping_servo_init_data)
|
|
|
|
# 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'):
|
|
self.servo._power_state.power_off()
|
|
try:
|
|
self.servo.image_to_servo_usb(image_path=image_url,
|
|
power_off_dut=False)
|
|
except error.AutotestError as e:
|
|
metrics.Counter('chromeos/autotest/repair/image_to_usb_error'
|
|
).increment(
|
|
fields={'host': self.hostname or ''})
|
|
six.reraise(error.AutotestError, str(e), sys.exc_info()[2])
|
|
# Give the DUT some time to power_off if we skip
|
|
# download image to usb. (crbug.com/982993)
|
|
if not image_url:
|
|
time.sleep(10)
|
|
need_snk = self.require_snk_mode_in_recovery()
|
|
self.servo.boot_in_recovery_mode(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')
|
|
|
|
# Make sure the DUT is boot from an external device.
|
|
if not self.is_boot_from_external_device():
|
|
raise hosts.AutoservRepairError(
|
|
'DUT is expected to boot from an external device(e.g. '
|
|
'a usb stick), however it seems still boot from an'
|
|
' internal storage.', 'boot_from_internal_storage')
|
|
|
|
# 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 is_servo_in_working_state(self):
|
|
"""Validate servo is in WORKING state."""
|
|
servo_state = self.get_servo_state()
|
|
return servo_state == servo_constants.SERVO_STATE_WORKING
|
|
|
|
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:
|
|
# TODO(otabek): remove when finish b/174191325
|
|
self._stat_if_pingable_but_not_sshable()
|
|
# 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._reboot_labstation_if_needed()
|
|
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_constants.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 _read_arc_prop_file(self, filename):
|
|
for path in [
|
|
'/usr/share/arcvm/properties/', '/usr/share/arc/properties/'
|
|
]:
|
|
if self.path_exists(path + filename):
|
|
return utils.parse_cmd_output('cat ' + path + filename,
|
|
run_method=self.run)
|
|
return None
|
|
|
|
def _get_arc_build_info(self):
|
|
"""Returns a dictionary mapping build properties to their values."""
|
|
build_info = None
|
|
for filename in ['build.prop', 'vendor_build.prop']:
|
|
properties = self._read_arc_prop_file(filename)
|
|
if properties:
|
|
if build_info:
|
|
build_info.update(properties)
|
|
else:
|
|
build_info = properties
|
|
else:
|
|
logging.error('Failed to find %s in device.', filename)
|
|
return build_info
|
|
|
|
def _get_arc_primary_abi(self):
|
|
"""Returns the primary abi of the host."""
|
|
return self._get_arc_build_info().get('ro.product.cpu.abi')
|
|
|
|
def _get_arc_security_patch(self):
|
|
"""Returns the security patch of the host."""
|
|
return self._get_arc_build_info().get('ro.build.version.security_patch')
|
|
|
|
def get_arc_first_api_level(self):
|
|
"""Returns the security patch of the host."""
|
|
return self._get_arc_build_info().get('ro.product.first_api_level')
|
|
|
|
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):
|
|
"""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.')
|
|
|
|
# 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()
|
|
|
|
|
|
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, 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 or boot_device != main_storage:
|
|
logging.debug('Device booted from external storage storage.')
|
|
return True
|
|
logging.debug('Device booted from main storage.')
|
|
return False
|
|
|
|
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 _dut_fail_ssh_verifier(self):
|
|
"""Check if DUT failed SSH verifier.
|
|
|
|
@returns: bool, True - verifier marked as fail.
|
|
False - result not reachable, verifier did not fail.
|
|
"""
|
|
if not self._repair_strategy:
|
|
return False
|
|
dut_ssh_verifier = self._repair_strategy.verifier_is_good('ssh')
|
|
return dut_ssh_verifier == hosts.VERIFY_FAILED
|
|
|
|
def _stat_if_pingable_but_not_sshable(self):
|
|
"""Check if DUT pingable but failed SSH verifier."""
|
|
if not self._repair_strategy:
|
|
return
|
|
dut_ssh = self._repair_strategy.verifier_is_good('ssh')
|
|
dut_ping = self._repair_strategy.verifier_is_good('ping')
|
|
if (dut_ping == hosts.VERIFY_FAILED
|
|
and dut_ssh == hosts.VERIFY_FAILED):
|
|
metrics.Counter('chromeos/autotest/dut_pingable_no_ssh').increment(
|
|
fields={'host': self.hostname})
|
|
|
|
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():
|
|
return
|
|
if not self._dut_fail_ssh_verifier():
|
|
# DUT is sshable and we still have many options to repair it.
|
|
return
|
|
needs_manual_repair = False
|
|
dhp = self.health_profile
|
|
if dhp and dhp.get_repair_fail_count() > 49:
|
|
# 42 = 6 times during 7 days. (every 4 hour repair)
|
|
# round up to 50 in case somebody will run some attempt on it.
|
|
logging.info(
|
|
'DUT is not sshable and fail %s times.'
|
|
' Limit to try repair is 50 times',
|
|
dhp.get_repair_fail_count())
|
|
needs_manual_repair = True
|
|
|
|
if not needs_manual_repair:
|
|
# We cannot ssh to the DUT and we have hardware or set-up issues
|
|
# with servo then we need request manual repair for the 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.')
|
|
needs_manual_repair = True
|
|
|
|
if needs_manual_repair:
|
|
self.set_device_repair_state(
|
|
cros_constants.DEVICE_STATE_NEEDS_MANUAL_REPAIR)
|
|
|
|
def _reboot_labstation_if_needed(self):
|
|
"""Place request to reboot the labstation if DUT is not sshable.
|
|
|
|
@returns: None
|
|
"""
|
|
message_prefix = "Don't need to request servo-host reboot "
|
|
if not self._dut_fail_ssh_verifier():
|
|
return
|
|
if not self._servo_host:
|
|
logging.debug(message_prefix + 'as it not initialized')
|
|
return
|
|
if not self._servo_host.is_up_fast():
|
|
logging.debug(message_prefix + 'as servo-host is not sshable')
|
|
return
|
|
if not self._servo_host.is_labstation():
|
|
logging.debug('Servo_v3 is not requested to reboot for the DUT')
|
|
return
|
|
usb_path = self._servo_host.get_main_servo_usb_path()
|
|
if usb_path:
|
|
connected_port = os.path.basename(os.path.normpath(usb_path))
|
|
# Directly connected servo to the labstation looks like '1-5.3'
|
|
# and when connected by hub - '1-5.2.3' or '1-5.2.1.3'. Where:
|
|
# - '1-5' - port on labstation
|
|
# - '2' or '2.1' - port on the hub or smart-hub
|
|
# - '3' - port on servo hub
|
|
if len(connected_port.split('.')) > 2:
|
|
logging.debug(message_prefix + 'as servo connected by hub')
|
|
return
|
|
self._servo_host.request_reboot()
|
|
logging.info('Requested labstation reboot because DUT is not sshable')
|
|
|
|
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 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)
|
|
"""
|
|
has_battery = True
|
|
# Determine if the host has battery based on host_info first.
|
|
power_info = self.host_info_store.get().get_label_value('power')
|
|
if power_info:
|
|
has_battery = power_info == 'battery'
|
|
elif self.is_up_fast():
|
|
# when running local tests host_info is not available, so we
|
|
# need to determine whether the host has battery by checking
|
|
# from host side.
|
|
logging.debug('Label `power` is not found in host_info, checking'
|
|
' if the host has battery from host side.')
|
|
has_battery = self.has_battery()
|
|
|
|
if not has_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:
|
|
battery_percent = self.servo.get('battery_charge_percent')
|
|
if battery_percent < cros_constants.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_constants.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)
|