bitmaps patches for 2020-06-09

- documenation fix
 - various improvements to qcow2.py program used in iotests
 -----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEEccLMIrHEYCkn0vOqp6FrSiUnQ2oFAl7f9hsACgkQp6FrSiUn
 Q2pLhAf+OrRJZFO5yY/r3UmNlWs9r3ZkE7nAOlRdDWmqeO0dwq+IOVa/TXoGf4vT
 dqqEqnFZ6adwQD9HFHKtL50qEwcBiHlNp65WnqfQs8wlFoDo2pckd0UTAR04yM8R
 d62Ip7l5Rv3cIT8R7/plwitypNvq8oUFKSwfnvlcHzP7/0Jspq784Nam7bNvj4L1
 lurdsYUrbPc1DKlDL1UNaLA0v9+9QoSICBzKV6qgFtpPyRAT1Y83f3QmpiMGE4rQ
 Djr3Hs8uGUskpv9yGFbakLscQO/Zws1VUnCsH97X1P1S5lXd2x/I//ydxGL+DkPY
 cdgu7PmziBLuTdnaOyJ/xd3CTUpddQ==
 =nhZT
 -----END PGP SIGNATURE-----

Merge remote-tracking branch 'remotes/ericb/tags/pull-bitmaps-2020-06-09' into staging

bitmaps patches for 2020-06-09

- documenation fix
- various improvements to qcow2.py program used in iotests

# gpg: Signature made Tue 09 Jun 2020 21:50:35 BST
# gpg:                using RSA key 71C2CC22B1C4602927D2F3AAA7A16B4A2527436A
# gpg: Good signature from "Eric Blake <eblake@redhat.com>" [full]
# gpg:                 aka "Eric Blake (Free Software Programmer) <ebb9@byu.net>" [full]
# gpg:                 aka "[jpeg image of size 6874]" [full]
# Primary key fingerprint: 71C2 CC22 B1C4 6029 27D2  F3AA A7A1 6B4A 2527 436A

* remotes/ericb/tags/pull-bitmaps-2020-06-09:
  iotests: Fix 291 across more file systems
  qcow2_format.py: dump bitmaps header extension
  qcow2: QcowHeaderExtension print names for extension magics
  qcow2_format: refactor QcowHeaderExtension as a subclass of Qcow2Struct
  qcow2_format.py: QcowHeaderExtension: add dump method
  qcow2_format.py: add field-formatting class
  qcow2_format.py: separate generic functionality of structure classes
  qcow2_format.py: use strings to specify c-type of struct fields
  qcow2_format.py: use modern string formatting
  qcow2_format.py: use tuples instead of lists for fields
  qcow2_format.py: drop new line printing at end of dump()
  qcow2.py: move qcow2 format classes to separate module
  qcow2.py: add licensing blurb
  qcow2.py: python style fixes
  qemu-img: Fix doc typo for 'bitmap' subcommand

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2020-06-11 15:35:44 +01:00
commit 3666f68476
8 changed files with 398 additions and 193 deletions

View File

@ -300,7 +300,7 @@ Command description:
``--disable`` to change *BITMAP* to stop recording future edits.
``--merge`` to merge the contents of *SOURCE_BITMAP* into *BITMAP*.
``--merge`` to merge the contents of the *SOURCE* bitmap into *BITMAP*.
Additional options include ``-g`` which sets a non-default
*GRANULARITY* for ``--add``, and ``-b`` and ``-F`` which select an

View File

@ -25,7 +25,7 @@ refcount_order 4
header_length 72
Header extension:
magic 0x12345678
magic 0x12345678 (<unknown>)
length 31
data 'This is a test header extension'
@ -53,7 +53,7 @@ refcount_order 4
header_length 72
Header extension:
magic 0x12345678
magic 0x12345678 (<unknown>)
length 31
data 'This is a test header extension'
@ -81,12 +81,12 @@ refcount_order 4
header_length 72
Header extension:
magic 0xe2792aca
magic 0xe2792aca (Backing format)
length 11
data 'host_device'
Header extension:
magic 0x12345678
magic 0x12345678 (<unknown>)
length 31
data 'This is a test header extension'
@ -116,12 +116,12 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
Header extension:
magic 0x12345678
magic 0x12345678 (<unknown>)
length 31
data 'This is a test header extension'
@ -149,12 +149,12 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
Header extension:
magic 0x12345678
magic 0x12345678 (<unknown>)
length 31
data 'This is a test header extension'
@ -182,17 +182,17 @@ refcount_order 4
header_length 112
Header extension:
magic 0xe2792aca
magic 0xe2792aca (Backing format)
length 11
data 'host_device'
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
Header extension:
magic 0x12345678
magic 0x12345678 (<unknown>)
length 31
data 'This is a test header extension'

View File

@ -25,7 +25,7 @@ incompatible_features []
compatible_features []
autoclear_features [63]
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -37,7 +37,7 @@ incompatible_features []
compatible_features []
autoclear_features []
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>

View File

@ -25,7 +25,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -83,7 +83,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -139,7 +139,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -194,7 +194,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -263,7 +263,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -325,7 +325,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>
@ -354,7 +354,7 @@ refcount_order 4
header_length 112
Header extension:
magic 0x6803f857
magic 0x6803f857 (Feature table)
length 336
data <binary>

View File

@ -62,6 +62,8 @@ $QEMU_IO -c 'w 1M 1M' -f $IMGFMT "$TEST_IMG" | _filter_qemu_io
$QEMU_IMG bitmap --disable -f $IMGFMT "$TEST_IMG" b1
$QEMU_IMG bitmap --enable -f $IMGFMT "$TEST_IMG" b2
$QEMU_IO -c 'w 2M 1M' -f $IMGFMT "$TEST_IMG" | _filter_qemu_io
echo "Check resulting qcow2 header extensions:"
$PYTHON qcow2.py "$TEST_IMG" dump-header-exts
echo
echo "=== Bitmap preservation not possible to non-qcow2 ==="
@ -77,7 +79,7 @@ echo
# Only bitmaps from the active layer are copied
$QEMU_IMG convert --bitmaps -O qcow2 "$TEST_IMG.orig" "$TEST_IMG"
$QEMU_IMG info "$TEST_IMG" | _filter_img_info --format-specific
_img_info --format-specific
# But we can also merge in bitmaps from other layers. This test is a bit
# contrived to cover more code paths, in reality, you could merge directly
# into b0 without going through tmp
@ -87,7 +89,9 @@ $QEMU_IMG bitmap --add --merge b0 -b "$TEST_IMG.base" -F $IMGFMT \
$QEMU_IMG bitmap --merge tmp -f $IMGFMT "$TEST_IMG" b0
$QEMU_IMG bitmap --remove --image-opts \
driver=$IMGFMT,file.driver=file,file.filename="$TEST_IMG" tmp
$QEMU_IMG info "$TEST_IMG" | _filter_img_info --format-specific
_img_info --format-specific
echo "Check resulting qcow2 header extensions:"
$PYTHON qcow2.py "$TEST_IMG" dump-header-exts
echo
echo "=== Check bitmap contents ==="

View File

@ -14,6 +14,25 @@ wrote 1048576/1048576 bytes at offset 1048576
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 2097152
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
Check resulting qcow2 header extensions:
Header extension:
magic 0xe2792aca (Backing format)
length 5
data 'qcow2'
Header extension:
magic 0x6803f857 (Feature table)
length 336
data <binary>
Header extension:
magic 0x23852875 (Bitmaps)
length 24
nb_bitmaps 2
reserved32 0
bitmap_directory_size 0x40
bitmap_directory_offset 0x510000
=== Bitmap preservation not possible to non-qcow2 ===
@ -24,7 +43,7 @@ qemu-img: Format driver 'raw' does not support bitmaps
image: TEST_DIR/t.IMGFMT
file format: IMGFMT
virtual size: 10 MiB (10485760 bytes)
disk size: 4.39 MiB
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
@ -44,7 +63,7 @@ Format specific information:
image: TEST_DIR/t.IMGFMT
file format: IMGFMT
virtual size: 10 MiB (10485760 bytes)
disk size: 4.48 MiB
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
@ -65,6 +84,20 @@ Format specific information:
granularity: 65536
refcount bits: 16
corrupt: false
Check resulting qcow2 header extensions:
Header extension:
magic 0x6803f857 (Feature table)
length 336
data <binary>
Header extension:
magic 0x23852875 (Bitmaps)
length 24
nb_bitmaps 3
reserved32 0
bitmap_directory_size 0x60
bitmap_directory_offset 0x520000
=== Check bitmap contents ===

View File

@ -1,181 +1,50 @@
#!/usr/bin/env python3
#
# Manipulations with qcow2 image
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import sys
import struct
import string
class QcowHeaderExtension:
def __init__(self, magic, length, data):
if length % 8 != 0:
padding = 8 - (length % 8)
data += b"\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, 'mask', 'incompatible_features' ],
[ uint64_t, 'mask', 'compatible_features' ],
[ uint64_t, 'mask', '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, b""))
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:
value = self.__dict__[f[2]]
if f[1] == 'mask':
bits = []
for bit in range(64):
if value & (1 << bit):
bits.append(bit)
value_str = str(bits)
else:
value_str = f[1] % value
print("%-25s" % f[2], value_str)
print("")
def dump_extensions(self):
for ex in self.extensions:
data = ex.data[:ex.length]
if all(c in string.printable.encode('ascii') for c in data):
data = "'%s'" % data.decode('ascii')
else:
data = "<binary>"
print("Header extension:")
print("%-25s %#x" % ("magic", ex.magic))
print("%-25s %d" % ("length", ex.length))
print("%-25s %s" % ("data", data))
print("")
from qcow2_format import (
QcowHeader,
QcowHeaderExtension
)
def cmd_dump_header(fd):
h = QcowHeader(fd)
h.dump()
print()
h.dump_extensions()
def cmd_dump_header_exts(fd):
h = QcowHeader(fd)
h.dump_extensions()
def cmd_set_header(fd, name, value):
try:
value = int(value, 0)
except:
except ValueError:
print("'%s' is not a valid number" % value)
sys.exit(1)
fields = (field[2] for field in QcowHeader.fields)
if not name in fields:
if name not in fields:
print("'%s' is not a known header field" % name)
sys.exit(1)
@ -183,25 +52,29 @@ def cmd_set_header(fd, name, value):
h.__dict__[name] = value
h.update(fd)
def cmd_add_header_ext(fd, magic, data):
try:
magic = int(magic, 0)
except:
except ValueError:
print("'%s' is not a valid magic number" % magic)
sys.exit(1)
h = QcowHeader(fd)
h.extensions.append(QcowHeaderExtension.create(magic, data.encode('ascii')))
h.extensions.append(QcowHeaderExtension.create(magic,
data.encode('ascii')))
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:
except ValueError:
print("'%s' is not a valid magic number" % magic)
sys.exit(1)
@ -219,12 +92,13 @@ def cmd_del_header_ext(fd, magic):
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:
except ValueError:
print("'%s' is not a valid bit number in range [0, 64)" % bit)
sys.exit(1)
@ -236,21 +110,27 @@ def cmd_set_feature_bit(fd, group, bit):
elif group == 'autoclear':
h.autoclear_features |= 1 << bit
else:
print("'%s' is not a valid group, try 'incompatible', 'compatible', or 'autoclear'" % group)
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' ],
[ 'dump-header-exts', cmd_dump_header_exts, 0, 'Dump image 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'],
['dump-header', cmd_dump_header, 0,
'Dump image header and header extensions'],
['dump-header-exts', cmd_dump_header_exts, 0,
'Dump image 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:
@ -267,6 +147,7 @@ def main(filename, cmd, args):
finally:
fd.close()
def usage():
print("Usage: %s <file> <cmd> [<arg>, ...]" % sys.argv[0])
print("")
@ -274,6 +155,7 @@ def usage():
for name, handler, num_args, desc in cmds:
print(" %-20s - %s" % (name, desc))
if __name__ == '__main__':
if len(sys.argv) < 3:
usage()

View File

@ -0,0 +1,286 @@
# Library for manipulations with qcow2 image
#
# Copyright (c) 2020 Virtuozzo International GmbH.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import struct
import string
class Qcow2Field:
def __init__(self, value):
self.value = value
def __str__(self):
return str(self.value)
class Flags64(Qcow2Field):
def __str__(self):
bits = []
for bit in range(64):
if self.value & (1 << bit):
bits.append(bit)
return str(bits)
class Enum(Qcow2Field):
def __str__(self):
return f'{self.value:#x} ({self.mapping.get(self.value, "<unknown>")})'
class Qcow2StructMeta(type):
# Mapping from c types to python struct format
ctypes = {
'u8': 'B',
'u16': 'H',
'u32': 'I',
'u64': 'Q'
}
def __init__(self, name, bases, attrs):
if 'fields' in attrs:
self.fmt = '>' + ''.join(self.ctypes[f[0]] for f in self.fields)
class Qcow2Struct(metaclass=Qcow2StructMeta):
"""Qcow2Struct: base class for qcow2 data structures
Successors should define fields class variable, which is: list of tuples,
each of three elements:
- c-type (one of 'u8', 'u16', 'u32', 'u64')
- format (format_spec to use with .format() when dump or 'mask' to dump
bitmasks)
- field name
"""
def __init__(self, fd=None, offset=None, data=None):
"""
Two variants:
1. Specify data. fd and offset must be None.
2. Specify fd and offset, data must be None. offset may be omitted
in this case, than current position of fd is used.
"""
if data is None:
assert fd is not None
buf_size = struct.calcsize(self.fmt)
if offset is not None:
fd.seek(offset)
data = fd.read(buf_size)
else:
assert fd is None and offset is None
values = struct.unpack(self.fmt, data)
self.__dict__ = dict((field[2], values[i])
for i, field in enumerate(self.fields))
def dump(self):
for f in self.fields:
value = self.__dict__[f[2]]
if isinstance(f[1], str):
value_str = f[1].format(value)
else:
value_str = str(f[1](value))
print('{:<25} {}'.format(f[2], value_str))
class Qcow2BitmapExt(Qcow2Struct):
fields = (
('u32', '{}', 'nb_bitmaps'),
('u32', '{}', 'reserved32'),
('u64', '{:#x}', 'bitmap_directory_size'),
('u64', '{:#x}', 'bitmap_directory_offset')
)
QCOW2_EXT_MAGIC_BITMAPS = 0x23852875
class QcowHeaderExtension(Qcow2Struct):
class Magic(Enum):
mapping = {
0xe2792aca: 'Backing format',
0x6803f857: 'Feature table',
0x0537be77: 'Crypto header',
QCOW2_EXT_MAGIC_BITMAPS: 'Bitmaps',
0x44415441: 'Data file'
}
fields = (
('u32', Magic, 'magic'),
('u32', '{}', 'length')
# length bytes of data follows
# then padding to next multiply of 8
)
def __init__(self, magic=None, length=None, data=None, fd=None):
"""
Support both loading from fd and creation from user data.
For fd-based creation current position in a file will be used to read
the data.
This should be somehow refactored and functionality should be moved to
superclass (to allow creation of any qcow2 struct), but then, fields
of variable length (data here) should be supported in base class
somehow. Note also, that we probably want to parse different
extensions. Should they be subclasses of this class, or how to do it
better? Should it be something like QAPI union with discriminator field
(magic here). So, it's a TODO. We'll see how to properly refactor this
when we have more qcow2 structures.
"""
if fd is None:
assert all(v is not None for v in (magic, length, data))
self.magic = magic
self.length = length
if length % 8 != 0:
padding = 8 - (length % 8)
data += b'\0' * padding
self.data = data
else:
assert all(v is None for v in (magic, length, data))
super().__init__(fd=fd)
padded = (self.length + 7) & ~7
self.data = fd.read(padded)
assert self.data is not None
if self.magic == QCOW2_EXT_MAGIC_BITMAPS:
self.obj = Qcow2BitmapExt(data=self.data)
else:
self.obj = None
def dump(self):
super().dump()
if self.obj is None:
data = self.data[:self.length]
if all(c in string.printable.encode('ascii') for c in data):
data = f"'{ data.decode('ascii') }'"
else:
data = '<binary>'
print(f'{"data":<25} {data}')
else:
self.obj.dump()
@classmethod
def create(cls, magic, data):
return QcowHeaderExtension(magic, len(data), data)
class QcowHeader(Qcow2Struct):
fields = (
# Version 2 header fields
('u32', '{:#x}', 'magic'),
('u32', '{}', 'version'),
('u64', '{:#x}', 'backing_file_offset'),
('u32', '{:#x}', 'backing_file_size'),
('u32', '{}', 'cluster_bits'),
('u64', '{}', 'size'),
('u32', '{}', 'crypt_method'),
('u32', '{}', 'l1_size'),
('u64', '{:#x}', 'l1_table_offset'),
('u64', '{:#x}', 'refcount_table_offset'),
('u32', '{}', 'refcount_table_clusters'),
('u32', '{}', 'nb_snapshots'),
('u64', '{:#x}', 'snapshot_offset'),
# Version 3 header fields
('u64', Flags64, 'incompatible_features'),
('u64', Flags64, 'compatible_features'),
('u64', Flags64, 'autoclear_features'),
('u32', '{}', 'refcount_order'),
('u32', '{}', 'header_length'),
)
def __init__(self, fd):
super().__init__(fd=fd, offset=0)
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:
ext = QcowHeaderExtension(fd=fd)
if ext.magic == 0:
break
else:
self.extensions.append(ext)
def update_extensions(self, fd):
fd.seek(self.header_length)
extensions = self.extensions
extensions.append(QcowHeaderExtension(0, 0, b''))
for ex in extensions:
buf = struct.pack('>II', ex.magic, ex.length)
fd.write(buf)
fd.write(ex.data)
if self.backing_file is not 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_extensions(self):
for ex in self.extensions:
print('Header extension:')
ex.dump()
print()