260 lines
9.0 KiB
Python
Executable File
260 lines
9.0 KiB
Python
Executable File
#!/usr/bin/python2
|
|
|
|
# Copyright (c) 2010 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.
|
|
|
|
"""
|
|
Sync all SCSI (USB/SATA), NVMe, and eMMC devices. All logging is via
|
|
stdout and stderr, to avoid creating new disk writes on the DUT that would
|
|
then need to be synced.
|
|
|
|
If --freeze is set, this will also block writes to the stateful partition,
|
|
to ensure the disk is in a consistent state before a hard reset.
|
|
"""
|
|
|
|
|
|
import argparse
|
|
import collections
|
|
import glob
|
|
import logging
|
|
import logging.handlers
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import six
|
|
|
|
STATEFUL_MOUNT = '/mnt/stateful_partition'
|
|
ENCSTATEFUL_DEV = '/dev/mapper/encstateful'
|
|
ENCSTATEFUL_MOUNT = '/mnt/stateful_partition/encrypted'
|
|
|
|
|
|
Result = collections.namedtuple('Result', ['command', 'rc', 'stdout', 'stderr'])
|
|
|
|
|
|
def run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
strip=False):
|
|
"""Run the given command, and return a Result (namedtuple) for it.
|
|
|
|
@param cmd: the command to run
|
|
@param stdout: an open file to capture stdout in, or subprocess.PIPE
|
|
@param stderr: an open file to capture stderr in, or subprocess.PIPE
|
|
@param strip: if True, remove certain escape sequences from stdout
|
|
@type stdout: file | int | None
|
|
@type stderr: file | int | None
|
|
"""
|
|
logging.info("+ %s", cmd)
|
|
|
|
proc = subprocess.Popen(cmd, shell=True, stdout=stdout, stderr=stderr)
|
|
(stdout, stderr) = proc.communicate()
|
|
if stdout is not None:
|
|
stdout = six.ensure_text(stdout, errors='replace')
|
|
if stdout:
|
|
if strip:
|
|
stdout = stdout.replace('\x1b[0m', '')
|
|
stdout = stdout.replace('\x1b[1m', '')
|
|
logging.debug(' stdout: %s', repr(stdout))
|
|
if stderr is not None:
|
|
stderr = six.ensure_text(stderr, errors='replace')
|
|
if stderr:
|
|
logging.debug(' stderr: %s', repr(stderr))
|
|
if proc.returncode != 0:
|
|
logging.debug(' rc: %s', proc.returncode)
|
|
return Result(cmd, proc.returncode, stdout, stderr)
|
|
|
|
|
|
def run_background(cmd):
|
|
"""Run a command in the background, with stdout, and stderr detached."""
|
|
logging.info("+ %s &", cmd)
|
|
with open(os.devnull, 'w') as null:
|
|
subprocess.Popen(cmd, shell=True, stdout=null, stderr=null)
|
|
|
|
|
|
def _freeze_fs(fs):
|
|
"""Run fsfreeze --freeze or --unfreezeto block writes.
|
|
|
|
@param fs: the mountpoint path of the filesystem to freeze
|
|
"""
|
|
# ioctl: FIFREEZE
|
|
logging.warn("FREEZING THE FILESYSTEM: %s", fs)
|
|
run('fsfreeze --freeze %s' % fs)
|
|
|
|
|
|
def _unfreeze_fs_later(fs):
|
|
""" Trigger a background (stdin/out/err closed) run of unfreeze later.
|
|
|
|
In case a test dies after freeze, this should prevent the freeze from
|
|
breaking the repair logic for a long time.
|
|
|
|
@param fs: the mountpoint path of the filesystem to unfreeze
|
|
"""
|
|
# ioctl: FITHAW
|
|
run_background('sleep 120 && fsfreeze --unfreeze %s' % fs)
|
|
|
|
|
|
def _flush_blockdev(device, wildcard=None):
|
|
"""Run /sbin/blockdev to flush buffers
|
|
|
|
@param device: The base block device (/dev/nvme0n1, /dev/mmcblk0, /dev/sda)
|
|
@param wildcard: The wildcard pattern to match and iterate.
|
|
(e.g. the 'p*' in '/dev/mmcblk0p*')
|
|
"""
|
|
# ioctl: BLKFLSBUF
|
|
run('blockdev --flushbufs %s' % device)
|
|
|
|
if wildcard:
|
|
partitions = glob.glob(device + wildcard)
|
|
if device in partitions:
|
|
# sda* matches sda too, so avoid flushing it twice
|
|
partitions.remove(device)
|
|
if partitions:
|
|
run('for part in %s; do blockdev --flushbufs $part; done'
|
|
% ' '.join(partitions))
|
|
|
|
|
|
def _do_blocking_sync(device):
|
|
"""Run a blocking sync command.
|
|
|
|
'sync' only sends SYNCHRONIZE_CACHE but doesn't check the status.
|
|
This function will perform a device-specific sync command.
|
|
|
|
@param device: Name of the block dev: /dev/sda, /dev/nvme0n1, /dev/mmcblk0.
|
|
The value is assumed to be the full block device,
|
|
not a partition or the nvme controller char device.
|
|
"""
|
|
if 'mmcblk' in device:
|
|
# For mmc devices, use `mmc status get` command to send an
|
|
# empty command to wait for the disk to be available again.
|
|
|
|
# Flush device and partitions, ex. mmcblk0 and mmcblk0p1, mmcblk0p2, ...
|
|
_flush_blockdev(device, 'p*')
|
|
|
|
# mmc status get <device>: Print the response to STATUS_SEND (CMD13)
|
|
# ioctl: MMC_IOC_CMD, <hex value>
|
|
run('mmc status get %s' % device)
|
|
|
|
elif 'nvme' in device:
|
|
# For NVMe devices, use `nvme flush` command to commit data
|
|
# and metadata to non-volatile media.
|
|
|
|
# The flush command is sent to the namespace, not the char device:
|
|
# https://chromium.googlesource.com/chromiumos/third_party/kernel/+/bfd8947194b2e2a53db82bbc7eb7c15d028c46db
|
|
|
|
# Flush device and partitions, ex. nvme0n1, nvme0n1p1, nvme0n1p2, ...
|
|
_flush_blockdev(device, 'p*')
|
|
|
|
# Get a list of NVMe namespaces, and flush them individually.
|
|
# The output is assumed to be in the following format:
|
|
# [ 0]:0x1
|
|
# [ 1]:0x2
|
|
list_result = run("nvme list-ns %s" % device, strip=True)
|
|
available_ns = list_result.stdout.strip()
|
|
|
|
if list_result.rc != 0:
|
|
logging.warn("Listing namespaces failed (rc=%s); assuming default.",
|
|
list_result.rc)
|
|
available_ns = ''
|
|
|
|
elif available_ns.startswith('Usage:'):
|
|
logging.warn("Listing namespaces failed (just printed --help);"
|
|
" assuming default.")
|
|
available_ns = ''
|
|
|
|
elif not available_ns:
|
|
logging.warn("Listing namespaces failed (empty output).")
|
|
|
|
if not available_ns:
|
|
# -n Defaults to 0xffffffff, indicating flush for all namespaces.
|
|
flush_result = run('nvme flush %s' % device, strip=True)
|
|
|
|
if flush_result.rc != 0:
|
|
logging.warn("Flushing %s failed (rc=%s).",
|
|
device, flush_result.rc)
|
|
|
|
for line in available_ns.splitlines():
|
|
ns = line.split(':')[-1]
|
|
|
|
# ioctl NVME_IOCTL_IO_CMD, <hex value>
|
|
flush_result = run('nvme flush %s -n %s' % (device, ns), strip=True)
|
|
|
|
if flush_result.rc != 0:
|
|
logging.warn("Flushing %s namespace %s failed (rc=%s).",
|
|
device, ns, flush_result.rc)
|
|
|
|
elif 'sd' in device:
|
|
# For other devices, use hdparm to attempt a sync.
|
|
|
|
# flush device and partitions, ex. sda, sda1, sda2, sda3, ...
|
|
_flush_blockdev(device, '*')
|
|
|
|
# -f Flush buffer cache for device on exit
|
|
# ioctl: BLKFLSBUF: flush buffer cache
|
|
# ioctl: HDIO_DRIVE_CMD(0): wait for flush complete (unsupported)
|
|
run('hdparm --verbose -f %s' % device, stderr=subprocess.PIPE)
|
|
|
|
# -F Flush drive write cache (unsupported on many flash drives)
|
|
# ioctl: SG_IO, ata_op=0xec (ATA_OP_IDENTIFY)
|
|
# ioctl: SG_IO, ata_op=0xea (ATA_OP_FLUSHCACHE_EXT)
|
|
# run('hdparm --verbose -F %s' % device, stderr=subprocess.PIPE)
|
|
|
|
else:
|
|
logging.warn("Unhandled device type: %s", device)
|
|
_flush_blockdev(device, '*')
|
|
|
|
|
|
def blocking_sync(freeze=False):
|
|
"""Sync all known disk devices. If freeze is True, also block writes."""
|
|
|
|
# Reverse alphabetical order, to give USB more time: sd*, nvme*, mmcblk*
|
|
ls_result = run('ls /dev/mmcblk? /dev/nvme?n? /dev/sd? | sort -r')
|
|
|
|
devices = ls_result.stdout.splitlines()
|
|
if freeze:
|
|
description = 'Syncing and freezing device(s)'
|
|
else:
|
|
description = 'Syncing device(s)'
|
|
logging.info('%s: %s', description, ', '.join(devices) or '(none?)')
|
|
|
|
# The double call to sync fakes a blocking call.
|
|
# The first call returns before the flush is complete,
|
|
# but the second will wait for the first to finish.
|
|
run('sync && sync')
|
|
|
|
if freeze:
|
|
_unfreeze_fs_later(ENCSTATEFUL_MOUNT)
|
|
_freeze_fs(ENCSTATEFUL_MOUNT)
|
|
_flush_blockdev(ENCSTATEFUL_DEV)
|
|
|
|
_unfreeze_fs_later(STATEFUL_MOUNT)
|
|
_freeze_fs(STATEFUL_MOUNT)
|
|
# No need to figure out which partition is the stateful one,
|
|
# because _do_blocking_sync syncs every partition.
|
|
|
|
else:
|
|
_flush_blockdev(ENCSTATEFUL_DEV)
|
|
|
|
for dev in devices:
|
|
_do_blocking_sync(dev)
|
|
|
|
|
|
def main():
|
|
"""Main method (see module docstring for purpose of this script)"""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--freeze', '--for-reset', '--block-writes',
|
|
dest='freeze', action='store_true',
|
|
help='Block writes to prepare for hard reset.')
|
|
|
|
logging.root.setLevel(logging.NOTSET)
|
|
|
|
stdout_handler = logging.StreamHandler(stream=sys.stdout)
|
|
stdout_handler.setFormatter(logging.Formatter(
|
|
'%(asctime)s %(levelname)-5.5s| %(message)s'))
|
|
logging.root.addHandler(stdout_handler)
|
|
|
|
opts = parser.parse_args()
|
|
blocking_sync(freeze=opts.freeze)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|