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