| #!/usr/bin/env python |
| |
| import sys |
| import struct |
| import string |
| |
| class QcowHeaderExtension: |
| |
| def __init__(self, magic, length, data): |
| if length % 8 != 0: |
| padding = 8 - (length % 8) |
| data += "\0" * padding |
| |
| self.magic = magic |
| self.length = length |
| self.data = data |
| |
| @classmethod |
| def create(cls, magic, data): |
| return QcowHeaderExtension(magic, len(data), data) |
| |
| class QcowHeader: |
| |
| uint32_t = 'I' |
| uint64_t = 'Q' |
| |
| fields = [ |
| # Version 2 header fields |
| [ uint32_t, '%#x', 'magic' ], |
| [ uint32_t, '%d', 'version' ], |
| [ uint64_t, '%#x', 'backing_file_offset' ], |
| [ uint32_t, '%#x', 'backing_file_size' ], |
| [ uint32_t, '%d', 'cluster_bits' ], |
| [ uint64_t, '%d', 'size' ], |
| [ uint32_t, '%d', 'crypt_method' ], |
| [ uint32_t, '%d', 'l1_size' ], |
| [ uint64_t, '%#x', 'l1_table_offset' ], |
| [ uint64_t, '%#x', 'refcount_table_offset' ], |
| [ uint32_t, '%d', 'refcount_table_clusters' ], |
| [ uint32_t, '%d', 'nb_snapshots' ], |
| [ uint64_t, '%#x', 'snapshot_offset' ], |
| |
| # Version 3 header fields |
| [ uint64_t, '%#x', 'incompatible_features' ], |
| [ uint64_t, '%#x', 'compatible_features' ], |
| [ uint64_t, '%#x', 'autoclear_features' ], |
| [ uint32_t, '%d', 'refcount_order' ], |
| [ uint32_t, '%d', 'header_length' ], |
| ]; |
| |
| fmt = '>' + ''.join(field[0] for field in fields) |
| |
| def __init__(self, fd): |
| |
| buf_size = struct.calcsize(QcowHeader.fmt) |
| |
| fd.seek(0) |
| buf = fd.read(buf_size) |
| |
| header = struct.unpack(QcowHeader.fmt, buf) |
| self.__dict__ = dict((field[2], header[i]) |
| for i, field in enumerate(QcowHeader.fields)) |
| |
| self.set_defaults() |
| self.cluster_size = 1 << self.cluster_bits |
| |
| fd.seek(self.header_length) |
| self.load_extensions(fd) |
| |
| if self.backing_file_offset: |
| fd.seek(self.backing_file_offset) |
| self.backing_file = fd.read(self.backing_file_size) |
| else: |
| self.backing_file = None |
| |
| def set_defaults(self): |
| if self.version == 2: |
| self.incompatible_features = 0 |
| self.compatible_features = 0 |
| self.autoclear_features = 0 |
| self.refcount_order = 4 |
| self.header_length = 72 |
| |
| def load_extensions(self, fd): |
| self.extensions = [] |
| |
| if self.backing_file_offset != 0: |
| end = min(self.cluster_size, self.backing_file_offset) |
| else: |
| end = self.cluster_size |
| |
| while fd.tell() < end: |
| (magic, length) = struct.unpack('>II', fd.read(8)) |
| if magic == 0: |
| break |
| else: |
| padded = (length + 7) & ~7 |
| data = fd.read(padded) |
| self.extensions.append(QcowHeaderExtension(magic, length, data)) |
| |
| def update_extensions(self, fd): |
| |
| fd.seek(self.header_length) |
| extensions = self.extensions |
| extensions.append(QcowHeaderExtension(0, 0, "")) |
| for ex in extensions: |
| buf = struct.pack('>II', ex.magic, ex.length) |
| fd.write(buf) |
| fd.write(ex.data) |
| |
| if self.backing_file != None: |
| self.backing_file_offset = fd.tell() |
| fd.write(self.backing_file) |
| |
| if fd.tell() > self.cluster_size: |
| raise Exception("I think I just broke the image...") |
| |
| |
| def update(self, fd): |
| header_bytes = self.header_length |
| |
| self.update_extensions(fd) |
| |
| fd.seek(0) |
| header = tuple(self.__dict__[f] for t, p, f in QcowHeader.fields) |
| buf = struct.pack(QcowHeader.fmt, *header) |
| buf = buf[0:header_bytes-1] |
| fd.write(buf) |
| |
| def dump(self): |
| for f in QcowHeader.fields: |
| print "%-25s" % f[2], f[1] % self.__dict__[f[2]] |
| print "" |
| |
| def dump_extensions(self): |
| for ex in self.extensions: |
| |
| data = ex.data[:ex.length] |
| if all(c in string.printable for c in data): |
| data = "'%s'" % data |
| else: |
| data = "<binary>" |
| |
| print "Header extension:" |
| print "%-25s %#x" % ("magic", ex.magic) |
| print "%-25s %d" % ("length", ex.length) |
| print "%-25s %s" % ("data", data) |
| print "" |
| |
| |
| def cmd_dump_header(fd): |
| h = QcowHeader(fd) |
| h.dump() |
| h.dump_extensions() |
| |
| def cmd_set_header(fd, name, value): |
| try: |
| value = int(value, 0) |
| except: |
| print "'%s' is not a valid number" % value |
| sys.exit(1) |
| |
| fields = (field[2] for field in QcowHeader.fields) |
| if not name in fields: |
| print "'%s' is not a known header field" % name |
| sys.exit(1) |
| |
| h = QcowHeader(fd) |
| h.__dict__[name] = value |
| h.update(fd) |
| |
| def cmd_add_header_ext(fd, magic, data): |
| try: |
| magic = int(magic, 0) |
| except: |
| print "'%s' is not a valid magic number" % magic |
| sys.exit(1) |
| |
| h = QcowHeader(fd) |
| h.extensions.append(QcowHeaderExtension.create(magic, data)) |
| h.update(fd) |
| |
| def cmd_add_header_ext_stdio(fd, magic): |
| data = sys.stdin.read() |
| cmd_add_header_ext(fd, magic, data) |
| |
| def cmd_del_header_ext(fd, magic): |
| try: |
| magic = int(magic, 0) |
| except: |
| print "'%s' is not a valid magic number" % magic |
| sys.exit(1) |
| |
| h = QcowHeader(fd) |
| found = False |
| |
| for ex in h.extensions: |
| if ex.magic == magic: |
| found = True |
| h.extensions.remove(ex) |
| |
| if not found: |
| print "No such header extension" |
| return |
| |
| h.update(fd) |
| |
| def cmd_set_feature_bit(fd, group, bit): |
| try: |
| bit = int(bit, 0) |
| if bit < 0 or bit >= 64: |
| raise ValueError |
| except: |
| print "'%s' is not a valid bit number in range [0, 64)" % bit |
| sys.exit(1) |
| |
| h = QcowHeader(fd) |
| if group == 'incompatible': |
| h.incompatible_features |= 1 << bit |
| elif group == 'compatible': |
| h.compatible_features |= 1 << bit |
| elif group == 'autoclear': |
| h.autoclear_features |= 1 << bit |
| else: |
| print "'%s' is not a valid group, try 'incompatible', 'compatible', or 'autoclear'" % group |
| sys.exit(1) |
| |
| h.update(fd) |
| |
| cmds = [ |
| [ 'dump-header', cmd_dump_header, 0, 'Dump image header and header extensions' ], |
| [ 'set-header', cmd_set_header, 2, 'Set a field in the header'], |
| [ 'add-header-ext', cmd_add_header_ext, 2, 'Add a header extension' ], |
| [ 'add-header-ext-stdio', cmd_add_header_ext_stdio, 1, 'Add a header extension, data from stdin' ], |
| [ 'del-header-ext', cmd_del_header_ext, 1, 'Delete a header extension' ], |
| [ 'set-feature-bit', cmd_set_feature_bit, 2, 'Set a feature bit'], |
| ] |
| |
| def main(filename, cmd, args): |
| fd = open(filename, "r+b") |
| try: |
| for name, handler, num_args, desc in cmds: |
| if name != cmd: |
| continue |
| elif len(args) != num_args: |
| usage() |
| return |
| else: |
| handler(fd, *args) |
| return |
| print "Unknown command '%s'" % cmd |
| finally: |
| fd.close() |
| |
| def usage(): |
| print "Usage: %s <file> <cmd> [<arg>, ...]" % sys.argv[0] |
| print "" |
| print "Supported commands:" |
| for name, handler, num_args, desc in cmds: |
| print " %-20s - %s" % (name, desc) |
| |
| if __name__ == '__main__': |
| if len(sys.argv) < 3: |
| usage() |
| sys.exit(1) |
| |
| main(sys.argv[1], sys.argv[2], sys.argv[3:]) |