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