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:
commit
3666f68476
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 ==="
|
||||
|
@ -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 ===
|
||||
|
||||
|
@ -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()
|
||||
|
286
tests/qemu-iotests/qcow2_format.py
Normal file
286
tests/qemu-iotests/qcow2_format.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user