293 lines
11 KiB
Python
293 lines
11 KiB
Python
# Copyright (c) 2013 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.
|
|
|
|
"""
|
|
Sonic host.
|
|
|
|
This host can perform actions either over ssh or by submitting requests to
|
|
an http server running on the client. Though the server provides flexibility
|
|
and allows us to test things at a modular level, there are times we must
|
|
resort to ssh (eg: to reboot into recovery). The server exposes the same stack
|
|
that the chromecast extension needs to communicate with the sonic device, so
|
|
any test involving an sonic host will fail if it cannot submit posts/gets
|
|
to the server. In cases where we can achieve the same action over ssh or
|
|
the rpc server, we choose the rpc server by default, because several existing
|
|
sonic tests do the same.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
|
|
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.server import site_utils
|
|
from autotest_lib.server.cros import sonic_client_utils
|
|
from autotest_lib.server.cros.dynamic_suite import constants
|
|
from autotest_lib.server.hosts import abstract_ssh
|
|
|
|
|
|
class SonicHost(abstract_ssh.AbstractSSHHost):
|
|
"""This class represents a sonic host."""
|
|
|
|
# Maximum time a reboot can take.
|
|
REBOOT_TIME = 360
|
|
|
|
COREDUMP_DIR = '/data/coredump'
|
|
OTA_LOCATION = '/cache/ota.zip'
|
|
RECOVERY_DIR = '/cache/recovery'
|
|
COMMAND_FILE = os.path.join(RECOVERY_DIR, 'command')
|
|
PLATFORM = 'sonic'
|
|
LABELS = [sonic_client_utils.SONIC_BOARD_LABEL]
|
|
|
|
|
|
@staticmethod
|
|
def check_host(host, timeout=10):
|
|
"""
|
|
Check if the given host is a sonic host.
|
|
|
|
@param host: An ssh host representing a device.
|
|
@param timeout: The timeout for the run command.
|
|
|
|
@return: True if the host device is sonic.
|
|
|
|
@raises AutoservRunError: If the command failed.
|
|
@raises AutoservSSHTimeout: Ssh connection has timed out.
|
|
"""
|
|
try:
|
|
result = host.run('getprop ro.product.device', timeout=timeout)
|
|
except (error.AutoservRunError, error.AutoservSSHTimeout,
|
|
error.AutotestHostRunError):
|
|
return False
|
|
return 'anchovy' in result.stdout
|
|
|
|
|
|
def _initialize(self, hostname, *args, **dargs):
|
|
super(SonicHost, self)._initialize(hostname=hostname, *args, **dargs)
|
|
|
|
# Sonic devices expose a server that can respond to json over http.
|
|
self.client = sonic_client_utils.SonicProxy(hostname)
|
|
|
|
|
|
def enable_test_extension(self):
|
|
"""Enable a chromecast test extension on the sonic host.
|
|
|
|
Appends the extension id to the list of accepted cast
|
|
extensions, without which the sonic device will fail to
|
|
respond to any Dial requests submitted by the extension.
|
|
|
|
@raises CmdExecutionError: If the expected files are not found
|
|
on the sonic host.
|
|
"""
|
|
extension_id = sonic_client_utils.get_extension_id()
|
|
tempdir = autotemp.tempdir()
|
|
local_dest = os.path.join(tempdir.name, 'content_shell.sh')
|
|
remote_src = '/system/usr/bin/content_shell.sh'
|
|
whitelist_flag = '--extra-cast-extension-ids'
|
|
|
|
try:
|
|
self.run('mount -o rw,remount /system')
|
|
self.get_file(remote_src, local_dest)
|
|
with open(local_dest) as f:
|
|
content = f.read()
|
|
if extension_id in content:
|
|
return
|
|
if whitelist_flag in content:
|
|
append_str = ',%s' % extension_id
|
|
else:
|
|
append_str = ' %s=%s' % (whitelist_flag, extension_id)
|
|
|
|
with open(local_dest, 'a') as f:
|
|
f.write(append_str)
|
|
self.send_file(local_dest, remote_src)
|
|
self.reboot()
|
|
finally:
|
|
tempdir.clean()
|
|
|
|
|
|
def get_boot_id(self, timeout=60):
|
|
"""Get a unique ID associated with the current boot.
|
|
|
|
@param timeout The number of seconds to wait before timing out, as
|
|
taken by utils.run.
|
|
|
|
@return A string unique to this boot or None if not available.
|
|
"""
|
|
BOOT_ID_FILE = '/proc/sys/kernel/random/boot_id'
|
|
cmd = 'cat %r' % (BOOT_ID_FILE)
|
|
return self.run(cmd, timeout=timeout).stdout.strip()
|
|
|
|
|
|
def get_platform(self):
|
|
return self.PLATFORM
|
|
|
|
|
|
def get_labels(self):
|
|
return self.LABELS
|
|
|
|
|
|
def ssh_ping(self, timeout=60, base_cmd=''):
|
|
"""Checks if we can ssh into the host and run getprop.
|
|
|
|
Ssh ping is vital for connectivity checks and waiting on a reboot.
|
|
A simple true check, or something like if [ 0 ], is not guaranteed
|
|
to always exit with a successful return value.
|
|
|
|
@param timeout: timeout in seconds to wait on the ssh_ping.
|
|
@param base_cmd: The base command to use to confirm that a round
|
|
trip ssh works.
|
|
"""
|
|
super(SonicHost, self).ssh_ping(timeout=timeout,
|
|
base_cmd="getprop>/dev/null")
|
|
|
|
|
|
def verify_software(self):
|
|
"""Verified that the server on the client device is responding to gets.
|
|
|
|
The server on the client device is crucial for the sonic device to
|
|
communicate with the chromecast extension. Device verify on the whole
|
|
consists of verify_(hardware, connectivity and software), ssh
|
|
connectivity is verified in the base class' verify_connectivity.
|
|
|
|
@raises: SonicProxyException if the server doesn't respond.
|
|
"""
|
|
self.client.check_server()
|
|
|
|
|
|
def get_build_number(self, timeout_mins=1):
|
|
"""
|
|
Gets the build number on the sonic device.
|
|
|
|
Since this method is usually called right after a reboot/install,
|
|
it has retries built in.
|
|
|
|
@param timeout_mins: The timeout in minutes.
|
|
|
|
@return: The build number of the build on the host.
|
|
|
|
@raises TimeoutError: If we're unable to get the build number within
|
|
the specified timeout.
|
|
@raises ValueError: If the build number returned isn't an integer.
|
|
"""
|
|
cmd = 'getprop ro.build.version.incremental'
|
|
timeout = timeout_mins * 60
|
|
cmd_result = utils.poll_for_condition(
|
|
lambda: self.run(cmd, timeout=timeout/10),
|
|
timeout=timeout, sleep_interval=timeout/10)
|
|
return int(cmd_result.stdout)
|
|
|
|
|
|
def get_kernel_ver(self):
|
|
"""Returns the build number of the build on the device."""
|
|
return self.get_build_number()
|
|
|
|
|
|
def reboot(self, timeout=5):
|
|
"""Reboot the sonic device by submitting a post to the server."""
|
|
|
|
# TODO(beeps): crbug.com/318306
|
|
current_boot_id = self.get_boot_id()
|
|
try:
|
|
self.client.reboot()
|
|
except sonic_client_utils.SonicProxyException as e:
|
|
raise error.AutoservRebootError(
|
|
'Unable to reboot through the sonic proxy: %s' % e)
|
|
|
|
self.wait_for_restart(timeout=timeout, old_boot_id=current_boot_id)
|
|
|
|
|
|
def cleanup(self):
|
|
"""Cleanup state.
|
|
|
|
If removing state information fails, do a hard reboot. This will hit
|
|
our reboot method through the ssh host's cleanup.
|
|
"""
|
|
try:
|
|
self.run('rm -r /data/*')
|
|
self.run('rm -f /cache/*')
|
|
except (error.AutotestRunError, error.AutoservRunError) as e:
|
|
logging.warning('Unable to remove /data and /cache %s', e)
|
|
super(SonicHost, self).cleanup()
|
|
|
|
|
|
def _remount_root(self, permissions):
|
|
"""Remount root partition.
|
|
|
|
@param permissions: Permissions to use for the remount, eg: ro, rw.
|
|
|
|
@raises error.AutoservRunError: If something goes wrong in executing
|
|
the remount command.
|
|
"""
|
|
self.run('mount -o %s,remount /' % permissions)
|
|
|
|
|
|
def _setup_coredump_dirs(self):
|
|
"""Sets up the /data/coredump directory on the client.
|
|
|
|
The device will write a memory dump to this directory on crash,
|
|
if it exists. No crashdump will get written if it doesn't.
|
|
"""
|
|
try:
|
|
self.run('mkdir -p %s' % self.COREDUMP_DIR)
|
|
self.run('chmod 4777 %s' % self.COREDUMP_DIR)
|
|
except (error.AutotestRunError, error.AutoservRunError) as e:
|
|
error.AutoservRunError('Unable to create coredump directories with '
|
|
'the appropriate permissions: %s' % e)
|
|
|
|
|
|
def _setup_for_recovery(self, update_url):
|
|
"""Sets up the /cache/recovery directory on the client.
|
|
|
|
Copies over the OTA zipfile from the update_url to /cache, then
|
|
sets up the recovery directory. Normal installs are achieved
|
|
by rebooting into recovery mode.
|
|
|
|
@param update_url: A url pointing to a staged ota zip file.
|
|
|
|
@raises error.AutoservRunError: If something goes wrong while
|
|
executing a command.
|
|
"""
|
|
ssh_cmd = '%s %s' % (self.make_ssh_command(), self.hostname)
|
|
site_utils.remote_wget(update_url, self.OTA_LOCATION, ssh_cmd)
|
|
self.run('ls %s' % self.OTA_LOCATION)
|
|
|
|
self.run('mkdir -p %s' % self.RECOVERY_DIR)
|
|
|
|
# These 2 commands will always return a non-zero exit status
|
|
# even if they complete successfully. This is a confirmed
|
|
# non-issue, since the install will actually complete. If one
|
|
# of the commands fails we can only detect it as a failure
|
|
# to install the specified build.
|
|
self.run('echo --update_package>%s' % self.COMMAND_FILE,
|
|
ignore_status=True)
|
|
self.run('echo %s>>%s' % (self.OTA_LOCATION, self.COMMAND_FILE),
|
|
ignore_status=True)
|
|
|
|
|
|
def machine_install(self, update_url):
|
|
"""Installs a build on the Sonic device.
|
|
|
|
@returns A tuple of (string of the current build number,
|
|
{'job_repo_url': update_url}).
|
|
"""
|
|
old_build_number = self.get_build_number()
|
|
self._remount_root(permissions='rw')
|
|
self._setup_coredump_dirs()
|
|
self._setup_for_recovery(update_url)
|
|
|
|
current_boot_id = self.get_boot_id()
|
|
self.run_background('reboot recovery')
|
|
self.wait_for_restart(timeout=self.REBOOT_TIME,
|
|
old_boot_id=current_boot_id)
|
|
new_build_number = self.get_build_number()
|
|
|
|
# TODO(beeps): crbug.com/318278
|
|
if new_build_number == old_build_number:
|
|
raise error.AutoservRunError('Build number did not change on: '
|
|
'%s after update with %s' %
|
|
(self.hostname, update_url()))
|
|
|
|
return str(new_build_number), {constants.JOB_REPO_URL: update_url}
|