Home | History | Annotate | Download | only in hosts
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """
      6 Sonic host.
      7 
      8 This host can perform actions either over ssh or by submitting requests to
      9 an http server running on the client. Though the server provides flexibility
     10 and allows us to test things at a modular level, there are times we must
     11 resort to ssh (eg: to reboot into recovery). The server exposes the same stack
     12 that the chromecast extension needs to communicate with the sonic device, so
     13 any test involving an sonic host will fail if it cannot submit posts/gets
     14 to the server. In cases where we can achieve the same action over ssh or
     15 the rpc server, we choose the rpc server by default, because several existing
     16 sonic tests do the same.
     17 """
     18 
     19 import logging
     20 import os
     21 
     22 import common
     23 
     24 from autotest_lib.client.bin import utils
     25 from autotest_lib.client.common_lib import autotemp
     26 from autotest_lib.client.common_lib import error
     27 from autotest_lib.server import site_utils
     28 from autotest_lib.server.cros import sonic_client_utils
     29 from autotest_lib.server.cros.dynamic_suite import constants
     30 from autotest_lib.server.hosts import abstract_ssh
     31 
     32 
     33 class SonicHost(abstract_ssh.AbstractSSHHost):
     34     """This class represents a sonic host."""
     35 
     36     # Maximum time a reboot can take.
     37     REBOOT_TIME = 360
     38 
     39     COREDUMP_DIR = '/data/coredump'
     40     OTA_LOCATION = '/cache/ota.zip'
     41     RECOVERY_DIR = '/cache/recovery'
     42     COMMAND_FILE = os.path.join(RECOVERY_DIR, 'command')
     43     PLATFORM = 'sonic'
     44     LABELS = [sonic_client_utils.SONIC_BOARD_LABEL]
     45 
     46 
     47     @staticmethod
     48     def check_host(host, timeout=10):
     49         """
     50         Check if the given host is a sonic host.
     51 
     52         @param host: An ssh host representing a device.
     53         @param timeout: The timeout for the run command.
     54 
     55         @return: True if the host device is sonic.
     56 
     57         @raises AutoservRunError: If the command failed.
     58         @raises AutoservSSHTimeout: Ssh connection has timed out.
     59         """
     60         try:
     61             result = host.run('getprop ro.product.device', timeout=timeout)
     62         except (error.AutoservRunError, error.AutoservSSHTimeout,
     63                 error.AutotestHostRunError):
     64             return False
     65         return 'anchovy' in result.stdout
     66 
     67 
     68     def _initialize(self, hostname, *args, **dargs):
     69         super(SonicHost, self)._initialize(hostname=hostname, *args, **dargs)
     70 
     71         # Sonic devices expose a server that can respond to json over http.
     72         self.client = sonic_client_utils.SonicProxy(hostname)
     73 
     74 
     75     def enable_test_extension(self):
     76         """Enable a chromecast test extension on the sonic host.
     77 
     78         Appends the extension id to the list of accepted cast
     79         extensions, without which the sonic device will fail to
     80         respond to any Dial requests submitted by the extension.
     81 
     82         @raises CmdExecutionError: If the expected files are not found
     83             on the sonic host.
     84         """
     85         extension_id = sonic_client_utils.get_extension_id()
     86         tempdir = autotemp.tempdir()
     87         local_dest = os.path.join(tempdir.name, 'content_shell.sh')
     88         remote_src = '/system/usr/bin/content_shell.sh'
     89         whitelist_flag = '--extra-cast-extension-ids'
     90 
     91         try:
     92             self.run('mount -o rw,remount /system')
     93             self.get_file(remote_src, local_dest)
     94             with open(local_dest) as f:
     95                 content = f.read()
     96                 if extension_id in content:
     97                     return
     98                 if whitelist_flag in content:
     99                     append_str = ',%s' % extension_id
    100                 else:
    101                     append_str = ' %s=%s' % (whitelist_flag, extension_id)
    102 
    103             with open(local_dest, 'a') as f:
    104                 f.write(append_str)
    105             self.send_file(local_dest, remote_src)
    106             self.reboot()
    107         finally:
    108             tempdir.clean()
    109 
    110 
    111     def get_boot_id(self, timeout=60):
    112         """Get a unique ID associated with the current boot.
    113 
    114         @param timeout The number of seconds to wait before timing out, as
    115             taken by utils.run.
    116 
    117         @return A string unique to this boot or None if not available.
    118         """
    119         BOOT_ID_FILE = '/proc/sys/kernel/random/boot_id'
    120         cmd = 'cat %r' % (BOOT_ID_FILE)
    121         return self.run(cmd, timeout=timeout).stdout.strip()
    122 
    123 
    124     def get_platform(self):
    125         return self.PLATFORM
    126 
    127 
    128     def get_labels(self):
    129         return self.LABELS
    130 
    131 
    132     def ssh_ping(self, timeout=60, base_cmd=''):
    133         """Checks if we can ssh into the host and run getprop.
    134 
    135         Ssh ping is vital for connectivity checks and waiting on a reboot.
    136         A simple true check, or something like if [ 0 ], is not guaranteed
    137         to always exit with a successful return value.
    138 
    139         @param timeout: timeout in seconds to wait on the ssh_ping.
    140         @param base_cmd: The base command to use to confirm that a round
    141             trip ssh works.
    142         """
    143         super(SonicHost, self).ssh_ping(timeout=timeout,
    144                                          base_cmd="getprop>/dev/null")
    145 
    146 
    147     def verify_software(self):
    148         """Verified that the server on the client device is responding to gets.
    149 
    150         The server on the client device is crucial for the sonic device to
    151         communicate with the chromecast extension. Device verify on the whole
    152         consists of verify_(hardware, connectivity and software), ssh
    153         connectivity is verified in the base class' verify_connectivity.
    154 
    155         @raises: SonicProxyException if the server doesn't respond.
    156         """
    157         self.client.check_server()
    158 
    159 
    160     def get_build_number(self, timeout_mins=1):
    161         """
    162         Gets the build number on the sonic device.
    163 
    164         Since this method is usually called right after a reboot/install,
    165         it has retries built in.
    166 
    167         @param timeout_mins: The timeout in minutes.
    168 
    169         @return: The build number of the build on the host.
    170 
    171         @raises TimeoutError: If we're unable to get the build number within
    172             the specified timeout.
    173         @raises ValueError: If the build number returned isn't an integer.
    174         """
    175         cmd = 'getprop ro.build.version.incremental'
    176         timeout = timeout_mins * 60
    177         cmd_result = utils.poll_for_condition(
    178                         lambda: self.run(cmd, timeout=timeout/10),
    179                         timeout=timeout, sleep_interval=timeout/10)
    180         return int(cmd_result.stdout)
    181 
    182 
    183     def get_kernel_ver(self):
    184         """Returns the build number of the build on the device."""
    185         return self.get_build_number()
    186 
    187 
    188     def reboot(self, timeout=5):
    189         """Reboot the sonic device by submitting a post to the server."""
    190 
    191         # TODO(beeps): crbug.com/318306
    192         current_boot_id = self.get_boot_id()
    193         try:
    194             self.client.reboot()
    195         except sonic_client_utils.SonicProxyException as e:
    196             raise error.AutoservRebootError(
    197                     'Unable to reboot through the sonic proxy: %s' % e)
    198 
    199         self.wait_for_restart(timeout=timeout, old_boot_id=current_boot_id)
    200 
    201 
    202     def cleanup(self):
    203         """Cleanup state.
    204 
    205         If removing state information fails, do a hard reboot. This will hit
    206         our reboot method through the ssh host's cleanup.
    207         """
    208         try:
    209             self.run('rm -r /data/*')
    210             self.run('rm -f /cache/*')
    211         except (error.AutotestRunError, error.AutoservRunError) as e:
    212             logging.warning('Unable to remove /data and /cache %s', e)
    213             super(SonicHost, self).cleanup()
    214 
    215 
    216     def _remount_root(self, permissions):
    217         """Remount root partition.
    218 
    219         @param permissions: Permissions to use for the remount, eg: ro, rw.
    220 
    221         @raises error.AutoservRunError: If something goes wrong in executing
    222             the remount command.
    223         """
    224         self.run('mount -o %s,remount /' % permissions)
    225 
    226 
    227     def _setup_coredump_dirs(self):
    228         """Sets up the /data/coredump directory on the client.
    229 
    230         The device will write a memory dump to this directory on crash,
    231         if it exists. No crashdump will get written if it doesn't.
    232         """
    233         try:
    234             self.run('mkdir -p %s' % self.COREDUMP_DIR)
    235             self.run('chmod 4777 %s' % self.COREDUMP_DIR)
    236         except (error.AutotestRunError, error.AutoservRunError) as e:
    237             error.AutoservRunError('Unable to create coredump directories with '
    238                                    'the appropriate permissions: %s' % e)
    239 
    240 
    241     def _setup_for_recovery(self, update_url):
    242         """Sets up the /cache/recovery directory on the client.
    243 
    244         Copies over the OTA zipfile from the update_url to /cache, then
    245         sets up the recovery directory. Normal installs are achieved
    246         by rebooting into recovery mode.
    247 
    248         @param update_url: A url pointing to a staged ota zip file.
    249 
    250         @raises error.AutoservRunError: If something goes wrong while
    251             executing a command.
    252         """
    253         ssh_cmd = '%s %s' % (self.make_ssh_command(), self.hostname)
    254         site_utils.remote_wget(update_url, self.OTA_LOCATION, ssh_cmd)
    255         self.run('ls %s' % self.OTA_LOCATION)
    256 
    257         self.run('mkdir -p %s' % self.RECOVERY_DIR)
    258 
    259         # These 2 commands will always return a non-zero exit status
    260         # even if they complete successfully. This is a confirmed
    261         # non-issue, since the install will actually complete. If one
    262         # of the commands fails we can only detect it as a failure
    263         # to install the specified build.
    264         self.run('echo --update_package>%s' % self.COMMAND_FILE,
    265                  ignore_status=True)
    266         self.run('echo %s>>%s' % (self.OTA_LOCATION, self.COMMAND_FILE),
    267                  ignore_status=True)
    268 
    269 
    270     def machine_install(self, update_url):
    271         """Installs a build on the Sonic device.
    272 
    273         @returns A tuple of (string of the current build number,
    274                              {'job_repo_url': update_url}).
    275         """
    276         old_build_number = self.get_build_number()
    277         self._remount_root(permissions='rw')
    278         self._setup_coredump_dirs()
    279         self._setup_for_recovery(update_url)
    280 
    281         current_boot_id = self.get_boot_id()
    282         self.run_background('reboot recovery')
    283         self.wait_for_restart(timeout=self.REBOOT_TIME,
    284                               old_boot_id=current_boot_id)
    285         new_build_number = self.get_build_number()
    286 
    287         # TODO(beeps): crbug.com/318278
    288         if new_build_number ==  old_build_number:
    289             raise error.AutoservRunError('Build number did not change on: '
    290                                          '%s after update with %s' %
    291                                          (self.hostname, update_url()))
    292 
    293         return str(new_build_number), {constants.JOB_REPO_URL: update_url}
    294