9a3a9a636e
In Python 3, / is always a floating-point division. We usually do not want this, and as Python 2.7 understands // as well, change all integer divisions to use that. Signed-off-by: Max Reitz <mreitz@redhat.com> Reviewed-by: Eduardo Habkost <ehabkost@redhat.com> Reviewed-by: Cleber Rosa <crosa@redhat.com> Message-Id: <20181022135307.14398-5-mreitz@redhat.com> Signed-off-by: Eduardo Habkost <ehabkost@redhat.com>
237 lines
7.1 KiB
Python
Executable File
237 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Tool to manipulate QED image files
|
|
#
|
|
# Copyright (C) 2010 IBM, Corp.
|
|
#
|
|
# Authors:
|
|
# Stefan Hajnoczi <stefanha@linux.vnet.ibm.com>
|
|
#
|
|
# This work is licensed under the terms of the GNU GPL, version 2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
from __future__ import print_function
|
|
import sys
|
|
import struct
|
|
import random
|
|
import optparse
|
|
|
|
# This can be used as a module
|
|
__all__ = ['QED_F_NEED_CHECK', 'QED']
|
|
|
|
QED_F_NEED_CHECK = 0x02
|
|
|
|
header_fmt = '<IIIIQQQQQII'
|
|
header_size = struct.calcsize(header_fmt)
|
|
field_names = ['magic', 'cluster_size', 'table_size',
|
|
'header_size', 'features', 'compat_features',
|
|
'autoclear_features', 'l1_table_offset', 'image_size',
|
|
'backing_filename_offset', 'backing_filename_size']
|
|
table_elem_fmt = '<Q'
|
|
table_elem_size = struct.calcsize(table_elem_fmt)
|
|
|
|
def err(msg):
|
|
sys.stderr.write(msg + '\n')
|
|
sys.exit(1)
|
|
|
|
def unpack_header(s):
|
|
fields = struct.unpack(header_fmt, s)
|
|
return dict((field_names[idx], val) for idx, val in enumerate(fields))
|
|
|
|
def pack_header(header):
|
|
fields = tuple(header[x] for x in field_names)
|
|
return struct.pack(header_fmt, *fields)
|
|
|
|
def unpack_table_elem(s):
|
|
return struct.unpack(table_elem_fmt, s)[0]
|
|
|
|
def pack_table_elem(elem):
|
|
return struct.pack(table_elem_fmt, elem)
|
|
|
|
class QED(object):
|
|
def __init__(self, f):
|
|
self.f = f
|
|
|
|
self.f.seek(0, 2)
|
|
self.filesize = f.tell()
|
|
|
|
self.load_header()
|
|
self.load_l1_table()
|
|
|
|
def raw_pread(self, offset, size):
|
|
self.f.seek(offset)
|
|
return self.f.read(size)
|
|
|
|
def raw_pwrite(self, offset, data):
|
|
self.f.seek(offset)
|
|
return self.f.write(data)
|
|
|
|
def load_header(self):
|
|
self.header = unpack_header(self.raw_pread(0, header_size))
|
|
|
|
def store_header(self):
|
|
self.raw_pwrite(0, pack_header(self.header))
|
|
|
|
def read_table(self, offset):
|
|
size = self.header['table_size'] * self.header['cluster_size']
|
|
s = self.raw_pread(offset, size)
|
|
table = [unpack_table_elem(s[i:i + table_elem_size]) for i in xrange(0, size, table_elem_size)]
|
|
return table
|
|
|
|
def load_l1_table(self):
|
|
self.l1_table = self.read_table(self.header['l1_table_offset'])
|
|
self.table_nelems = self.header['table_size'] * self.header['cluster_size'] // table_elem_size
|
|
|
|
def write_table(self, offset, table):
|
|
s = ''.join(pack_table_elem(x) for x in table)
|
|
self.raw_pwrite(offset, s)
|
|
|
|
def random_table_item(table):
|
|
vals = [(index, offset) for index, offset in enumerate(table) if offset != 0]
|
|
if not vals:
|
|
err('cannot pick random item because table is empty')
|
|
return random.choice(vals)
|
|
|
|
def corrupt_table_duplicate(table):
|
|
'''Corrupt a table by introducing a duplicate offset'''
|
|
victim_idx, victim_val = random_table_item(table)
|
|
unique_vals = set(table)
|
|
if len(unique_vals) == 1:
|
|
err('no duplication corruption possible in table')
|
|
dup_val = random.choice(list(unique_vals.difference([victim_val])))
|
|
table[victim_idx] = dup_val
|
|
|
|
def corrupt_table_invalidate(qed, table):
|
|
'''Corrupt a table by introducing an invalid offset'''
|
|
index, _ = random_table_item(table)
|
|
table[index] = qed.filesize + random.randint(0, 100 * 1024 * 1024 * 1024 * 1024)
|
|
|
|
def cmd_show(qed, *args):
|
|
'''show [header|l1|l2 <offset>]- Show header or l1/l2 tables'''
|
|
if not args or args[0] == 'header':
|
|
print(qed.header)
|
|
elif args[0] == 'l1':
|
|
print(qed.l1_table)
|
|
elif len(args) == 2 and args[0] == 'l2':
|
|
offset = int(args[1])
|
|
print(qed.read_table(offset))
|
|
else:
|
|
err('unrecognized sub-command')
|
|
|
|
def cmd_duplicate(qed, table_level):
|
|
'''duplicate l1|l2 - Duplicate a random table element'''
|
|
if table_level == 'l1':
|
|
offset = qed.header['l1_table_offset']
|
|
table = qed.l1_table
|
|
elif table_level == 'l2':
|
|
_, offset = random_table_item(qed.l1_table)
|
|
table = qed.read_table(offset)
|
|
else:
|
|
err('unrecognized sub-command')
|
|
corrupt_table_duplicate(table)
|
|
qed.write_table(offset, table)
|
|
|
|
def cmd_invalidate(qed, table_level):
|
|
'''invalidate l1|l2 - Plant an invalid table element at random'''
|
|
if table_level == 'l1':
|
|
offset = qed.header['l1_table_offset']
|
|
table = qed.l1_table
|
|
elif table_level == 'l2':
|
|
_, offset = random_table_item(qed.l1_table)
|
|
table = qed.read_table(offset)
|
|
else:
|
|
err('unrecognized sub-command')
|
|
corrupt_table_invalidate(qed, table)
|
|
qed.write_table(offset, table)
|
|
|
|
def cmd_need_check(qed, *args):
|
|
'''need-check [on|off] - Test, set, or clear the QED_F_NEED_CHECK header bit'''
|
|
if not args:
|
|
print(bool(qed.header['features'] & QED_F_NEED_CHECK))
|
|
return
|
|
|
|
if args[0] == 'on':
|
|
qed.header['features'] |= QED_F_NEED_CHECK
|
|
elif args[0] == 'off':
|
|
qed.header['features'] &= ~QED_F_NEED_CHECK
|
|
else:
|
|
err('unrecognized sub-command')
|
|
qed.store_header()
|
|
|
|
def cmd_zero_cluster(qed, pos, *args):
|
|
'''zero-cluster <pos> [<n>] - Zero data clusters'''
|
|
pos, n = int(pos), 1
|
|
if args:
|
|
if len(args) != 1:
|
|
err('expected one argument')
|
|
n = int(args[0])
|
|
|
|
for i in xrange(n):
|
|
l1_index = pos // qed.header['cluster_size'] // len(qed.l1_table)
|
|
if qed.l1_table[l1_index] == 0:
|
|
err('no l2 table allocated')
|
|
|
|
l2_offset = qed.l1_table[l1_index]
|
|
l2_table = qed.read_table(l2_offset)
|
|
|
|
l2_index = (pos // qed.header['cluster_size']) % len(qed.l1_table)
|
|
l2_table[l2_index] = 1 # zero the data cluster
|
|
qed.write_table(l2_offset, l2_table)
|
|
pos += qed.header['cluster_size']
|
|
|
|
def cmd_copy_metadata(qed, outfile):
|
|
'''copy-metadata <outfile> - Copy metadata only (for scrubbing corrupted images)'''
|
|
out = open(outfile, 'wb')
|
|
|
|
# Match file size
|
|
out.seek(qed.filesize - 1)
|
|
out.write('\0')
|
|
|
|
# Copy header clusters
|
|
out.seek(0)
|
|
header_size_bytes = qed.header['header_size'] * qed.header['cluster_size']
|
|
out.write(qed.raw_pread(0, header_size_bytes))
|
|
|
|
# Copy L1 table
|
|
out.seek(qed.header['l1_table_offset'])
|
|
s = ''.join(pack_table_elem(x) for x in qed.l1_table)
|
|
out.write(s)
|
|
|
|
# Copy L2 tables
|
|
for l2_offset in qed.l1_table:
|
|
if l2_offset == 0:
|
|
continue
|
|
l2_table = qed.read_table(l2_offset)
|
|
out.seek(l2_offset)
|
|
s = ''.join(pack_table_elem(x) for x in l2_table)
|
|
out.write(s)
|
|
|
|
out.close()
|
|
|
|
def usage():
|
|
print('Usage: %s <file> <cmd> [<arg>, ...]' % sys.argv[0])
|
|
print()
|
|
print('Supported commands:')
|
|
for cmd in sorted(x for x in globals() if x.startswith('cmd_')):
|
|
print(globals()[cmd].__doc__)
|
|
sys.exit(1)
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
usage()
|
|
filename, cmd = sys.argv[1:3]
|
|
|
|
cmd = 'cmd_' + cmd.replace('-', '_')
|
|
if cmd not in globals():
|
|
usage()
|
|
|
|
qed = QED(open(filename, 'r+b'))
|
|
try:
|
|
globals()[cmd](qed, *sys.argv[3:])
|
|
except TypeError as e:
|
|
sys.stderr.write(globals()[cmd].__doc__ + '\n')
|
|
sys.exit(1)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|