• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2# Copyright 2016 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 containing functions to execute auto-update on a remote device.
7
8ChromiumOSUpdater includes:
9  ----Check-----
10  * Check functions, including kernel/version/cgpt check.
11
12  ----Precheck---
13  * Pre-check if the device can run its nebraska.
14  * Pre-check for stateful/rootfs update/whole update.
15
16  ----Tranfer----
17  * This step is carried out by Transfer subclasses in
18    auto_updater_transfer.py.
19
20  ----Auto-Update---
21  * Do rootfs partition update if it's required.
22  * Do stateful partition update if it's required.
23  * Do reboot for device if it's required.
24
25  ----Verify----
26  * Do verification if it's required.
27  * Disable rootfs verification in device if it's required.
28  * Post-check stateful/rootfs update/whole update.
29"""
30
31from __future__ import print_function
32
33import json
34import os
35import re
36import subprocess
37import tempfile
38import time
39
40import six
41
42from autotest_lib.utils.frozen_chromite.cli import command
43from autotest_lib.utils.frozen_chromite.lib import auto_update_util
44from autotest_lib.utils.frozen_chromite.lib import auto_updater_transfer
45from autotest_lib.utils.frozen_chromite.lib import cros_build_lib
46from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging
47from autotest_lib.utils.frozen_chromite.lib import nebraska_wrapper
48from autotest_lib.utils.frozen_chromite.lib import operation
49from autotest_lib.utils.frozen_chromite.lib import osutils
50from autotest_lib.utils.frozen_chromite.lib import remote_access
51from autotest_lib.utils.frozen_chromite.lib import retry_util
52from autotest_lib.utils.frozen_chromite.lib import stateful_updater
53from autotest_lib.utils.frozen_chromite.lib import timeout_util
54
55from autotest_lib.utils.frozen_chromite.utils import key_value_store
56
57# Naming conventions for global variables:
58#   File on remote host without slash: REMOTE_XXX_FILENAME
59#   File on remote host with slash: REMOTE_XXX_FILE_PATH
60#   Path on remote host with slash: REMOTE_XXX_PATH
61#   File on local server without slash: LOCAL_XXX_FILENAME
62
63# Update Status for remote device.
64UPDATE_STATUS_IDLE = 'UPDATE_STATUS_IDLE'
65UPDATE_STATUS_DOWNLOADING = 'UPDATE_STATUS_DOWNLOADING'
66UPDATE_STATUS_FINALIZING = 'UPDATE_STATUS_FINALIZING'
67UPDATE_STATUS_UPDATED_NEED_REBOOT = 'UPDATE_STATUS_UPDATED_NEED_REBOOT'
68
69# Max number of the times for retry:
70# 1. for transfer functions to be retried.
71# 2. for some retriable commands to be retried.
72MAX_RETRY = 5
73
74# The delay between retriable tasks.
75DELAY_SEC_FOR_RETRY = 5
76
77# Number of seconds to wait for the post check version to settle.
78POST_CHECK_SETTLE_SECONDS = 15
79
80# Number of seconds to delay between post check retries.
81POST_CHECK_RETRY_SECONDS = 5
82
83
84class ChromiumOSUpdateError(Exception):
85  """Thrown when there is a general ChromiumOS-specific update error."""
86
87
88class PreSetupUpdateError(ChromiumOSUpdateError):
89  """Raised for the rootfs/stateful update pre-setup failures."""
90
91
92class RootfsUpdateError(ChromiumOSUpdateError):
93  """Raised for the Rootfs partition update failures."""
94
95
96class StatefulUpdateError(ChromiumOSUpdateError):
97  """Raised for the stateful partition update failures."""
98
99
100class AutoUpdateVerifyError(ChromiumOSUpdateError):
101  """Raised for verification failures after auto-update."""
102
103
104class RebootVerificationError(ChromiumOSUpdateError):
105  """Raised for failing to reboot errors."""
106
107
108class BaseUpdater(object):
109  """The base updater class."""
110
111  def __init__(self, device, payload_dir):
112    self.device = device
113    self.payload_dir = payload_dir
114
115
116class ChromiumOSUpdater(BaseUpdater):
117  """Used to update DUT with image."""
118
119  # Nebraska files.
120  LOCAL_NEBRASKA_LOG_FILENAME = 'nebraska.log'
121  REMOTE_NEBRASKA_FILENAME = 'nebraska.py'
122
123  # rootfs update files.
124  REMOTE_UPDATE_ENGINE_BIN_FILENAME = 'update_engine_client'
125  REMOTE_UPDATE_ENGINE_LOGFILE_PATH = '/var/log/update_engine.log'
126
127  UPDATE_CHECK_INTERVAL_PROGRESSBAR = 0.5
128  UPDATE_CHECK_INTERVAL_NORMAL = 10
129
130  # `mode` parameter when copying payload files to the DUT.
131  PAYLOAD_MODE_PARALLEL = 'parallel'
132  PAYLOAD_MODE_SCP = 'scp'
133
134  # Related to crbug.com/276094: Restore to 5 mins once the 'host did not
135  # return from reboot' bug is solved.
136  REBOOT_TIMEOUT = 480
137
138  REMOTE_STATEFUL_PATH_TO_CHECK = ('/var', '/home', '/mnt/stateful_partition')
139  REMOTE_STATEFUL_TEST_FILENAME = '.test_file_to_be_deleted'
140  REMOTE_UPDATED_MARKERFILE_PATH = '/run/update_engine_autoupdate_completed'
141  REMOTE_LAB_MACHINE_FILE_PATH = '/mnt/stateful_partition/.labmachine'
142  KERNEL_A = {'name': 'KERN-A', 'kernel': 2, 'root': 3}
143  KERNEL_B = {'name': 'KERN-B', 'kernel': 4, 'root': 5}
144  KERNEL_UPDATE_TIMEOUT = 180
145
146  def __init__(self, device, build_name, payload_dir, transfer_class,
147               log_file=None, tempdir=None, clobber_stateful=True,
148               yes=False, do_rootfs_update=True, do_stateful_update=True,
149               reboot=True, disable_verification=False,
150               send_payload_in_parallel=False, payload_filename=None,
151               staging_server=None, clear_tpm_owner=False, ignore_appid=False):
152    """Initialize a ChromiumOSUpdater for auto-update a chromium OS device.
153
154    Args:
155      device: the ChromiumOSDevice to be updated.
156      build_name: the target update version for the device.
157      payload_dir: the directory of payload(s).
158      transfer_class: A reference to any subclass of
159          auto_updater_transfer.Transfer class.
160      log_file: The file to save running logs.
161      tempdir: the temp directory in caller, not in the device. For example,
162          the tempdir for cros flash is /tmp/cros-flash****/, used to
163          temporarily keep files when transferring update-utils package, and
164          reserve nebraska and update engine logs.
165      do_rootfs_update: whether to do rootfs partition update. The default is
166          True.
167      do_stateful_update: whether to do stateful partition update. The default
168          is True.
169      reboot: whether to reboot device after update. The default is True.
170      disable_verification: whether to disabling rootfs verification on the
171          device. The default is False.
172      clobber_stateful: whether to do a clean stateful update. The default is
173          False.
174      yes: Assume "yes" (True) for any prompt. The default is False. However,
175          it should be set as True if we want to disable all the prompts for
176          auto-update.
177      payload_filename: Filename of exact payload file to use for
178          update instead of the default: update.gz. Defaults to None. Use
179          only if you staged a payload by filename (i.e not artifact) first.
180      send_payload_in_parallel: whether to transfer payload in chunks
181          in parallel. The default is False.
182      staging_server: URL (str) of the server that's staging the payload files.
183          Assuming transfer_class is None, if value for staging_server is None
184          or empty, an auto_updater_transfer.LocalTransfer reference must be
185          passed through the transfer_class parameter.
186      clear_tpm_owner: If true, it will clear the TPM owner on reboot.
187      ignore_appid: True to tell Nebraska to ignore the update request's
188          App ID. This allows mismatching the source and target version boards.
189          One specific use case is updating between <board> and
190          <board>-kernelnext images which have different App IDs.
191    """
192    super(ChromiumOSUpdater, self).__init__(device, payload_dir)
193
194    self.tempdir = (tempdir if tempdir is not None
195                    else tempfile.mkdtemp(prefix='cros-update'))
196    self.inactive_kernel = None
197    self.update_version = build_name
198
199    # Update setting
200    self._cmd_kwargs = {}
201    self._cmd_kwargs_omit_error = {'check': False}
202    self._do_stateful_update = do_stateful_update
203    self._do_rootfs_update = do_rootfs_update
204    self._disable_verification = disable_verification
205    self._clobber_stateful = clobber_stateful
206    self._reboot = reboot
207    self._yes = yes
208    # Device's directories
209    self.device_dev_dir = os.path.join(self.device.work_dir, 'src')
210    self.device_payload_dir = os.path.join(
211        self.device.work_dir,
212        auto_updater_transfer.Transfer.PAYLOAD_DIR_NAME)
213    # autoupdate_EndToEndTest uses exact payload filename for update
214    self.payload_filename = payload_filename
215    if send_payload_in_parallel:
216      self.payload_mode = self.PAYLOAD_MODE_PARALLEL
217    else:
218      self.payload_mode = self.PAYLOAD_MODE_SCP
219    self.perf_id = None
220
221    if log_file:
222      log_kwargs = {
223          'stdout': log_file,
224          'append_to_file': True,
225          'stderr': subprocess.STDOUT,
226      }
227      self._cmd_kwargs.update(log_kwargs)
228      self._cmd_kwargs_omit_error.update(log_kwargs)
229
230    self._staging_server = staging_server
231    self._transfer_obj = self._CreateTransferObject(transfer_class)
232
233    self._clear_tpm_owner = clear_tpm_owner
234    self._ignore_appid = ignore_appid
235
236  @property
237  def is_au_endtoendtest(self):
238    return self.payload_filename is not None
239
240  @property
241  def request_logs_dir(self):
242    """Returns path to the nebraska request logfiles directory.
243
244    Returns:
245      A complete path to the logfiles directory.
246    """
247    return self.tempdir
248
249  def _CreateTransferObject(self, transfer_class):
250    """Create the correct Transfer class.
251
252    Args:
253      transfer_class: A variable that contains a reference to one of the
254          Transfer classes in auto_updater_transfer.
255    """
256    assert issubclass(transfer_class, auto_updater_transfer.Transfer)
257
258    # Determine if staging_server needs to be passed as an argument to
259    # class_ref.
260    cls_kwargs = {}
261    if self._staging_server:
262      cls_kwargs['staging_server'] = self._staging_server
263
264    return transfer_class(
265        device=self.device, payload_dir=self.payload_dir,
266        payload_name=self._GetRootFsPayloadFileName(),
267        cmd_kwargs=self._cmd_kwargs,
268        transfer_rootfs_update=self._do_rootfs_update,
269        transfer_stateful_update=self._do_rootfs_update,
270        device_payload_dir=self.device_payload_dir, tempdir=self.tempdir,
271        payload_mode=self.payload_mode, **cls_kwargs)
272
273  def CheckRestoreStateful(self):
274    """Check whether to restore stateful."""
275    logging.debug('Checking whether to restore stateful...')
276    restore_stateful = False
277    try:
278      self._CheckNebraskaCanRun()
279      return restore_stateful
280    except nebraska_wrapper.NebraskaStartupError as e:
281      if self._do_rootfs_update:
282        msg = ('Cannot start nebraska! The stateful partition may be '
283               'corrupted: %s' % e)
284        prompt = 'Attempt to restore the stateful partition?'
285        restore_stateful = self._yes or cros_build_lib.BooleanPrompt(
286            prompt=prompt, default=False, prolog=msg)
287        if not restore_stateful:
288          raise ChromiumOSUpdateError(
289              'Cannot continue to perform rootfs update!')
290
291    logging.debug('Restore stateful partition is%s required.',
292                  ('' if restore_stateful else ' not'))
293    return restore_stateful
294
295  def _CheckNebraskaCanRun(self):
296    """We can run Nebraska on |device|."""
297    nebraska_bin = os.path.join(self.device_dev_dir,
298                                self.REMOTE_NEBRASKA_FILENAME)
299    nebraska = nebraska_wrapper.RemoteNebraskaWrapper(
300        self.device, nebraska_bin=nebraska_bin)
301    nebraska.CheckNebraskaCanRun()
302
303  @classmethod
304  def GetUpdateStatus(cls, device, keys=None):
305    """Returns the status of the update engine on the |device|.
306
307    Retrieves the status from update engine and confirms all keys are
308    in the status.
309
310    Args:
311      device: A ChromiumOSDevice object.
312      keys: the keys to look for in the status result (defaults to
313          ['CURRENT_OP']).
314
315    Returns:
316      A list of values in the order of |keys|.
317    """
318    keys = keys or ['CURRENT_OP']
319    result = device.run([cls.REMOTE_UPDATE_ENGINE_BIN_FILENAME, '--status'],
320                        capture_output=True, log_output=True)
321
322    if not result.output:
323      raise Exception('Cannot get update status')
324
325    try:
326      status = key_value_store.LoadData(result.output)
327    except ValueError:
328      raise ValueError('Cannot parse update status')
329
330    values = []
331    for key in keys:
332      if key not in status:
333        raise ValueError('Missing "%s" in the update engine status' % key)
334
335      values.append(status.get(key))
336
337    return values
338
339  @classmethod
340  def GetRootDev(cls, device):
341    """Get the current root device on |device|.
342
343    Args:
344      device: a ChromiumOSDevice object, defines whose root device we
345          want to fetch.
346    """
347    rootdev = device.run(
348        ['rootdev', '-s'], capture_output=True).output.strip()
349    logging.debug('Current root device is %s', rootdev)
350    return rootdev
351
352  def _StartUpdateEngineIfNotRunning(self, device):
353    """Starts update-engine service if it is not running.
354
355    Args:
356      device: a ChromiumOSDevice object, defines the target root device.
357    """
358    try:
359      result = device.run(['start', 'update-engine'],
360                          capture_output=True, log_output=True).stdout
361      if 'start/running' in result:
362        logging.info('update engine was not running, so we started it.')
363    except cros_build_lib.RunCommandError as e:
364      if e.result.returncode != 1 or 'is already running' not in e.result.error:
365        raise e
366
367  def SetupRootfsUpdate(self):
368    """Makes sure |device| is ready for rootfs update."""
369    logging.info('Checking if update engine is idle...')
370    self._StartUpdateEngineIfNotRunning(self.device)
371    status = self.GetUpdateStatus(self.device)[0]
372    if status == UPDATE_STATUS_UPDATED_NEED_REBOOT:
373      logging.info('Device needs to reboot before updating...')
374      self._Reboot('setup of Rootfs Update')
375      status = self.GetUpdateStatus(self.device)[0]
376
377    if status != UPDATE_STATUS_IDLE:
378      raise RootfsUpdateError('Update engine is not idle. Status: %s' % status)
379
380    if self.is_au_endtoendtest:
381      # TODO(ahassani): This should only be done for jetsteam devices.
382      self._RetryCommand(['sudo', 'stop', 'ap-update-manager'],
383                         **self._cmd_kwargs_omit_error)
384
385      self._RetryCommand(['rm', '-f', self.REMOTE_UPDATED_MARKERFILE_PATH],
386                         **self._cmd_kwargs)
387      self._RetryCommand(['stop', 'ui'], **self._cmd_kwargs_omit_error)
388
389
390  def _GetDevicePythonSysPath(self):
391    """Get python sys.path of the given |device|."""
392    sys_path = self.device.run(
393        ['python', '-c', '"import json, sys; json.dump(sys.path, sys.stdout)"'],
394        capture_output=True, log_output=True).output
395    return json.loads(sys_path)
396
397  def _FindDevicePythonPackagesDir(self):
398    """Find the python packages directory for the given |device|."""
399    third_party_host_dir = ''
400    sys_path = self._GetDevicePythonSysPath()
401    for p in sys_path:
402      if p.endswith('site-packages') or p.endswith('dist-packages'):
403        third_party_host_dir = p
404        break
405
406    if not third_party_host_dir:
407      raise ChromiumOSUpdateError(
408          'Cannot find proper site-packages/dist-packages directory from '
409          'sys.path for storing packages: %s' % sys_path)
410
411    return third_party_host_dir
412
413  def _GetRootFsPayloadFileName(self):
414    """Get the correct RootFs payload filename.
415
416    Returns:
417      The payload filename. (update.gz or a custom payload filename).
418    """
419    if self.is_au_endtoendtest:
420      return self.payload_filename
421    else:
422      return auto_updater_transfer.ROOTFS_FILENAME
423
424  def ResetStatefulPartition(self):
425    """Clear any pending stateful update request."""
426    logging.debug('Resetting stateful partition...')
427    try:
428      stateful_updater.StatefulUpdater(self.device).Reset()
429    except stateful_updater.Error as e:
430      raise StatefulUpdateError(e)
431
432  def RevertBootPartition(self):
433    """Revert the boot partition."""
434    part = self.GetRootDev(self.device)
435    logging.warning('Reverting update; Boot partition will be %s', part)
436    try:
437      self.device.run(['/postinst', part], **self._cmd_kwargs)
438    except cros_build_lib.RunCommandError as e:
439      logging.warning('Reverting the boot partition failed: %s', e)
440
441  def UpdateRootfs(self):
442    """Update the rootfs partition of the device (utilizing nebraska)."""
443    logging.notice('Updating rootfs partition...')
444    nebraska_bin = os.path.join(self.device_dev_dir,
445                                self.REMOTE_NEBRASKA_FILENAME)
446
447    nebraska = nebraska_wrapper.RemoteNebraskaWrapper(
448        self.device, nebraska_bin=nebraska_bin,
449        update_payloads_address='file://' + self.device_payload_dir,
450        update_metadata_dir=self.device_payload_dir,
451        ignore_appid=self._ignore_appid)
452
453    try:
454      nebraska.Start()
455
456      # Use the localhost IP address (default) to ensure that update engine
457      # client can connect to the nebraska.
458      nebraska_url = nebraska.GetURL(critical_update=True)
459      cmd = [self.REMOTE_UPDATE_ENGINE_BIN_FILENAME, '--check_for_update',
460             '--omaha_url="%s"' % nebraska_url]
461
462      self.device.run(cmd, **self._cmd_kwargs)
463
464      # If we are using a progress bar, update it every 0.5s instead of 10s.
465      if command.UseProgressBar():
466        update_check_interval = self.UPDATE_CHECK_INTERVAL_PROGRESSBAR
467        oper = operation.ProgressBarOperation()
468      else:
469        update_check_interval = self.UPDATE_CHECK_INTERVAL_NORMAL
470        oper = None
471      end_message_not_printed = True
472
473      # Loop until update is complete.
474      while True:
475        # Number of times to retry `update_engine_client --status`. See
476        # crbug.com/744212.
477        update_engine_status_retry = 30
478        op, progress = retry_util.RetryException(
479            cros_build_lib.RunCommandError,
480            update_engine_status_retry,
481            self.GetUpdateStatus,
482            self.device,
483            ['CURRENT_OP', 'PROGRESS'],
484            delay_sec=DELAY_SEC_FOR_RETRY)[0:2]
485        logging.info('Waiting for update...status: %s at progress %s',
486                     op, progress)
487
488        if op == UPDATE_STATUS_UPDATED_NEED_REBOOT:
489          logging.info('Update completed.')
490          break
491
492        if op == UPDATE_STATUS_IDLE:
493          # Something went wrong. Try to get last error code.
494          cmd = ['cat', self.REMOTE_UPDATE_ENGINE_LOGFILE_PATH]
495          log = self.device.run(cmd).stdout.strip().splitlines()
496          err_str = 'Updating payload state for error code: '
497          targets = [line for line in log if err_str in line]
498          logging.debug('Error lines found: %s', targets)
499          if not targets:
500            raise RootfsUpdateError(
501                'Update failed with unexpected update status: %s' % op)
502          else:
503            # e.g 20 (ErrorCode::kDownloadStateInitializationError)
504            raise RootfsUpdateError(targets[-1].rpartition(err_str)[2])
505
506        if oper is not None:
507          if op == UPDATE_STATUS_DOWNLOADING:
508            oper.ProgressBar(float(progress))
509          elif end_message_not_printed and op == UPDATE_STATUS_FINALIZING:
510            oper.Cleanup()
511            logging.info('Finalizing image.')
512            end_message_not_printed = False
513
514        time.sleep(update_check_interval)
515    # TODO(ahassani): Scope the Exception to finer levels. For example we don't
516    # need to revert the boot partition if the Nebraska fails to start, etc.
517    except Exception as e:
518      logging.error('Rootfs update failed %s', e)
519      self.RevertBootPartition()
520      logging.warning(nebraska.PrintLog() or 'No nebraska log is available.')
521      raise RootfsUpdateError('Failed to perform rootfs update: %r' % e)
522    finally:
523      nebraska.Stop()
524
525      nebraska.CollectLogs(os.path.join(self.tempdir,
526                                        self.LOCAL_NEBRASKA_LOG_FILENAME))
527      self.device.CopyFromDevice(
528          self.REMOTE_UPDATE_ENGINE_LOGFILE_PATH,
529          os.path.join(self.tempdir, os.path.basename(
530              self.REMOTE_UPDATE_ENGINE_LOGFILE_PATH)),
531          follow_symlinks=True, **self._cmd_kwargs_omit_error)
532
533  def UpdateStateful(self):
534    """Update the stateful partition of the device."""
535    try:
536      stateful_update_payload = os.path.join(
537          self.device.work_dir, auto_updater_transfer.STATEFUL_FILENAME)
538
539      updater = stateful_updater.StatefulUpdater(self.device)
540      updater.Update(
541          stateful_update_payload,
542          update_type=(stateful_updater.StatefulUpdater.UPDATE_TYPE_CLOBBER if
543                       self._clobber_stateful else None))
544
545      # Delete the stateful update file on success so it doesn't occupy extra
546      # disk space. On failure it will get cleaned up.
547      self.device.DeletePath(stateful_update_payload)
548    except stateful_updater.Error as e:
549      error_msg = 'Stateful update failed with error: %s' % str(e)
550      logging.exception(error_msg)
551      self.ResetStatefulPartition()
552      raise StatefulUpdateError(error_msg)
553
554  def _FixPayloadPropertiesFile(self):
555    """Fix the update payload properties file so nebraska can use it.
556
557    Update the payload properties file to make sure that nebraska can use it.
558    The reason is that very old payloads are still being used for provisioning
559    the AU tests, but those properties files are not compatible with recent
560    nebraska protocols.
561
562    TODO(ahassani): Once we only test delta or full payload with
563    source image of M77 or higher, this function can be deprecated.
564    """
565    logging.info('Fixing payload properties file.')
566    payload_properties_path = self._transfer_obj.GetPayloadPropsFile()
567    props = json.loads(osutils.ReadFile(payload_properties_path))
568    props['appid'] = self.ResolveAPPIDMismatchIfAny(props.get('appid'))
569    values = self._transfer_obj.GetPayloadProps()
570
571    # TODO(ahassani): Use the keys form nebraska.py once it is moved to
572    # chromite.
573    valid_entries = {
574        # Since only old payloads don't have this and they are only used for
575        # provisioning, they will be full payloads.
576        'is_delta': False,
577        'size': values['size'],
578        'target_version': values['image_version'],
579    }
580
581    for key, value in valid_entries.items():
582      if props.get(key) is None:
583        props[key] = value
584
585    with open(payload_properties_path, 'w') as fp:
586      json.dump(props, fp)
587
588  def RunUpdateRootfs(self):
589    """Run all processes needed by updating rootfs.
590
591    1. Check device's status to make sure it can be updated.
592    2. Copy files to remote device needed for rootfs update.
593    3. Do root updating.
594    """
595
596    # Any call to self._transfer_obj.TransferRootfsUpdate() must be preceeded by
597    # a conditional call to self._FixPayloadPropertiesFile() as this handles the
598    # usecase in reported in crbug.com/1012520. Whenever
599    # self._FixPayloadPropertiesFile() gets deprecated, this call can be safely
600    # removed. For more details on TODOs, refer to self.TransferRootfsUpdate()
601    # docstrings.
602
603    self._FixPayloadPropertiesFile()
604
605    # SetupRootfsUpdate() may reboot the device and therefore should be called
606    # before any payloads are transferred to the device and only if rootfs
607    # update is required.
608    self.SetupRootfsUpdate()
609
610    # Copy payload for rootfs update.
611    self._transfer_obj.TransferRootfsUpdate()
612
613    self.UpdateRootfs()
614
615    if self.is_au_endtoendtest:
616      self.PostCheckRootfsUpdate()
617
618    # Delete the update file so it doesn't take much space on disk for the
619    # remainder of the update process.
620    self.device.DeletePath(self.device_payload_dir, recursive=True)
621
622  def RunUpdateStateful(self):
623    """Run all processes needed by updating stateful.
624
625    1. Copy files to remote device needed by stateful update.
626    2. Do stateful update.
627    """
628    self._transfer_obj.TransferStatefulUpdate()
629    self.UpdateStateful()
630
631  def RebootAndVerify(self):
632    """Reboot and verify the remote device.
633
634    1. Reboot the remote device. If _clobber_stateful (--clobber-stateful)
635    is executed, the stateful partition is wiped, and the working directory
636    on the remote device no longer exists. So, recreate the working directory
637    for this remote device.
638    2. Verify the remote device, by checking that whether the root device
639    changed after reboot.
640    """
641    logging.notice('Rebooting device...')
642    # Record the current root device. This must be done after SetupRootfsUpdate
643    # and before reboot, since SetupRootfsUpdate may reboot the device if there
644    # is a pending update, which changes the root device, and reboot will
645    # definitely change the root device if update successfully finishes.
646    old_root_dev = self.GetRootDev(self.device)
647    self.device.Reboot()
648    if self._clobber_stateful:
649      self.device.run(['mkdir', '-p', self.device.work_dir])
650
651    if self._do_rootfs_update:
652      logging.notice('Verifying that the device has been updated...')
653      new_root_dev = self.GetRootDev(self.device)
654      if old_root_dev is None:
655        raise AutoUpdateVerifyError(
656            'Failed to locate root device before update.')
657
658      if new_root_dev is None:
659        raise AutoUpdateVerifyError(
660            'Failed to locate root device after update.')
661
662      if new_root_dev == old_root_dev:
663        raise AutoUpdateVerifyError(
664            'Failed to boot into the new version. Possibly there was a '
665            'signing problem, or an automated rollback occurred because '
666            'your new image failed to boot.')
667
668  def ResolveAPPIDMismatchIfAny(self, payload_app_id):
669    """Resolves and APP ID mismatch between the payload and device.
670
671    If the APP ID of the payload is different than the device, then the nebraska
672    will fail. We empty the payload's AppID so nebraska can do partial APP ID
673    matching.
674    """
675    if ((self.device.app_id and self.device.app_id == payload_app_id) or
676        payload_app_id == ''):
677      return payload_app_id
678
679    logging.warning('You are installing an image with a different release '
680                    'App ID than the device (%s vs %s), we are forcing the '
681                    'install!', payload_app_id, self.device.app_id)
682    return ''
683
684  def RunUpdate(self):
685    """Update the device with image of specific version."""
686    self._transfer_obj.CheckPayloads()
687
688    self._transfer_obj.TransferUpdateUtilsPackage()
689
690    restore_stateful = self.CheckRestoreStateful()
691    if restore_stateful:
692      self.RestoreStateful()
693
694    # Perform device updates.
695    if self._do_rootfs_update:
696      self.RunUpdateRootfs()
697      logging.info('Rootfs update completed.')
698
699    if self._do_stateful_update and not restore_stateful:
700      self.RunUpdateStateful()
701      logging.info('Stateful update completed.')
702
703    if self._clear_tpm_owner:
704      self.SetClearTpmOwnerRequest()
705
706    if self._reboot:
707      self.RebootAndVerify()
708
709    if self.is_au_endtoendtest:
710      self.PostCheckCrOSUpdate()
711
712    if self._disable_verification:
713      logging.info('Disabling rootfs verification on the device...')
714      self.device.DisableRootfsVerification()
715
716  def _Reboot(self, error_stage, timeout=None):
717    try:
718      if timeout is None:
719        timeout = self.REBOOT_TIMEOUT
720      self.device.Reboot(timeout_sec=timeout)
721    except cros_build_lib.DieSystemExit:
722      raise ChromiumOSUpdateError('Could not recover from reboot at %s' %
723                                  error_stage)
724    except remote_access.SSHConnectionError:
725      raise ChromiumOSUpdateError('Failed to connect at %s' % error_stage)
726
727  def _cgpt(self, flag, kernel, dev='$(rootdev -s -d)'):
728    """Return numeric cgpt value for the specified flag, kernel, device."""
729    cmd = ['cgpt', 'show', '-n', '-i', '%d' % kernel['kernel'], flag, dev]
730    return int(self._RetryCommand(
731        cmd, capture_output=True, log_output=True).output.strip())
732
733  def _GetKernelPriority(self, kernel):
734    """Return numeric priority for the specified kernel.
735
736    Args:
737      kernel: information of the given kernel, KERNEL_A or KERNEL_B.
738    """
739    return self._cgpt('-P', kernel)
740
741  def _GetKernelSuccess(self, kernel):
742    """Return boolean success flag for the specified kernel.
743
744    Args:
745      kernel: information of the given kernel, KERNEL_A or KERNEL_B.
746    """
747    return self._cgpt('-S', kernel) != 0
748
749  def _GetKernelTries(self, kernel):
750    """Return tries count for the specified kernel.
751
752    Args:
753      kernel: information of the given kernel, KERNEL_A or KERNEL_B.
754    """
755    return self._cgpt('-T', kernel)
756
757  def _GetKernelState(self):
758    """Returns the (<active>, <inactive>) kernel state as a pair."""
759    active_root = int(re.findall(r'(\d+\Z)', self.GetRootDev(self.device))[0])
760    if active_root == self.KERNEL_A['root']:
761      return self.KERNEL_A, self.KERNEL_B
762    elif active_root == self.KERNEL_B['root']:
763      return self.KERNEL_B, self.KERNEL_A
764    else:
765      raise ChromiumOSUpdateError('Encountered unknown root partition: %s' %
766                                  active_root)
767
768  def _GetReleaseVersion(self):
769    """Get release version of the device."""
770    lsb_release_content = self._RetryCommand(
771        ['cat', '/etc/lsb-release'],
772        capture_output=True, log_output=True).output.strip()
773    regex = r'^CHROMEOS_RELEASE_VERSION=(.+)$'
774    return auto_update_util.GetChromeosBuildInfo(
775        lsb_release_content=lsb_release_content, regex=regex)
776
777  def _GetReleaseBuilderPath(self):
778    """Get release version of the device."""
779    lsb_release_content = self._RetryCommand(
780        ['cat', '/etc/lsb-release'],
781        capture_output=True, log_output=True).output.strip()
782    regex = r'^CHROMEOS_RELEASE_BUILDER_PATH=(.+)$'
783    return auto_update_util.GetChromeosBuildInfo(
784        lsb_release_content=lsb_release_content, regex=regex)
785
786  def CheckVersion(self):
787    """Check the image running in DUT has the expected version.
788
789    Returns:
790      True if the DUT's image version matches the version that the
791      ChromiumOSUpdater tries to update to.
792    """
793    if not self.update_version:
794      return False
795
796    # Use CHROMEOS_RELEASE_BUILDER_PATH to match the build version if it exists
797    # in lsb-release, otherwise, continue using CHROMEOS_RELEASE_VERSION.
798    release_builder_path = self._GetReleaseBuilderPath()
799    if release_builder_path:
800      return self.update_version == release_builder_path
801
802    return self.update_version.endswith(self._GetReleaseVersion())
803
804  def _VerifyBootExpectations(self, expected_kernel_state, rollback_message):
805    """Verify that we fully booted given expected kernel state.
806
807    It verifies that we booted using the correct kernel state, and that the
808    OS has marked the kernel as good.
809
810    Args:
811      expected_kernel_state: kernel state that we're verifying with i.e. I
812        expect to be booted onto partition 4 etc. See output of _GetKernelState.
813      rollback_message: string to raise as a RootfsUpdateError if we booted
814        with the wrong partition.
815    """
816    logging.debug('Start verifying boot expectations...')
817    # Figure out the newly active kernel
818    active_kernel_state = self._GetKernelState()[0]
819
820    # Rollback
821    if (expected_kernel_state and
822        active_kernel_state != expected_kernel_state):
823      logging.debug('Dumping partition table.')
824      self.device.run(['cgpt', 'show', '$(rootdev -s -d)'],
825                      **self._cmd_kwargs)
826      logging.debug('Dumping crossystem for firmware debugging.')
827      self.device.run(['crossystem', '--all'], **self._cmd_kwargs)
828      raise RootfsUpdateError(rollback_message)
829
830    # Make sure chromeos-setgoodkernel runs
831    try:
832      timeout_util.WaitForReturnTrue(
833          lambda: (self._GetKernelTries(active_kernel_state) == 0
834                   and self._GetKernelSuccess(active_kernel_state)),
835          self.KERNEL_UPDATE_TIMEOUT,
836          period=5)
837    except timeout_util.TimeoutError:
838      services_status = self.device.run(
839          ['status', 'system-services'], capture_output=True,
840          log_output=True).output
841      logging.debug('System services_status: %r', services_status)
842      if services_status != 'system-services start/running\n':
843        event = ('Chrome failed to reach login screen')
844      else:
845        event = ('update-engine failed to call '
846                 'chromeos-setgoodkernel')
847      raise RootfsUpdateError(
848          'After update and reboot, %s '
849          'within %d seconds' % (event, self.KERNEL_UPDATE_TIMEOUT))
850
851  def _CheckVersionToConfirmInstall(self):
852    logging.debug('Checking whether the new build is successfully installed...')
853    if not self.update_version:
854      logging.debug('No update_version is provided if test is executed with'
855                    'local nebraska.')
856      return True
857
858    # Always try the default check_version method first, this prevents
859    # any backward compatibility issue.
860    if self.CheckVersion():
861      return True
862
863    return auto_update_util.VersionMatch(
864        self.update_version, self._GetReleaseVersion())
865
866  def _RetryCommand(self, cmd, **kwargs):
867    """Retry commands if SSHConnectionError happens.
868
869    Args:
870      cmd: the command to be run by device.
871      kwargs: the parameters for device to run the command.
872
873    Returns:
874      the output of running the command.
875    """
876    return retry_util.RetryException(
877        remote_access.SSHConnectionError,
878        MAX_RETRY,
879        self.device.run,
880        cmd, delay_sec=DELAY_SEC_FOR_RETRY,
881        shell=isinstance(cmd, six.string_types),
882        **kwargs)
883
884  def PreSetupStatefulUpdate(self):
885    """Pre-setup for stateful update for CrOS host."""
886    logging.debug('Start pre-setup for stateful update...')
887    if self._clobber_stateful:
888      for folder in self.REMOTE_STATEFUL_PATH_TO_CHECK:
889        touch_path = os.path.join(folder, self.REMOTE_STATEFUL_TEST_FILENAME)
890        self._RetryCommand(['touch', touch_path], **self._cmd_kwargs)
891
892  def PostCheckStatefulUpdate(self):
893    """Post-check for stateful update for CrOS host."""
894    logging.debug('Start post check for stateful update...')
895    self._Reboot('post check of stateful update')
896    if self._clobber_stateful:
897      for folder in self.REMOTE_STATEFUL_PATH_TO_CHECK:
898        test_file_path = os.path.join(folder,
899                                      self.REMOTE_STATEFUL_TEST_FILENAME)
900        # If stateful update succeeds, these test files should not exist.
901        if self.device.IfFileExists(test_file_path,
902                                    **self._cmd_kwargs_omit_error):
903          raise StatefulUpdateError('failed to post-check stateful update.')
904
905  def _IsUpdateUtilsPackageInstalled(self):
906    """Check whether update-utils package is well installed.
907
908    There's a chance that nebraska package is removed in the middle of
909    auto-update process. This function double check it and transfer it if it's
910    removed.
911    """
912    logging.info('Checking whether nebraska files are still on the device...')
913    try:
914      nebraska_bin = os.path.join(self.device_dev_dir,
915                                  self.REMOTE_NEBRASKA_FILENAME)
916      if not self.device.IfFileExists(
917          nebraska_bin, **self._cmd_kwargs_omit_error):
918        logging.info('Nebraska files not found on device. Resending them...')
919
920        self._transfer_obj.TransferUpdateUtilsPackage()
921
922      return True
923    except cros_build_lib.RunCommandError as e:
924      logging.warning('Failed to verify whether packages still exist: %s', e)
925      return False
926
927  def CheckNebraskaCanRun(self):
928    """Check if nebraska can successfully run for ChromiumOSUpdater."""
929    self._IsUpdateUtilsPackageInstalled()
930    self._CheckNebraskaCanRun()
931
932  def RestoreStateful(self):
933    """Restore stateful partition for device."""
934    logging.warning('Restoring the stateful partition.')
935    self.PreSetupStatefulUpdate()
936    self._transfer_obj.TransferStatefulUpdate()
937    self.ResetStatefulPartition()
938    self.UpdateStateful()
939    self.PostCheckStatefulUpdate()
940    try:
941      self.CheckNebraskaCanRun()
942      logging.info('Stateful partition restored.')
943    except nebraska_wrapper.NebraskaStartupError as e:
944      raise ChromiumOSUpdateError(
945          'Unable to restore stateful partition: %s' % e)
946
947  def SetClearTpmOwnerRequest(self):
948    """Set clear_tpm_owner_request flag."""
949    # The issue is that certain AU tests leave the TPM in a bad state which
950    # most commonly shows up in provisioning.  Executing this 'crossystem'
951    # command before rebooting clears the problem state during the reboot.
952    # It's also worth mentioning that this isn't a complete fix:  The bad
953    # TPM state in theory might happen some time other than during
954    # provisioning.  Also, the bad TPM state isn't supposed to happen at
955    # all; this change is just papering over the real bug.
956    logging.info('Setting clear_tpm_owner_request to 1.')
957    self._RetryCommand('crossystem clear_tpm_owner_request=1',
958                       **self._cmd_kwargs_omit_error)
959
960  def PostCheckRootfsUpdate(self):
961    """Post-check for rootfs update for CrOS host."""
962    logging.debug('Start post check for rootfs update...')
963    active_kernel, inactive_kernel = self._GetKernelState()
964    logging.debug('active_kernel= %s, inactive_kernel=%s',
965                  active_kernel, inactive_kernel)
966    if (self._GetKernelPriority(inactive_kernel) <
967        self._GetKernelPriority(active_kernel)):
968      raise RootfsUpdateError('Update failed. The priority of the inactive '
969                              'kernel partition is less than that of the '
970                              'active kernel partition.')
971    self.inactive_kernel = inactive_kernel
972
973  def PostCheckCrOSUpdate(self):
974    """Post check for the whole auto-update process."""
975    logging.debug('Post check for the whole CrOS update...')
976    start_time = time.time()
977    # Not use 'sh' here since current device.run cannot recognize
978    # the content of $FILE.
979    autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || '
980                      '( touch "$FILE" ; start autoreboot )')
981    self._RetryCommand(autoreboot_cmd % self.REMOTE_LAB_MACHINE_FILE_PATH,
982                       **self._cmd_kwargs)
983
984    # Loop in case the initial check happens before the reboot.
985    while True:
986      try:
987        start_verify_time = time.time()
988        self._VerifyBootExpectations(
989            self.inactive_kernel, rollback_message=
990            'Build %s failed to boot on %s; system rolled back to previous '
991            'build' % (self.update_version, self.device.hostname))
992
993        # Check that we've got the build we meant to install.
994        if not self._CheckVersionToConfirmInstall():
995          raise ChromiumOSUpdateError(
996              'Failed to update %s to build %s; found build '
997              '%s instead' % (self.device.hostname,
998                              self.update_version,
999                              self._GetReleaseVersion()))
1000      except RebootVerificationError as e:
1001        # If a minimum amount of time since starting the check has not
1002        # occurred, wait and retry.  Use the start of the verification
1003        # time in case an SSH call takes a long time to return/fail.
1004        if start_verify_time - start_time < POST_CHECK_SETTLE_SECONDS:
1005          logging.warning('Delaying for re-check of %s to update to %s (%s)',
1006                          self.device.hostname, self.update_version, e)
1007          time.sleep(POST_CHECK_RETRY_SECONDS)
1008          continue
1009        raise
1010      break
1011
1012    if not self._clobber_stateful:
1013      self.PostRebootUpdateCheckForAUTest()
1014
1015  def PostRebootUpdateCheckForAUTest(self):
1016    """Do another update check after reboot to get the post update hostlog.
1017
1018    This is only done with autoupdate_EndToEndTest.
1019    """
1020    logging.debug('Doing one final update check to get post update hostlog.')
1021    nebraska_bin = os.path.join(self.device_dev_dir,
1022                                self.REMOTE_NEBRASKA_FILENAME)
1023    nebraska = nebraska_wrapper.RemoteNebraskaWrapper(
1024        self.device, nebraska_bin=nebraska_bin,
1025        update_metadata_dir=self.device.work_dir)
1026
1027    try:
1028      nebraska.Start()
1029
1030      nebraska_url = nebraska.GetURL(critical_update=True, no_update=True)
1031      cmd = [self.REMOTE_UPDATE_ENGINE_BIN_FILENAME, '--check_for_update',
1032             '--omaha_url="%s"' % nebraska_url]
1033      self.device.run(cmd, **self._cmd_kwargs)
1034      op = self.GetUpdateStatus(self.device)[0]
1035      logging.info('Post update check status: %s', op)
1036    except Exception as err:
1037      logging.error('Post reboot update check failed: %s', str(err))
1038      logging.warning(nebraska.PrintLog() or 'No nebraska log is available.')
1039    finally:
1040      nebraska.Stop()
1041