1#!/usr/bin/python2 2 3# Copyright (c) 2010 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7""" 8Sync all SCSI (USB/SATA), NVMe, and eMMC devices. All logging is via 9stdout and stderr, to avoid creating new disk writes on the DUT that would 10then need to be synced. 11 12If --freeze is set, this will also block writes to the stateful partition, 13to ensure the disk is in a consistent state before a hard reset. 14""" 15 16 17import argparse 18import collections 19import glob 20import logging 21import logging.handlers 22import os 23import subprocess 24import sys 25import six 26 27STATEFUL_MOUNT = '/mnt/stateful_partition' 28ENCSTATEFUL_DEV = '/dev/mapper/encstateful' 29ENCSTATEFUL_MOUNT = '/mnt/stateful_partition/encrypted' 30 31 32Result = collections.namedtuple('Result', ['command', 'rc', 'stdout', 'stderr']) 33 34 35def run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 36 strip=False): 37 """Run the given command, and return a Result (namedtuple) for it. 38 39 @param cmd: the command to run 40 @param stdout: an open file to capture stdout in, or subprocess.PIPE 41 @param stderr: an open file to capture stderr in, or subprocess.PIPE 42 @param strip: if True, remove certain escape sequences from stdout 43 @type stdout: file | int | None 44 @type stderr: file | int | None 45 """ 46 logging.info("+ %s", cmd) 47 48 proc = subprocess.Popen(cmd, shell=True, stdout=stdout, stderr=stderr) 49 (stdout, stderr) = proc.communicate() 50 if stdout is not None: 51 stdout = six.ensure_text(stdout, errors='replace') 52 if stdout: 53 if strip: 54 stdout = stdout.replace('\x1b[0m', '') 55 stdout = stdout.replace('\x1b[1m', '') 56 logging.debug(' stdout: %s', repr(stdout)) 57 if stderr is not None: 58 stderr = six.ensure_text(stderr, errors='replace') 59 if stderr: 60 logging.debug(' stderr: %s', repr(stderr)) 61 if proc.returncode != 0: 62 logging.debug(' rc: %s', proc.returncode) 63 return Result(cmd, proc.returncode, stdout, stderr) 64 65 66def run_background(cmd): 67 """Run a command in the background, with stdout, and stderr detached.""" 68 logging.info("+ %s &", cmd) 69 with open(os.devnull, 'w') as null: 70 subprocess.Popen(cmd, shell=True, stdout=null, stderr=null) 71 72 73def _freeze_fs(fs): 74 """Run fsfreeze --freeze or --unfreezeto block writes. 75 76 @param fs: the mountpoint path of the filesystem to freeze 77 """ 78 # ioctl: FIFREEZE 79 logging.warn("FREEZING THE FILESYSTEM: %s", fs) 80 run('fsfreeze --freeze %s' % fs) 81 82 83def _unfreeze_fs_later(fs): 84 """ Trigger a background (stdin/out/err closed) run of unfreeze later. 85 86 In case a test dies after freeze, this should prevent the freeze from 87 breaking the repair logic for a long time. 88 89 @param fs: the mountpoint path of the filesystem to unfreeze 90 """ 91 # ioctl: FITHAW 92 run_background('sleep 120 && fsfreeze --unfreeze %s' % fs) 93 94 95def _flush_blockdev(device, wildcard=None): 96 """Run /sbin/blockdev to flush buffers 97 98 @param device: The base block device (/dev/nvme0n1, /dev/mmcblk0, /dev/sda) 99 @param wildcard: The wildcard pattern to match and iterate. 100 (e.g. the 'p*' in '/dev/mmcblk0p*') 101 """ 102 # ioctl: BLKFLSBUF 103 run('blockdev --flushbufs %s' % device) 104 105 if wildcard: 106 partitions = glob.glob(device + wildcard) 107 if device in partitions: 108 # sda* matches sda too, so avoid flushing it twice 109 partitions.remove(device) 110 if partitions: 111 run('for part in %s; do blockdev --flushbufs $part; done' 112 % ' '.join(partitions)) 113 114 115def _do_blocking_sync(device): 116 """Run a blocking sync command. 117 118 'sync' only sends SYNCHRONIZE_CACHE but doesn't check the status. 119 This function will perform a device-specific sync command. 120 121 @param device: Name of the block dev: /dev/sda, /dev/nvme0n1, /dev/mmcblk0. 122 The value is assumed to be the full block device, 123 not a partition or the nvme controller char device. 124 """ 125 if 'mmcblk' in device: 126 # For mmc devices, use `mmc status get` command to send an 127 # empty command to wait for the disk to be available again. 128 129 # Flush device and partitions, ex. mmcblk0 and mmcblk0p1, mmcblk0p2, ... 130 _flush_blockdev(device, 'p*') 131 132 # mmc status get <device>: Print the response to STATUS_SEND (CMD13) 133 # ioctl: MMC_IOC_CMD, <hex value> 134 run('mmc status get %s' % device) 135 136 elif 'nvme' in device: 137 # For NVMe devices, use `nvme flush` command to commit data 138 # and metadata to non-volatile media. 139 140 # The flush command is sent to the namespace, not the char device: 141 # https://chromium.googlesource.com/chromiumos/third_party/kernel/+/bfd8947194b2e2a53db82bbc7eb7c15d028c46db 142 143 # Flush device and partitions, ex. nvme0n1, nvme0n1p1, nvme0n1p2, ... 144 _flush_blockdev(device, 'p*') 145 146 # Get a list of NVMe namespaces, and flush them individually. 147 # The output is assumed to be in the following format: 148 # [ 0]:0x1 149 # [ 1]:0x2 150 list_result = run("nvme list-ns %s" % device, strip=True) 151 available_ns = list_result.stdout.strip() 152 153 if list_result.rc != 0: 154 logging.warn("Listing namespaces failed (rc=%s); assuming default.", 155 list_result.rc) 156 available_ns = '' 157 158 elif available_ns.startswith('Usage:'): 159 logging.warn("Listing namespaces failed (just printed --help);" 160 " assuming default.") 161 available_ns = '' 162 163 elif not available_ns: 164 logging.warn("Listing namespaces failed (empty output).") 165 166 if not available_ns: 167 # -n Defaults to 0xffffffff, indicating flush for all namespaces. 168 flush_result = run('nvme flush %s' % device, strip=True) 169 170 if flush_result.rc != 0: 171 logging.warn("Flushing %s failed (rc=%s).", 172 device, flush_result.rc) 173 174 for line in available_ns.splitlines(): 175 ns = line.split(':')[-1] 176 177 # ioctl NVME_IOCTL_IO_CMD, <hex value> 178 flush_result = run('nvme flush %s -n %s' % (device, ns), strip=True) 179 180 if flush_result.rc != 0: 181 logging.warn("Flushing %s namespace %s failed (rc=%s).", 182 device, ns, flush_result.rc) 183 184 elif 'sd' in device: 185 # For other devices, use hdparm to attempt a sync. 186 187 # flush device and partitions, ex. sda, sda1, sda2, sda3, ... 188 _flush_blockdev(device, '*') 189 190 # -f Flush buffer cache for device on exit 191 # ioctl: BLKFLSBUF: flush buffer cache 192 # ioctl: HDIO_DRIVE_CMD(0): wait for flush complete (unsupported) 193 run('hdparm --verbose -f %s' % device, stderr=subprocess.PIPE) 194 195 # -F Flush drive write cache (unsupported on many flash drives) 196 # ioctl: SG_IO, ata_op=0xec (ATA_OP_IDENTIFY) 197 # ioctl: SG_IO, ata_op=0xea (ATA_OP_FLUSHCACHE_EXT) 198 # run('hdparm --verbose -F %s' % device, stderr=subprocess.PIPE) 199 200 else: 201 logging.warn("Unhandled device type: %s", device) 202 _flush_blockdev(device, '*') 203 204 205def blocking_sync(freeze=False): 206 """Sync all known disk devices. If freeze is True, also block writes.""" 207 208 # Reverse alphabetical order, to give USB more time: sd*, nvme*, mmcblk* 209 ls_result = run('ls /dev/mmcblk? /dev/nvme?n? /dev/sd? | sort -r') 210 211 devices = ls_result.stdout.splitlines() 212 if freeze: 213 description = 'Syncing and freezing device(s)' 214 else: 215 description = 'Syncing device(s)' 216 logging.info('%s: %s', description, ', '.join(devices) or '(none?)') 217 218 # The double call to sync fakes a blocking call. 219 # The first call returns before the flush is complete, 220 # but the second will wait for the first to finish. 221 run('sync && sync') 222 223 if freeze: 224 _unfreeze_fs_later(ENCSTATEFUL_MOUNT) 225 _freeze_fs(ENCSTATEFUL_MOUNT) 226 _flush_blockdev(ENCSTATEFUL_DEV) 227 228 _unfreeze_fs_later(STATEFUL_MOUNT) 229 _freeze_fs(STATEFUL_MOUNT) 230 # No need to figure out which partition is the stateful one, 231 # because _do_blocking_sync syncs every partition. 232 233 else: 234 _flush_blockdev(ENCSTATEFUL_DEV) 235 236 for dev in devices: 237 _do_blocking_sync(dev) 238 239 240def main(): 241 """Main method (see module docstring for purpose of this script)""" 242 parser = argparse.ArgumentParser(description=__doc__) 243 parser.add_argument('--freeze', '--for-reset', '--block-writes', 244 dest='freeze', action='store_true', 245 help='Block writes to prepare for hard reset.') 246 247 logging.root.setLevel(logging.NOTSET) 248 249 stdout_handler = logging.StreamHandler(stream=sys.stdout) 250 stdout_handler.setFormatter(logging.Formatter( 251 '%(asctime)s %(levelname)-5.5s| %(message)s')) 252 logging.root.addHandler(stdout_handler) 253 254 opts = parser.parse_args() 255 blocking_sync(freeze=opts.freeze) 256 257 258if __name__ == '__main__': 259 sys.exit(main()) 260