blob: b8301e347b6698720b62caae24124e01136df969 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Library containing functions to transfer files onto a remote device.
LocalTransfer includes:
----Precheck---
* Pre-check payload's existence before auto-update.
----Tranfer----
* Transfer update-utils (nebraska, et. al.) package at first.
* Transfer rootfs update files if rootfs update is required.
* Transfer stateful update files if stateful update is required.
* @retry to all transfer functions.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import abc
import os
import shutil
import six
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import nebraska_wrapper
from chromite.lib import osutils
from chromite.lib import path_util
from chromite.lib import retry_util
# Naming conventions for global variables:
# Path on remote host with slash: REMOTE_XXX_PATH
# File on local server without slash: LOCAL_XXX_FILENAME
# Path on local server: LOCAL_XXX_PATH
# Max number of the times for retry:
# 1. for transfer functions to be retried.
# 2. for some retriable commands to be retried.
_MAX_RETRY = 5
# The delay between retriable tasks.
_DELAY_SEC_FOR_RETRY = 5
# Update file names for rootfs+kernel and stateful partitions.
ROOTFS_FILENAME = 'update.gz'
STATEFUL_FILENAME = 'stateful.tgz'
_STATEFUL_UPDATE_FILENAME = 'stateful_update'
class Error(Exception):
"""A generic auto updater transfer error."""
class ChromiumOSTransferError(Error):
"""Thrown when there is a general transfer specific error."""
def GetPayloadPropertiesFileName(payload):
"""Returns the payload properties file given the path to the payload."""
return payload + '.json'
class Transfer(six.with_metaclass(abc.ABCMeta, object)):
"""Abstract Base Class that handles payload precheck and transfer."""
PAYLOAD_DIR_NAME = 'payloads'
def __init__(self, device, payload_dir, device_restore_dir,
payload_name, cmd_kwargs, device_payload_dir, dev_dir='',
original_payload_dir=None, transfer_stateful_update=True,
transfer_rootfs_update=True):
"""Initialize Base Class for transferring payloads functionality.
Args:
device: The ChromiumOSDevice to be updated.
payload_dir: The directory of payload(s).
device_restore_dir: Path to the old payload directory in the device's work
directory.
payload_name: Filename of exact payload file to use for update.
cmd_kwargs: Keyword arguments that are sent along with the commands that
are run on the device.
device_payload_dir: Path to the payload directory in the device's work
directory.
dev_dir: The directory of the nebraska that runs the CrOS auto-update.
original_payload_dir: The directory containing payloads whose version is
the same as current host's rootfs partition. If it's None, will first
try installing the matched stateful.tgz with the host's rootfs
Partition when restoring stateful. Otherwise, install the target
stateful.tgz.
transfer_stateful_update: Whether to transfer payloads necessary for
stateful update. The default is True.
transfer_rootfs_update: Whether to transfer payloads necessary for
rootfs update. The default is True.
"""
self._device = device
self._payload_dir = payload_dir
self._device_restore_dir = device_restore_dir
self._payload_name = payload_name
self._cmd_kwargs = cmd_kwargs
self._device_payload_dir = device_payload_dir
self._dev_dir = dev_dir
self._original_payload_dir = original_payload_dir
self._transfer_stateful_update = transfer_stateful_update
self._transfer_rootfs_update = transfer_rootfs_update
self._stateful_update_bin = None
@abc.abstractmethod
def CheckPayloads(self):
"""Verify that all required payloads are in |self.payload_dir|."""
def TransferUpdateUtilsPackage(self):
"""Transfer update-utils package to work directory of the remote device."""
retry_util.RetryException(
cros_build_lib.RunCommandError,
_MAX_RETRY,
self._TransferUpdateUtilsPackage,
delay_sec=_DELAY_SEC_FOR_RETRY)
def TransferRootfsUpdate(self):
"""Transfer files for rootfs update.
The corresponding payloads are copied to the remote device for rootfs
update.
"""
retry_util.RetryException(
cros_build_lib.RunCommandError,
_MAX_RETRY,
self._TransferRootfsUpdate,
delay_sec=_DELAY_SEC_FOR_RETRY)
def TransferStatefulUpdate(self):
"""Transfer files for stateful update.
The stateful update bin and the corresponding payloads are copied to the
target remote device for stateful update.
"""
retry_util.RetryException(
cros_build_lib.RunCommandError,
_MAX_RETRY,
self._TransferStatefulUpdate,
delay_sec=_DELAY_SEC_FOR_RETRY)
return self._stateful_update_bin
def _EnsureDeviceDirectory(self, directory):
"""Mkdir the directory no matther whether this directory exists on host.
Args:
directory: The directory to be made on the device.
"""
self._device.RunCommand(['mkdir', '-p', directory], **self._cmd_kwargs)
class LocalTransfer(Transfer):
"""Abstracts logic that handles transferring local files to the DUT."""
# Stateful update files.
LOCAL_STATEFUL_UPDATE_FILENAME = _STATEFUL_UPDATE_FILENAME
LOCAL_CHROOT_STATEFUL_UPDATE_PATH = '/usr/bin/stateful_update'
REMOTE_STATEFUL_UPDATE_PATH = '/usr/local/bin/stateful_update'
def __init__(self, device, payload_dir, tempdir, device_restore_dir,
payload_name, cmd_kwargs, device_payload_dir, dev_dir='',
payload_mode='scp', original_payload_dir=None,
transfer_stateful_update=True, transfer_rootfs_update=True):
"""Initialize LocalTransfer to handle transferring files from local to DUT.
Args:
device: See Base class.
payload_dir: See Base class.
tempdir: The temp directory in caller, not in the device. For example,
the tempdir for cros flash is /tmp/cros-flash****/, used to
temporarily keep files when transferring update-utils package, and
reserve nebraska and update engine logs.
device_restore_dir: See Base class.
payload_name: See Base class.
cmd_kwargs: See Base class.
device_payload_dir: See Base class.
dev_dir: See Base class.
payload_mode: The payload mode - it can be 'parallel' or 'scp'.
original_payload_dir: See Base class.
transfer_stateful_update: See Base class.
transfer_rootfs_update: See Base class.
"""
super(LocalTransfer, self).__init__(
device=device, payload_dir=payload_dir,
device_restore_dir=device_restore_dir, payload_name=payload_name,
cmd_kwargs=cmd_kwargs, device_payload_dir=device_payload_dir,
dev_dir=dev_dir, original_payload_dir=original_payload_dir,
transfer_stateful_update=transfer_stateful_update,
transfer_rootfs_update=transfer_rootfs_update)
self._tempdir = tempdir
self._payload_mode = payload_mode
def CheckPayloads(self):
"""Verify that all required payloads are in |self.payload_dir|."""
logging.debug('Checking if payloads have been stored in directory %s...',
self._payload_dir)
filenames = []
if self._transfer_rootfs_update:
filenames += [self._payload_name,
GetPayloadPropertiesFileName(self._payload_name)]
if self._transfer_stateful_update:
filenames += [STATEFUL_FILENAME]
for fname in filenames:
payload = os.path.join(self._payload_dir, fname)
if not os.path.exists(payload):
raise ChromiumOSTransferError('Payload %s does not exist!' % payload)
def _TransferUpdateUtilsPackage(self):
"""Transfer update-utils package to work directory of the remote device."""
files_to_copy = (nebraska_wrapper.NEBRASKA_SOURCE_FILE,)
logging.info('Copying these files to device: %s', files_to_copy)
source_dir = os.path.join(self._tempdir, 'src')
osutils.SafeMakedirs(source_dir)
for f in files_to_copy:
shutil.copy2(f, source_dir)
# Make sure the device.work_dir exist after any installation and reboot.
self._EnsureDeviceDirectory(self._device.work_dir)
# Python packages are plain text files so we chose rsync --compress.
self._device.CopyToWorkDir(source_dir, mode='rsync', log_output=True,
**self._cmd_kwargs)
def _TransferRootfsUpdate(self):
"""Transfer files for rootfs update.
Copy the update payload to the remote device for rootfs update.
"""
self._EnsureDeviceDirectory(self._device_payload_dir)
logging.info('Copying rootfs payload to device...')
payload = os.path.join(self._payload_dir, self._payload_name)
self._device.CopyToWorkDir(payload, self.PAYLOAD_DIR_NAME,
mode=self._payload_mode,
log_output=True, **self._cmd_kwargs)
payload_properties_path = GetPayloadPropertiesFileName(payload)
self._device.CopyToWorkDir(payload_properties_path, self.PAYLOAD_DIR_NAME,
mode=self._payload_mode,
log_output=True, **self._cmd_kwargs)
def _GetStatefulUpdateScript(self):
"""Returns the path to the stateful_update_bin on the target.
Returns:
(need_transfer, path):
need_transfer is True if stateful_update_bin is found in local path,
False if we directly use stateful_update_bin on the host.
path: If need_transfer is True, it represents the local path of
stateful_update_bin, and is used for further transferring. Otherwise,
it refers to the host path.
"""
# We attempt to load the local stateful update path in 2 different
# ways. If this doesn't exist, we attempt to use the Chromium OS
# Chroot path to the installed script. If all else fails, we use the
# stateful update script on the host.
stateful_update_path = path_util.FromChrootPath(
self.LOCAL_CHROOT_STATEFUL_UPDATE_PATH)
if not os.path.exists(stateful_update_path):
logging.warning('Could not find chroot stateful_update script in %s, '
'falling back to the client copy.', stateful_update_path)
stateful_update_path = os.path.join(self._dev_dir,
self.LOCAL_STATEFUL_UPDATE_FILENAME)
if os.path.exists(stateful_update_path):
logging.debug('Use stateful_update script in devserver path: %s',
stateful_update_path)
return True, stateful_update_path
logging.debug('Cannot find stateful_update script, will use the script '
'on the host')
return False, self.REMOTE_STATEFUL_UPDATE_PATH
else:
return True, stateful_update_path
def _TransferStatefulUpdate(self):
"""Transfer files for stateful update.
The stateful update bin and the corresponding payloads are copied to the
target remote device for stateful update.
"""
logging.debug('Checking whether file stateful_update_bin needs to be '
'transferred to device...')
need_transfer, stateful_update_bin = self._GetStatefulUpdateScript()
if need_transfer:
logging.info('Copying stateful_update binary to device...')
# stateful_update is a tiny uncompressed text file, so use rsync.
self._device.CopyToWorkDir(stateful_update_bin, mode='rsync',
log_output=True, **self._cmd_kwargs)
self._stateful_update_bin = os.path.join(
self._device.work_dir,
os.path.basename(self.LOCAL_CHROOT_STATEFUL_UPDATE_PATH))
else:
self._stateful_update_bin = stateful_update_bin
if self._original_payload_dir:
logging.info('Copying original stateful payload to device...')
original_payload = os.path.join(
self._original_payload_dir, STATEFUL_FILENAME)
self._EnsureDeviceDirectory(self._device_restore_dir)
self._device.CopyToDevice(original_payload, self._device_restore_dir,
mode=self._payload_mode, log_output=True,
**self._cmd_kwargs)
logging.info('Copying target stateful payload to device...')
payload = os.path.join(self._payload_dir, STATEFUL_FILENAME)
self._device.CopyToWorkDir(payload, mode=self._payload_mode,
log_output=True, **self._cmd_kwargs)