# 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}