Philippe Mathieu-Daudé | 903cb1b | 2020-01-30 17:32:23 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 2 | # |
| 3 | # Tool to manipulate QED image files |
| 4 | # |
| 5 | # Copyright (C) 2010 IBM, Corp. |
| 6 | # |
| 7 | # Authors: |
| 8 | # Stefan Hajnoczi <stefanha@linux.vnet.ibm.com> |
| 9 | # |
| 10 | # This work is licensed under the terms of the GNU GPL, version 2 or later. |
| 11 | # See the COPYING file in the top-level directory. |
| 12 | |
| 13 | import sys |
| 14 | import struct |
| 15 | import random |
| 16 | import optparse |
| 17 | |
| 18 | # This can be used as a module |
| 19 | __all__ = ['QED_F_NEED_CHECK', 'QED'] |
| 20 | |
| 21 | QED_F_NEED_CHECK = 0x02 |
| 22 | |
| 23 | header_fmt = '<IIIIQQQQQII' |
| 24 | header_size = struct.calcsize(header_fmt) |
| 25 | field_names = ['magic', 'cluster_size', 'table_size', |
| 26 | 'header_size', 'features', 'compat_features', |
| 27 | 'autoclear_features', 'l1_table_offset', 'image_size', |
| 28 | 'backing_filename_offset', 'backing_filename_size'] |
| 29 | table_elem_fmt = '<Q' |
| 30 | table_elem_size = struct.calcsize(table_elem_fmt) |
| 31 | |
| 32 | def err(msg): |
| 33 | sys.stderr.write(msg + '\n') |
| 34 | sys.exit(1) |
| 35 | |
| 36 | def unpack_header(s): |
| 37 | fields = struct.unpack(header_fmt, s) |
| 38 | return dict((field_names[idx], val) for idx, val in enumerate(fields)) |
| 39 | |
| 40 | def pack_header(header): |
| 41 | fields = tuple(header[x] for x in field_names) |
| 42 | return struct.pack(header_fmt, *fields) |
| 43 | |
| 44 | def unpack_table_elem(s): |
| 45 | return struct.unpack(table_elem_fmt, s)[0] |
| 46 | |
| 47 | def pack_table_elem(elem): |
| 48 | return struct.pack(table_elem_fmt, elem) |
| 49 | |
| 50 | class QED(object): |
| 51 | def __init__(self, f): |
| 52 | self.f = f |
| 53 | |
| 54 | self.f.seek(0, 2) |
| 55 | self.filesize = f.tell() |
| 56 | |
| 57 | self.load_header() |
| 58 | self.load_l1_table() |
| 59 | |
| 60 | def raw_pread(self, offset, size): |
| 61 | self.f.seek(offset) |
| 62 | return self.f.read(size) |
| 63 | |
| 64 | def raw_pwrite(self, offset, data): |
| 65 | self.f.seek(offset) |
| 66 | return self.f.write(data) |
| 67 | |
| 68 | def load_header(self): |
| 69 | self.header = unpack_header(self.raw_pread(0, header_size)) |
| 70 | |
| 71 | def store_header(self): |
| 72 | self.raw_pwrite(0, pack_header(self.header)) |
| 73 | |
| 74 | def read_table(self, offset): |
| 75 | size = self.header['table_size'] * self.header['cluster_size'] |
| 76 | s = self.raw_pread(offset, size) |
| 77 | table = [unpack_table_elem(s[i:i + table_elem_size]) for i in xrange(0, size, table_elem_size)] |
| 78 | return table |
| 79 | |
| 80 | def load_l1_table(self): |
| 81 | self.l1_table = self.read_table(self.header['l1_table_offset']) |
Max Reitz | 9a3a9a6 | 2018-10-22 14:53:02 +0100 | [diff] [blame] | 82 | self.table_nelems = self.header['table_size'] * self.header['cluster_size'] // table_elem_size |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 83 | |
| 84 | def write_table(self, offset, table): |
| 85 | s = ''.join(pack_table_elem(x) for x in table) |
| 86 | self.raw_pwrite(offset, s) |
| 87 | |
| 88 | def random_table_item(table): |
| 89 | vals = [(index, offset) for index, offset in enumerate(table) if offset != 0] |
| 90 | if not vals: |
| 91 | err('cannot pick random item because table is empty') |
| 92 | return random.choice(vals) |
| 93 | |
| 94 | def corrupt_table_duplicate(table): |
| 95 | '''Corrupt a table by introducing a duplicate offset''' |
| 96 | victim_idx, victim_val = random_table_item(table) |
| 97 | unique_vals = set(table) |
| 98 | if len(unique_vals) == 1: |
| 99 | err('no duplication corruption possible in table') |
| 100 | dup_val = random.choice(list(unique_vals.difference([victim_val]))) |
| 101 | table[victim_idx] = dup_val |
| 102 | |
| 103 | def corrupt_table_invalidate(qed, table): |
| 104 | '''Corrupt a table by introducing an invalid offset''' |
| 105 | index, _ = random_table_item(table) |
| 106 | table[index] = qed.filesize + random.randint(0, 100 * 1024 * 1024 * 1024 * 1024) |
| 107 | |
| 108 | def cmd_show(qed, *args): |
| 109 | '''show [header|l1|l2 <offset>]- Show header or l1/l2 tables''' |
| 110 | if not args or args[0] == 'header': |
Eduardo Habkost | f03868b | 2018-06-08 09:29:43 -0300 | [diff] [blame] | 111 | print(qed.header) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 112 | elif args[0] == 'l1': |
Eduardo Habkost | f03868b | 2018-06-08 09:29:43 -0300 | [diff] [blame] | 113 | print(qed.l1_table) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 114 | elif len(args) == 2 and args[0] == 'l2': |
| 115 | offset = int(args[1]) |
Eduardo Habkost | f03868b | 2018-06-08 09:29:43 -0300 | [diff] [blame] | 116 | print(qed.read_table(offset)) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 117 | else: |
| 118 | err('unrecognized sub-command') |
| 119 | |
| 120 | def cmd_duplicate(qed, table_level): |
| 121 | '''duplicate l1|l2 - Duplicate a random table element''' |
| 122 | if table_level == 'l1': |
| 123 | offset = qed.header['l1_table_offset'] |
| 124 | table = qed.l1_table |
| 125 | elif table_level == 'l2': |
| 126 | _, offset = random_table_item(qed.l1_table) |
| 127 | table = qed.read_table(offset) |
| 128 | else: |
| 129 | err('unrecognized sub-command') |
| 130 | corrupt_table_duplicate(table) |
| 131 | qed.write_table(offset, table) |
| 132 | |
| 133 | def cmd_invalidate(qed, table_level): |
| 134 | '''invalidate l1|l2 - Plant an invalid table element at random''' |
| 135 | if table_level == 'l1': |
| 136 | offset = qed.header['l1_table_offset'] |
| 137 | table = qed.l1_table |
| 138 | elif table_level == 'l2': |
| 139 | _, offset = random_table_item(qed.l1_table) |
| 140 | table = qed.read_table(offset) |
| 141 | else: |
| 142 | err('unrecognized sub-command') |
| 143 | corrupt_table_invalidate(qed, table) |
| 144 | qed.write_table(offset, table) |
| 145 | |
| 146 | def cmd_need_check(qed, *args): |
| 147 | '''need-check [on|off] - Test, set, or clear the QED_F_NEED_CHECK header bit''' |
| 148 | if not args: |
Eduardo Habkost | f03868b | 2018-06-08 09:29:43 -0300 | [diff] [blame] | 149 | print(bool(qed.header['features'] & QED_F_NEED_CHECK)) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 150 | return |
| 151 | |
| 152 | if args[0] == 'on': |
| 153 | qed.header['features'] |= QED_F_NEED_CHECK |
| 154 | elif args[0] == 'off': |
| 155 | qed.header['features'] &= ~QED_F_NEED_CHECK |
| 156 | else: |
| 157 | err('unrecognized sub-command') |
| 158 | qed.store_header() |
| 159 | |
| 160 | def cmd_zero_cluster(qed, pos, *args): |
| 161 | '''zero-cluster <pos> [<n>] - Zero data clusters''' |
| 162 | pos, n = int(pos), 1 |
| 163 | if args: |
| 164 | if len(args) != 1: |
| 165 | err('expected one argument') |
| 166 | n = int(args[0]) |
| 167 | |
| 168 | for i in xrange(n): |
Max Reitz | 9a3a9a6 | 2018-10-22 14:53:02 +0100 | [diff] [blame] | 169 | l1_index = pos // qed.header['cluster_size'] // len(qed.l1_table) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 170 | if qed.l1_table[l1_index] == 0: |
| 171 | err('no l2 table allocated') |
| 172 | |
| 173 | l2_offset = qed.l1_table[l1_index] |
| 174 | l2_table = qed.read_table(l2_offset) |
| 175 | |
Max Reitz | 9a3a9a6 | 2018-10-22 14:53:02 +0100 | [diff] [blame] | 176 | l2_index = (pos // qed.header['cluster_size']) % len(qed.l1_table) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 177 | l2_table[l2_index] = 1 # zero the data cluster |
| 178 | qed.write_table(l2_offset, l2_table) |
| 179 | pos += qed.header['cluster_size'] |
| 180 | |
| 181 | def cmd_copy_metadata(qed, outfile): |
| 182 | '''copy-metadata <outfile> - Copy metadata only (for scrubbing corrupted images)''' |
| 183 | out = open(outfile, 'wb') |
| 184 | |
| 185 | # Match file size |
| 186 | out.seek(qed.filesize - 1) |
| 187 | out.write('\0') |
| 188 | |
| 189 | # Copy header clusters |
| 190 | out.seek(0) |
| 191 | header_size_bytes = qed.header['header_size'] * qed.header['cluster_size'] |
| 192 | out.write(qed.raw_pread(0, header_size_bytes)) |
| 193 | |
| 194 | # Copy L1 table |
| 195 | out.seek(qed.header['l1_table_offset']) |
| 196 | s = ''.join(pack_table_elem(x) for x in qed.l1_table) |
| 197 | out.write(s) |
| 198 | |
| 199 | # Copy L2 tables |
| 200 | for l2_offset in qed.l1_table: |
| 201 | if l2_offset == 0: |
| 202 | continue |
| 203 | l2_table = qed.read_table(l2_offset) |
| 204 | out.seek(l2_offset) |
| 205 | s = ''.join(pack_table_elem(x) for x in l2_table) |
| 206 | out.write(s) |
| 207 | |
| 208 | out.close() |
| 209 | |
| 210 | def usage(): |
Eduardo Habkost | f03868b | 2018-06-08 09:29:43 -0300 | [diff] [blame] | 211 | print('Usage: %s <file> <cmd> [<arg>, ...]' % sys.argv[0]) |
| 212 | print() |
| 213 | print('Supported commands:') |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 214 | for cmd in sorted(x for x in globals() if x.startswith('cmd_')): |
Eduardo Habkost | f03868b | 2018-06-08 09:29:43 -0300 | [diff] [blame] | 215 | print(globals()[cmd].__doc__) |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 216 | sys.exit(1) |
| 217 | |
| 218 | def main(): |
| 219 | if len(sys.argv) < 3: |
| 220 | usage() |
| 221 | filename, cmd = sys.argv[1:3] |
| 222 | |
| 223 | cmd = 'cmd_' + cmd.replace('-', '_') |
| 224 | if cmd not in globals(): |
| 225 | usage() |
| 226 | |
| 227 | qed = QED(open(filename, 'r+b')) |
| 228 | try: |
| 229 | globals()[cmd](qed, *sys.argv[3:]) |
Markus Armbruster | 03e1881 | 2015-12-18 08:52:44 +0100 | [diff] [blame] | 230 | except TypeError as e: |
Stefan Hajnoczi | e77964f | 2012-07-27 09:01:27 +0100 | [diff] [blame] | 231 | sys.stderr.write(globals()[cmd].__doc__ + '\n') |
| 232 | sys.exit(1) |
| 233 | |
| 234 | if __name__ == '__main__': |
| 235 | main() |