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