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