• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
26from autotest_lib.server.hosts import cros_constants
27from autotest_lib.server.hosts import servo_constants
28
29
30_FIRMWARE_UPDATE_TIMEOUT = 600
31# Check battery level with retries.
32# If battery level is low then sleep to 15 minutes.
33_BATTERY_LEVEL_CHECK_RETRIES = 8
34_BATTERY_LEVEL_CHECK_RETRIES_TIMEOUT = 900
35# We expecting that battery will change more than 4% for 15 minutes.
36_BATTERY_LEVEL_CHANGE_IN_ONE_RETRY = 4
37
38
39@contextlib.contextmanager
40def create_cros_host(hostname, board, model, servo_hostname, servo_port,
41                servo_serial=None, logs_dir=None):
42    """Yield a server.hosts.CrosHost object to use for DUT preparation.
43
44    This object contains just enough inventory data to be able to prepare the
45    DUT for lab deployment. It does not contain any reference to AFE / Skylab so
46    that DUT preparation is guaranteed to be isolated from the scheduling
47    infrastructure.
48
49    @param hostname:        FQDN of the host to prepare.
50    @param board:           The autotest board label for the DUT.
51    @param model:           The autotest model label for the DUT.
52    @param servo_hostname:  FQDN of the servo host controlling the DUT.
53    @param servo_port:      Servo host port used for the controlling servo.
54    @param servo_serial:    (Optional) Serial number of the controlling servo.
55    @param logs_dir:        (Optional) Directory to save logs obtained from the
56                            host.
57
58    @yield a server.hosts.Host object.
59    """
60    labels = [
61            'board:%s' % board,
62            'model:%s' % model,
63    ]
64    attributes = {
65            servo_constants.SERVO_HOST_ATTR: servo_hostname,
66            servo_constants.SERVO_PORT_ATTR: servo_port,
67    }
68    if servo_serial is not None:
69        attributes[servo_constants.SERVO_SERIAL_ATTR] = servo_serial
70
71    store = host_info.InMemoryHostInfoStore(info=host_info.HostInfo(
72            labels=labels,
73            attributes=attributes,
74    ))
75    machine_dict = _get_machine_dict(hostname, store)
76    host = hosts.create_host(machine_dict)
77    servohost = servo_host.ServoHost(
78            **servo_host.get_servo_args_for_host(host))
79    _prepare_servo(servohost)
80    host.set_servo_host(servohost)
81    host.servo.uart_logs_dir = logs_dir
82    try:
83        yield host
84    finally:
85        host.close()
86
87
88def _get_machine_dict(hostname, host_info_store):
89    """Helper function to generate a machine_dic to feed hosts.create_host.
90
91    @param hostname
92    @param host_info_store
93
94    @return A dict that hosts.create_host can consume.
95    """
96    return {'hostname': hostname,
97            'host_info_store': host_info_store,
98            'afe_host': server_utils.EmptyAFEHost(),
99            }
100
101
102def download_image_to_servo_usb(host, build):
103    """Download the given image to the USB attached to host's servo.
104
105    @param host   A server.hosts.Host object.
106    @param build  A Chrome OS version string for the build to download.
107    """
108    _, update_url = host.stage_image_for_servo(build)
109    host.servo.image_to_servo_usb(update_url)
110
111
112def try_reset_by_servo(host):
113    """Reboot the DUT by run cold_reset by servo.
114
115    Cold reset implemented as
116    `dut-control -p <SERVO-PORT> power_state:reset`.
117
118    @params host: CrosHost instance with initialized servo instance.
119    """
120    logging.info('Attempting reset via servo...')
121    host.servo.get_power_state_controller().reset()
122
123    logging.info('Waiting for DUT to come back up.')
124    if not host.wait_up(timeout=host.BOOT_TIMEOUT):
125        raise error.AutoservError(
126            'DUT failed to come back after %d seconds' % host.BOOT_TIMEOUT)
127
128
129def power_cycle_via_servo(host, recover_src=False):
130    """Power cycle a host though it's attached servo.
131
132    @param host: A server.hosts.Host object.
133    @param recover_src: Indicate if we need switch servo_v4_role
134           back to src mode.
135    """
136    try:
137        logging.info('Shutting down %s from via ssh.', host.hostname)
138        host.halt()
139    except Exception as e:
140        logging.info('Unable to shutdown DUT via ssh; %s', str(e))
141
142    if recover_src:
143        host.servo.set_servo_v4_role('src')
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_battery_status(host):
167    """Verify that battery status.
168
169    If DUT battery still in the factory mode then DUT required re-work.
170
171    @param host server.hosts.CrosHost object.
172    @raise Exception: if status as unexpected value.
173    """
174    logging.info("Started to verify battery status")
175    host_info = host.host_info_store.get()
176    if host_info.get_label_value('power') != 'battery':
177        logging.info("Skepping due DUT does not have the battery")
178        return
179    power_info = host.get_power_supply_info()
180    battery_path = power_info['Battery']['path']
181    cmd = 'cat %s/status' % battery_path
182    status = host.run(cmd, timeout=30, ignore_status=True).stdout.strip()
183    if status not in ['Charging', 'Discharging', 'Full']:
184        raise Exception(
185                'Unexpected battery status. Please verify that DUT prepared'
186                ' for deployment.')
187
188    # Verify battery level to avoid cases when DUT in factory mode which can
189    # block battery from charging. Retry check will take 8 attempts by
190    # 15 minutes to allow battery to reach required level.
191    battery_level_good = False
192    last_battery_level = 0
193    for _ in range(_BATTERY_LEVEL_CHECK_RETRIES):
194        power_info = host.get_power_supply_info()
195        battery_level = float(power_info['Battery']['percentage'])
196        # Verify if battery reached the required level
197        battery_level_good = battery_level >= cros_constants.MIN_BATTERY_LEVEL
198        if battery_level_good:
199            # Stop retry as battery reached the required level
200            break
201        logging.info(
202                'Battery level %s%% is lower than expected %s%%.'
203                ' Sleep for %s seconds to try again', battery_level,
204                cros_constants.MIN_BATTERY_LEVEL,
205                _BATTERY_LEVEL_CHECK_RETRIES_TIMEOUT)
206        time.sleep(_BATTERY_LEVEL_CHECK_RETRIES_TIMEOUT)
207
208        if last_battery_level > 0:
209            # If level of battery is changing less than 4% per 15 minutes
210            # then we can assume that the battery is not charging as expected
211            # or stuck on some level.
212            battery_level_change = abs(last_battery_level - battery_level)
213            if battery_level_change < _BATTERY_LEVEL_CHANGE_IN_ONE_RETRY:
214                logging.info(
215                        'Battery charged less than 4%% for 15 minutes which'
216                        ' means that something wrong with charging.'
217                        ' Stop retry to charge it. Battery level: %s%%',
218                        battery_level)
219                break
220        last_battery_level = battery_level
221    if not battery_level_good:
222        raise Exception(
223                'Battery is not charged or discharging.'
224                ' Please verify that DUT connected to power and charging.'
225                ' Possible that the DUT is not ready for deployment in lab.')
226    logging.info("Battery status verification passed!")
227
228
229def verify_servo(host):
230    """Verify that we have good Servo.
231
232    The servo_topology and servo_type will be clean up when initiate the
233    deploy process by run add-dut or update-dut.
234    """
235    host_info = host.host_info_store.get()
236    if host_info.os == 'labstation':
237        # skip labstation because they do not have servo
238        return
239    servo_host = host._servo_host
240    if not servo_host:
241        raise Exception('Servo host is not initialized. All DUTs need to have'
242                        ' a stable and working servo.')
243    if host._servo_host.is_servo_topology_supported():
244        servo_topology = host._servo_host.get_topology()
245        if not servo_topology or servo_topology.is_empty():
246            raise Exception(
247                    'Servo topology is not initialized. All DUTs need to have'
248                    ' a stable and working servo.')
249    servo_type = host.servo.get_servo_type()
250    if not servo_type:
251        raise Exception(
252                'The servo_type did not received from Servo. Please verify'
253                ' that Servo is in good state. All DUTs need to have a stable'
254                ' and working servo.')
255    if not host.is_servo_in_working_state():
256        raise Exception(
257                'Servo is not initialized properly or did not passed one or'
258                ' more verifiers. All DUTs need to have a stable and working'
259                ' servo.')
260    host._set_servo_topology()
261    logging.info("Servo initialized and working as expected.")
262
263
264def verify_ccd_testlab_enable(host):
265    """Verify that ccd testlab enable when DUT support cr50.
266
267    The new deploy process required to deploy DUTs with testlab enable when
268    connection to the servo by type-c, so we will be sure that communication
269    by servo is permanent, it's critical for auto-repair capability.
270
271    @param host server.hosts.CrosHost object.
272    """
273
274    host_info = host.host_info_store.get()
275    if host_info.os == 'labstation':
276        # skip labstation because they do not has servo
277        return
278
279    # Only verify for ccd servo connection
280    if host.servo and host.servo.get_main_servo_device() == 'ccd_cr50':
281        if not host.servo.has_control('cr50_testlab'):
282            raise Exception(
283                'CCD connection required support of cr50 on the DUT. Please '
284                'verify which servo need to be used for DUT setup.')
285
286        status = host.servo.get('cr50_testlab')
287        if status == 'on':
288            logging.info("CCD testlab mode is enabled on the DUT.")
289        else:
290            raise Exception(
291                'CCD testlab mode is not enabled on the DUT, enable '
292                'testlab mode is required for all DUTs that support CR50.')
293
294
295def verify_labstation_RPM_config_unsafe(host):
296    """Verify that we can power cycle a labstation with its RPM information.
297    Any host without RPM information will be safely skipped.
298
299    @param host: any host
300
301    This procedure is intended to catch inaccurate RPM info when the
302    host is deployed.
303
304    If the RPM config information is wrong, then this command will fail.
305
306    Note that we do not cleanly stop servod as part of power-cycling the DUT;
307    therefore calling this function is not safe in general.
308
309    """
310    host_info = host.host_info_store.get()
311
312    powerunit_hostname = host_info.attributes.get('powerunit_hostname')
313    powerunit_outlet   = host_info.attributes.get('powerunit_outlet')
314
315    powerunit_hasinfo = (bool(powerunit_hostname), bool(powerunit_outlet))
316
317    if powerunit_hasinfo == (True, True):
318        pass
319    elif powerunit_hasinfo == (False, False):
320        logging.info("intentionally skipping labstation %s", host.hostname)
321        return
322    else:
323        msg = "inconsistent power info: %s %s" % (
324            powerunit_hostname, powerunit_outlet
325        )
326        logging.error(msg)
327        raise Exception(msg)
328
329    logging.info("Shutting down labstation...")
330    host.rpm_power_off_and_wait()
331    host.rpm_power_on_and_wait()
332    logging.info("RPM Check Successful")
333
334
335def verify_boot_into_rec_mode(host):
336    """Verify that we can boot into USB when in recover mode, and reset tpm.
337
338    The new deploy process will install test image before firmware update, so
339    we don't need boot into recovery mode during deploy, but we still want to
340    make sure that DUT can boot into recover mode as it's critical for
341    auto-repair capability.
342
343    @param host   servers.host.Host object.
344    """
345    try:
346        # The DUT could be start with un-sshable state, so do shutdown from
347        # DUT side in a try block.
348        logging.info('Shutting down %s from via ssh.', host.hostname)
349        host.halt()
350    except Exception as e:
351        logging.info('Unable to shutdown DUT via ssh; %s', str(e))
352
353    host.servo.get_power_state_controller().power_off()
354    time.sleep(host.SHUTDOWN_TIMEOUT)
355    logging.info("Booting DUT into recovery mode...")
356    need_snk = host.require_snk_mode_in_recovery()
357    host.servo.boot_in_recovery_mode(snk_mode=need_snk)
358    try:
359        if not host.wait_up(timeout=host.USB_BOOT_TIMEOUT):
360            raise Exception('DUT failed to boot into recovery mode.')
361
362        logging.info('Resetting the TPM status')
363        try:
364            host.run('chromeos-tpm-recovery')
365        except error.AutoservRunError:
366            logging.warn('chromeos-tpm-recovery is too old.')
367    except Exception:
368        # Restore the servo_v4 role to src if we called boot_in_recovery_mode
369        # method with snk_mode=True earlier. If no exception raise, recover
370        # src mode will be handled by power_cycle_via_servo() method.
371        if need_snk:
372            host.servo.set_servo_v4_role('src')
373        raise
374
375    logging.info("Rebooting host into normal mode.")
376    power_cycle_via_servo(host, recover_src=need_snk)
377    logging.info("Verify boot into recovery mode completed successfully.")
378
379
380def install_test_image(host):
381    """Initial install a test image on a DUT.
382
383    This function assumes that the required image is already downloaded onto the
384    USB key connected to the DUT via servo, and the DUT is in dev mode with
385    dev_boot_usb enabled.
386
387    @param host   servers.host.Host object.
388    """
389    servo = host.servo
390    # First power on.  We sleep to allow the firmware plenty of time
391    # to display the dev-mode screen; some boards take their time to
392    # be ready for the ctrl+U after power on.
393    servo.get_power_state_controller().power_off()
394    time.sleep(host.SHUTDOWN_TIMEOUT)
395    servo.switch_usbkey('dut')
396    servo.get_power_state_controller().power_on()
397
398    # Type ctrl+U repeatedly for up to BOOT_TIMEOUT or until DUT boots.
399    boot_deadline = time.time() + host.BOOT_TIMEOUT
400    while time.time() < boot_deadline:
401        logging.info("Pressing ctrl+u")
402        servo.ctrl_u()
403        if host.ping_wait_up(timeout=5):
404            break
405    else:
406        raise Exception('DUT failed to boot from USB for install test image.')
407
408    host.run('chromeos-install --yes', timeout=host.INSTALL_TIMEOUT)
409
410    logging.info("Rebooting DUT to boot from hard drive.")
411    try:
412        host.reboot()
413    except Exception as e:
414        logging.info('Failed to reboot DUT via ssh; %s', str(e))
415        try_reset_by_servo(host)
416    logging.info("Install test image completed successfully.")
417
418
419def reinstall_test_image(host):
420    """Install the test image of given build to DUT.
421
422    This function assumes that the required image is already downloaded onto the
423    USB key connected to the DUT via servo.
424
425    @param host   servers.host.Host object.
426    """
427    host.servo_install()
428
429
430def flash_firmware_using_servo(host, build):
431    """Flash DUT firmware directly using servo.
432
433    Rather than running `chromeos-firmwareupdate` on DUT, we can flash DUT
434    firmware directly using servo (run command `flashrom`, etc. on servo). In
435    this way, we don't require DUT to be in dev mode and with dev_boot_usb
436    enabled."""
437    host.firmware_install(build)
438
439
440def install_firmware(host):
441    """Install dev-signed firmware after removing write-protect.
442
443    At start, it's assumed that hardware write-protect is disabled,
444    the DUT is in dev mode, and the servo's USB stick already has a
445    test image installed.
446
447    The firmware is installed by powering on and typing ctrl+U on
448    the keyboard in order to boot the test image from USB.  Once
449    the DUT is booted, we run a series of commands to install the
450    read-only firmware from the test image.  Then we clear debug
451    mode, and shut down.
452
453    @param host   Host instance to use for servo and ssh operations.
454    """
455    logging.info("Started install firmware on the DUT.")
456    # Disable software-controlled write-protect for both FPROMs, and
457    # install the RO firmware.
458    for fprom in ['host', 'ec']:
459        host.run('flashrom -p %s --wp-disable' % fprom,
460                 ignore_status=True)
461
462    fw_update_log = '/mnt/stateful_partition/home/root/cros-fw-update.log'
463    pid = _start_firmware_update(host, fw_update_log)
464    _wait_firmware_update_process(host, pid)
465    _check_firmware_update_result(host, fw_update_log)
466
467    try:
468        host.reboot()
469    except Exception as e:
470        logging.debug('Failed to reboot the DUT after update firmware; %s', e)
471        try_reset_by_servo(host)
472
473    # Once we confirmed DUT can boot from new firmware, get us out of
474    # dev-mode and clear GBB flags.  GBB flags are non-zero because
475    # boot from USB was enabled.
476    logging.info("Resting gbb flags and disable dev mode.")
477    host.run('/usr/share/vboot/bin/set_gbb_flags.sh 0',
478             ignore_status=True)
479    host.run('crossystem disable_dev_request=1',
480             ignore_status=True)
481
482    logging.info("Rebooting DUT in normal mode(non-dev).")
483    try:
484        host.reboot()
485    except Exception as e:
486        logging.debug(
487                'Failed to reboot the DUT after switch to'
488                ' non-dev mode; %s', e)
489        try_reset_by_servo(host)
490    logging.info("Install firmware completed successfully.")
491
492
493def _start_firmware_update(host, result_file):
494    """Run `chromeos-firmwareupdate` in background.
495
496    In scenario servo v4 type C, some boards of DUT may lose ethernet
497    connectivity on firmware update. There's no way to bring it back except
498    rebooting the system.
499
500    @param host         Host instance to use for servo and ssh operations.
501    @param result_file  Path on DUT to save operation logs.
502
503    @returns The process id."""
504    # TODO(guocb): Use `make_dev_firmware` to re-sign from MP to test/dev.
505    fw_update_cmd = 'chromeos-firmwareupdate --mode=factory --force'
506
507    cmd = [
508        "date > %s" % result_file,
509        "nohup %s &>> %s" % (fw_update_cmd, result_file),
510        "/usr/local/bin/hooks/check_ethernet.hook"
511    ]
512    return host.run_background(';'.join(cmd))
513
514
515def _wait_firmware_update_process(host, pid, timeout=_FIRMWARE_UPDATE_TIMEOUT):
516    """Wait `chromeos-firmwareupdate` to finish.
517
518    @param host     Host instance to use for servo and ssh operations.
519    @param pid      The process ID of `chromeos-firmwareupdate`.
520    @param timeout  Maximum time to wait for firmware updating.
521    """
522    try:
523        utils.poll_for_condition(
524            lambda: host.run('ps -f -p %s' % pid, timeout=20).exit_status,
525            exception=Exception(
526                    "chromeos-firmwareupdate (pid: %s) didn't complete in %s "
527                    'seconds.' % (pid, timeout)),
528            timeout=_FIRMWARE_UPDATE_TIMEOUT,
529            sleep_interval=10,
530        )
531    except error.AutoservRunError:
532        # We lose the connectivity, so the DUT should be booting up.
533        if not host.wait_up(timeout=host.USB_BOOT_TIMEOUT):
534            raise Exception(
535                    'DUT failed to boot up after firmware updating.')
536
537
538def _check_firmware_update_result(host, result_file):
539    """Check if firmware updating is good or not.
540
541    @param host         Host instance to use for servo and ssh operations.
542    @param result_file  Path of the file saving output of
543                        `chromeos-firmwareupdate`.
544    """
545    fw_update_was_good = ">> DONE: Firmware updater exits successfully."
546    result = host.run('cat %s' % result_file)
547    if result.stdout.rstrip().rsplit('\n', 1)[1] != fw_update_was_good:
548        raise Exception("chromeos-firmwareupdate failed!")
549
550
551def _prepare_servo(servohost):
552    """Prepare servo connected to host for installation steps.
553
554    @param servohost  A server.hosts.servo_host.ServoHost object.
555    """
556    # Stopping `servod` on the servo host will force `repair()` to
557    # restart it.  We want that restart for a few reasons:
558    #   + `servod` caches knowledge about the image on the USB stick.
559    #     We want to clear the cache to force the USB stick to be
560    #     re-imaged unconditionally.
561    #   + If there's a problem with servod that verify and repair
562    #     can't find, this provides a UI through which `servod` can
563    #     be restarted.
564    servohost.run('stop servod PORT=%d' % servohost.servo_port,
565                  ignore_status=True)
566    servohost.repair()
567
568    if not servohost.get_servo().probe_host_usb_dev():
569        raise Exception('No USB stick detected on Servo host')
570
571
572def setup_hwid_and_serialnumber(host):
573    """Do initial setup for ChromeOS host.
574
575    @param host    servers.host.Host object.
576    """
577    if not hasattr(host, 'host_info_store'):
578        raise Exception('%s does not have host_info_store' % host.hostname)
579
580    info = host.host_info_store.get()
581    hwid = host.run('crossystem hwid', ignore_status=True).stdout
582    serial_number = host.run('vpd -g serial_number', ignore_status=True).stdout
583
584    if not hwid and not serial_number:
585        raise Exception(
586                'Failed to retrieve HWID and SerialNumber from host %s' %
587                host.hostname)
588    if not serial_number:
589        raise Exception('Failed to retrieve SerialNumber from host %s' %
590                        host.hostname)
591    if not hwid:
592        raise Exception('Failed to retrieve HWID from host %s' % host.hostname)
593
594    info.attributes['HWID'] = hwid
595    info.attributes['serial_number'] = serial_number
596    if info != host.host_info_store.get():
597        host.host_info_store.commit(info)
598    logging.info("Reading HWID and SerialNumber completed successfully.")
599