1#!/usr/bin/env python2 2# Copyright 2019 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""library functions to prepare a DUT for lab deployment. 7 8This library will be shared between Autotest and Skylab DUT deployment tools. 9""" 10 11from __future__ import absolute_import 12from __future__ import division 13from __future__ import print_function 14 15import contextlib 16import time 17 18import common 19import logging 20from autotest_lib.client.common_lib import error 21from autotest_lib.client.common_lib import utils 22from autotest_lib.server import hosts 23from autotest_lib.server import site_utils as server_utils 24from autotest_lib.server.hosts import host_info 25from autotest_lib.server.hosts import servo_host 26 27 28_FIRMWARE_UPDATE_TIMEOUT = 600 29 30 31@contextlib.contextmanager 32def create_cros_host(hostname, board, model, servo_hostname, servo_port, 33 servo_serial=None, logs_dir=None): 34 """Yield a server.hosts.CrosHost object to use for DUT preparation. 35 36 This object contains just enough inventory data to be able to prepare the 37 DUT for lab deployment. It does not contain any reference to AFE / Skylab so 38 that DUT preparation is guaranteed to be isolated from the scheduling 39 infrastructure. 40 41 @param hostname: FQDN of the host to prepare. 42 @param board: The autotest board label for the DUT. 43 @param model: The autotest model label for the DUT. 44 @param servo_hostname: FQDN of the servo host controlling the DUT. 45 @param servo_port: Servo host port used for the controlling servo. 46 @param servo_serial: (Optional) Serial number of the controlling servo. 47 @param logs_dir: (Optional) Directory to save logs obtained from the 48 host. 49 50 @yield a server.hosts.Host object. 51 """ 52 labels = [ 53 'board:%s' % board, 54 'model:%s' % model, 55 ] 56 attributes = { 57 servo_host.SERVO_HOST_ATTR: servo_hostname, 58 servo_host.SERVO_PORT_ATTR: servo_port, 59 } 60 if servo_serial is not None: 61 attributes[servo_host.SERVO_SERIAL_ATTR] = servo_serial 62 63 store = host_info.InMemoryHostInfoStore(info=host_info.HostInfo( 64 labels=labels, 65 attributes=attributes, 66 )) 67 machine_dict = _get_machine_dict(hostname, store) 68 host = hosts.create_host(machine_dict) 69 servohost = servo_host.ServoHost( 70 **servo_host.get_servo_args_for_host(host)) 71 _prepare_servo(servohost) 72 host.set_servo_host(servohost) 73 host.servo.uart_logs_dir = logs_dir 74 try: 75 yield host 76 finally: 77 host.close() 78 79 80@contextlib.contextmanager 81def create_labstation_host(hostname, board, model): 82 """Yield a server.hosts.LabstationHost object to use for labstation 83 preparation. 84 85 This object contains just enough inventory data to be able to prepare the 86 labstation for lab deployment. It does not contain any reference to 87 AFE / Skylab so that DUT preparation is guaranteed to be isolated from 88 the scheduling infrastructure. 89 90 @param hostname: FQDN of the host to prepare. 91 @param board: The autotest board label for the DUT. 92 @param model: The autotest model label for the DUT. 93 94 @yield a server.hosts.Host object. 95 """ 96 labels = [ 97 'board:%s' % board, 98 'model:%s' % model, 99 'os:labstation' 100 ] 101 102 store = host_info.InMemoryHostInfoStore(info=host_info.HostInfo( 103 labels=labels, 104 )) 105 machine_dict = _get_machine_dict(hostname, store) 106 host = hosts.create_host(machine_dict) 107 try: 108 yield host 109 finally: 110 host.close() 111 112 113def _get_machine_dict(hostname, host_info_store): 114 """Helper function to generate a machine_dic to feed hosts.create_host. 115 116 @param hostname 117 @param host_info_store 118 119 @return A dict that hosts.create_host can consume. 120 """ 121 return {'hostname': hostname, 122 'host_info_store': host_info_store, 123 'afe_host': server_utils.EmptyAFEHost(), 124 } 125 126 127def download_image_to_servo_usb(host, build): 128 """Download the given image to the USB attached to host's servo. 129 130 @param host A server.hosts.Host object. 131 @param build A Chrome OS version string for the build to download. 132 """ 133 _, update_url = host.stage_image_for_servo(build) 134 host.servo.image_to_servo_usb(update_url) 135 136 137def power_cycle_via_servo(host): 138 """Power cycle a host though it's attached servo. 139 140 @param host A server.hosts.Host object. 141 """ 142 logging.info("Shutting down the host...") 143 host.halt() 144 145 logging.info('Power cycling DUT through servo...') 146 host.servo.get_power_state_controller().power_off() 147 host.servo.switch_usbkey('off') 148 time.sleep(host.SHUTDOWN_TIMEOUT) 149 # N.B. The Servo API requires that we use power_on() here 150 # for two reasons: 151 # 1) After turning on a DUT in recovery mode, you must turn 152 # it off and then on with power_on() once more to 153 # disable recovery mode (this is a Parrot specific 154 # requirement). 155 # 2) After power_off(), the only way to turn on is with 156 # power_on() (this is a Storm specific requirement). 157 time.sleep(host.SHUTDOWN_TIMEOUT) 158 host.servo.get_power_state_controller().power_on() 159 160 logging.info('Waiting for DUT to come back up.') 161 if not host.wait_up(timeout=host.BOOT_TIMEOUT): 162 raise error.AutoservError('DUT failed to come back after %d seconds' % 163 host.BOOT_TIMEOUT) 164 165 166def verify_boot_into_rec_mode(host): 167 """Verify that we can boot into USB when in recover mode, and reset tpm. 168 169 The new deploy process will install test image before firmware update, so 170 we don't need boot into recovery mode during deploy, but we still want to 171 make sure that DUT can boot into recover mode as it's critical for 172 auto-repair capability. 173 174 @param host servers.host.Host object. 175 """ 176 logging.info("Shutting down DUT...") 177 host.halt() 178 host.servo.get_power_state_controller().power_off() 179 time.sleep(host.SHUTDOWN_TIMEOUT) 180 logging.info("Booting DUT into recovery mode...") 181 host.servo.boot_in_recovery_mode() 182 183 if not host.wait_up(timeout=host.USB_BOOT_TIMEOUT): 184 raise Exception('DUT failed to boot into recovery mode.') 185 186 logging.info('Resetting the TPM status') 187 try: 188 host.run('chromeos-tpm-recovery') 189 except error.AutoservRunError: 190 logging.warn('chromeos-tpm-recovery is too old.') 191 192 logging.info("Rebooting host into normal mode.") 193 power_cycle_via_servo(host) 194 logging.info("Verify boot into recovery mode completed successfully.") 195 196 197def install_test_image(host): 198 """Initial install a test image on a DUT. 199 200 This function assumes that the required image is already downloaded onto the 201 USB key connected to the DUT via servo, and the DUT is in dev mode with 202 dev_boot_usb enabled. 203 204 @param host servers.host.Host object. 205 """ 206 servo = host.servo 207 # First power on. We sleep to allow the firmware plenty of time 208 # to display the dev-mode screen; some boards take their time to 209 # be ready for the ctrl+U after power on. 210 servo.get_power_state_controller().power_off() 211 time.sleep(host.SHUTDOWN_TIMEOUT) 212 servo.switch_usbkey('dut') 213 servo.get_power_state_controller().power_on() 214 215 # Dev mode screen should be up now: type ctrl+U and wait for 216 # boot from USB to finish. 217 time.sleep(10) 218 servo.ctrl_u() 219 220 if not host.wait_up(timeout=host.USB_BOOT_TIMEOUT): 221 raise Exception('DUT failed to boot from USB for install test image.') 222 223 host.run('chromeos-install --yes', timeout=host.INSTALL_TIMEOUT) 224 225 logging.info("Rebooting DUT to boot from hard drive.") 226 power_cycle_via_servo(host) 227 logging.info("Install test image completed successfully.") 228 229 230def reinstall_test_image(host): 231 """Install the test image of given build to DUT. 232 233 This function assumes that the required image is already downloaded onto the 234 USB key connected to the DUT via servo. 235 236 @param host servers.host.Host object. 237 """ 238 host.servo_install() 239 240 241def flash_firmware_using_servo(host, build): 242 """Flash DUT firmware directly using servo. 243 244 Rather than running `chromeos-firmwareupdate` on DUT, we can flash DUT 245 firmware directly using servo (run command `flashrom`, etc. on servo). In 246 this way, we don't require DUT to be in dev mode and with dev_boot_usb 247 enabled.""" 248 host.firmware_install(build) 249 250 251def install_firmware(host): 252 """Install dev-signed firmware after removing write-protect. 253 254 At start, it's assumed that hardware write-protect is disabled, 255 the DUT is in dev mode, and the servo's USB stick already has a 256 test image installed. 257 258 The firmware is installed by powering on and typing ctrl+U on 259 the keyboard in order to boot the test image from USB. Once 260 the DUT is booted, we run a series of commands to install the 261 read-only firmware from the test image. Then we clear debug 262 mode, and shut down. 263 264 @param host Host instance to use for servo and ssh operations. 265 """ 266 # Disable software-controlled write-protect for both FPROMs, and 267 # install the RO firmware. 268 for fprom in ['host', 'ec']: 269 host.run('flashrom -p %s --wp-disable' % fprom, 270 ignore_status=True) 271 272 fw_update_log = '/mnt/stateful_partition/home/root/cros-fw-update.log' 273 pid = _start_firmware_update(host, fw_update_log) 274 _wait_firmware_update_process(host, pid) 275 _check_firmware_update_result(host, fw_update_log) 276 277 # Get us out of dev-mode and clear GBB flags. GBB flags are 278 # non-zero because boot from USB was enabled. 279 logging.info("Resting gbb flags and disable dev mode.") 280 host.run('/usr/share/vboot/bin/set_gbb_flags.sh 0', 281 ignore_status=True) 282 host.run('crossystem disable_dev_request=1', 283 ignore_status=True) 284 285 logging.info("Rebooting DUT in normal mode(non-dev).") 286 power_cycle_via_servo(host) 287 logging.info("Install firmware completed successfully.") 288 289 290 291def _start_firmware_update(host, result_file): 292 """Run `chromeos-firmwareupdate` in background. 293 294 In scenario servo v4 type C, some boards of DUT may lose ethernet 295 connectivity on firmware update. There's no way to bring it back except 296 rebooting the system. 297 298 @param host Host instance to use for servo and ssh operations. 299 @param result_file Path on DUT to save operation logs. 300 301 @returns The process id.""" 302 # TODO(guocb): Use `make_dev_firmware` to re-sign from MP to test/dev. 303 fw_update_cmd = 'chromeos-firmwareupdate --mode=factory --force' 304 305 cmd = [ 306 "date > %s" % result_file, 307 "nohup %s &>> %s" % (fw_update_cmd, result_file), 308 "/usr/local/bin/hooks/check_ethernet.hook" 309 ] 310 return host.run_background(';'.join(cmd)) 311 312 313def _wait_firmware_update_process(host, pid, timeout=_FIRMWARE_UPDATE_TIMEOUT): 314 """Wait `chromeos-firmwareupdate` to finish. 315 316 @param host Host instance to use for servo and ssh operations. 317 @param pid The process ID of `chromeos-firmwareupdate`. 318 @param timeout Maximum time to wait for firmware updating. 319 """ 320 try: 321 utils.poll_for_condition( 322 lambda: host.run('ps -f -p %s' % pid, timeout=20).exit_status, 323 exception=Exception( 324 "chromeos-firmwareupdate (pid: %s) didn't complete in %s " 325 'seconds.' % (pid, timeout)), 326 timeout=_FIRMWARE_UPDATE_TIMEOUT, 327 sleep_interval=10, 328 ) 329 except error.AutoservRunError: 330 # We lose the connectivity, so the DUT should be booting up. 331 if not host.wait_up(timeout=host.USB_BOOT_TIMEOUT): 332 raise Exception( 333 'DUT failed to boot up after firmware updating.') 334 335 336def _check_firmware_update_result(host, result_file): 337 """Check if firmware updating is good or not. 338 339 @param host Host instance to use for servo and ssh operations. 340 @param result_file Path of the file saving output of 341 `chromeos-firmwareupdate`. 342 """ 343 fw_update_was_good = ">> DONE: Firmware updater exits successfully." 344 result = host.run('cat %s' % result_file) 345 if result.stdout.rstrip().rsplit('\n', 1)[1] != fw_update_was_good: 346 raise Exception("chromeos-firmwareupdate failed!") 347 348 349def _prepare_servo(servohost): 350 """Prepare servo connected to host for installation steps. 351 352 @param servohost A server.hosts.servo_host.ServoHost object. 353 """ 354 # Stopping `servod` on the servo host will force `repair()` to 355 # restart it. We want that restart for a few reasons: 356 # + `servod` caches knowledge about the image on the USB stick. 357 # We want to clear the cache to force the USB stick to be 358 # re-imaged unconditionally. 359 # + If there's a problem with servod that verify and repair 360 # can't find, this provides a UI through which `servod` can 361 # be restarted. 362 servohost.run('stop servod PORT=%d' % servohost.servo_port, 363 ignore_status=True) 364 servohost.repair() 365 366 # Don't timeout probing for the host usb device, there could be a bunch 367 # of servos probing at the same time on the same servo host. And 368 # since we can't pass None through the xml rpcs, use 0 to indicate None. 369 if not servohost.get_servo().probe_host_usb_dev(timeout=0): 370 raise Exception('No USB stick detected on Servo host') 371 372 373def setup_labstation(host): 374 """Do initial setup for labstation host. 375 376 @param host A LabstationHost object. 377 378 """ 379 try: 380 if not host.is_labstation(): 381 raise Exception('Current OS on host %s is not a labstation image.' 382 % host.hostname) 383 except AttributeError: 384 raise Exception('Unable to verify host has a labstation image, this can' 385 ' be caused by host is unsshable.') 386 387 try: 388 # TODO: we should setup hwid and serial number for DUT in deploy script 389 # as well, which is currently obtained from repair job. 390 info = host.host_info_store.get() 391 hwid = host.run('crossystem hwid', ignore_status=True).stdout 392 if hwid: 393 info.attributes['HWID'] = hwid 394 395 serial_number = host.run('vpd -g serial_number', 396 ignore_status=True).stdout 397 if serial_number: 398 info.attributes['serial_number'] = serial_number 399 if info != host.host_info_store.get(): 400 host.host_info_store.commit(info) 401 except Exception as e: 402 raise Exception('Failed to get HWID & Serial Number for host %s: %s' 403 % (host.hostname, str(e))) 404