blob: fbdd057653a6975d4d0774056054e6be51e08dc4 [file] [log] [blame]
#!/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())