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