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""" 6Sonic host. 7 8This host can perform actions either over ssh or by submitting requests to 9an http server running on the client. Though the server provides flexibility 10and allows us to test things at a modular level, there are times we must 11resort to ssh (eg: to reboot into recovery). The server exposes the same stack 12that the chromecast extension needs to communicate with the sonic device, so 13any test involving an sonic host will fail if it cannot submit posts/gets 14to the server. In cases where we can achieve the same action over ssh or 15the rpc server, we choose the rpc server by default, because several existing 16sonic tests do the same. 17""" 18 19import logging 20import os 21 22import common 23 24from autotest_lib.client.bin import utils 25from autotest_lib.client.common_lib import autotemp 26from autotest_lib.client.common_lib import error 27from autotest_lib.server import site_utils 28from autotest_lib.server.cros import sonic_client_utils 29from autotest_lib.server.cros.dynamic_suite import constants 30from autotest_lib.server.hosts import abstract_ssh 31 32 33class 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