]>
Commit | Line | Data |
---|---|---|
6e19b3c4 KW |
1 | #!/usr/bin/env python |
2 | ||
3 | import sys | |
4 | import struct | |
5 | import string | |
6 | ||
7 | class QcowHeaderExtension: | |
8 | ||
9 | def __init__(self, magic, length, data): | |
10 | self.magic = magic | |
11 | self.length = length | |
12 | self.data = data | |
13 | ||
14 | @classmethod | |
15 | def create(cls, magic, data): | |
16 | return QcowHeaderExtension(magic, len(data), data) | |
17 | ||
18 | class QcowHeader: | |
19 | ||
20 | uint32_t = 'I' | |
21 | uint64_t = 'Q' | |
22 | ||
23 | fields = [ | |
24 | # Version 2 header fields | |
25 | [ uint32_t, '%#x', 'magic' ], | |
26 | [ uint32_t, '%d', 'version' ], | |
27 | [ uint64_t, '%#x', 'backing_file_offset' ], | |
28 | [ uint32_t, '%#x', 'backing_file_size' ], | |
29 | [ uint32_t, '%d', 'cluster_bits' ], | |
30 | [ uint64_t, '%d', 'size' ], | |
31 | [ uint32_t, '%d', 'crypt_method' ], | |
32 | [ uint32_t, '%d', 'l1_size' ], | |
33 | [ uint64_t, '%#x', 'l1_table_offset' ], | |
34 | [ uint64_t, '%#x', 'refcount_table_offset' ], | |
35 | [ uint32_t, '%d', 'refcount_table_clusters' ], | |
36 | [ uint32_t, '%d', 'nb_snapshots' ], | |
37 | [ uint64_t, '%#x', 'snapshot_offset' ], | |
1042ec94 KW |
38 | |
39 | # Version 3 header fields | |
40 | [ uint64_t, '%#x', 'incompatible_features' ], | |
41 | [ uint64_t, '%#x', 'compatible_features' ], | |
42 | [ uint64_t, '%#x', 'autoclear_features' ], | |
43 | [ uint32_t, '%d', 'refcount_order' ], | |
44 | [ uint32_t, '%d', 'header_length' ], | |
6e19b3c4 KW |
45 | ]; |
46 | ||
47 | fmt = '>' + ''.join(field[0] for field in fields) | |
48 | ||
49 | def __init__(self, fd): | |
50 | ||
51 | buf_size = struct.calcsize(QcowHeader.fmt) | |
52 | ||
53 | fd.seek(0) | |
54 | buf = fd.read(buf_size) | |
55 | ||
56 | header = struct.unpack(QcowHeader.fmt, buf) | |
57 | self.__dict__ = dict((field[2], header[i]) | |
58 | for i, field in enumerate(QcowHeader.fields)) | |
59 | ||
1042ec94 | 60 | self.set_defaults() |
6e19b3c4 KW |
61 | self.cluster_size = 1 << self.cluster_bits |
62 | ||
1042ec94 | 63 | fd.seek(self.header_length) |
6e19b3c4 KW |
64 | self.load_extensions(fd) |
65 | ||
66 | if self.backing_file_offset: | |
67 | fd.seek(self.backing_file_offset) | |
68 | self.backing_file = fd.read(self.backing_file_size) | |
69 | else: | |
70 | self.backing_file = None | |
71 | ||
1042ec94 | 72 | def set_defaults(self): |
6e19b3c4 | 73 | if self.version == 2: |
1042ec94 KW |
74 | self.incompatible_features = 0 |
75 | self.compatible_features = 0 | |
76 | self.autoclear_features = 0 | |
77 | self.refcount_order = 4 | |
78 | self.header_length = 72 | |
6e19b3c4 KW |
79 | |
80 | def load_extensions(self, fd): | |
81 | self.extensions = [] | |
82 | ||
83 | if self.backing_file_offset != 0: | |
84 | end = min(self.cluster_size, self.backing_file_offset) | |
85 | else: | |
86 | end = self.cluster_size | |
87 | ||
88 | while fd.tell() < end: | |
89 | (magic, length) = struct.unpack('>II', fd.read(8)) | |
90 | if magic == 0: | |
91 | break | |
92 | else: | |
93 | padded = (length + 7) & ~7 | |
94 | data = fd.read(padded) | |
95 | self.extensions.append(QcowHeaderExtension(magic, length, data)) | |
96 | ||
97 | def update_extensions(self, fd): | |
98 | ||
1042ec94 | 99 | fd.seek(self.header_length) |
6e19b3c4 KW |
100 | extensions = self.extensions |
101 | extensions.append(QcowHeaderExtension(0, 0, "")) | |
102 | for ex in extensions: | |
103 | buf = struct.pack('>II', ex.magic, ex.length) | |
104 | fd.write(buf) | |
105 | fd.write(ex.data) | |
106 | ||
107 | if self.backing_file != None: | |
108 | self.backing_file_offset = fd.tell() | |
109 | fd.write(self.backing_file) | |
110 | ||
111 | if fd.tell() > self.cluster_size: | |
112 | raise Exception("I think I just broke the image...") | |
113 | ||
114 | ||
115 | def update(self, fd): | |
1042ec94 | 116 | header_bytes = self.header_length |
6e19b3c4 KW |
117 | |
118 | self.update_extensions(fd) | |
119 | ||
120 | fd.seek(0) | |
121 | header = tuple(self.__dict__[f] for t, p, f in QcowHeader.fields) | |
122 | buf = struct.pack(QcowHeader.fmt, *header) | |
123 | buf = buf[0:header_bytes-1] | |
124 | fd.write(buf) | |
125 | ||
126 | def dump(self): | |
127 | for f in QcowHeader.fields: | |
128 | print "%-25s" % f[2], f[1] % self.__dict__[f[2]] | |
129 | print "" | |
130 | ||
131 | def dump_extensions(self): | |
132 | for ex in self.extensions: | |
133 | ||
134 | data = ex.data[:ex.length] | |
135 | if all(c in string.printable for c in data): | |
136 | data = "'%s'" % data | |
137 | else: | |
138 | data = "<binary>" | |
139 | ||
140 | print "Header extension:" | |
141 | print "%-25s %#x" % ("magic", ex.magic) | |
142 | print "%-25s %d" % ("length", ex.length) | |
143 | print "%-25s %s" % ("data", data) | |
144 | print "" | |
145 | ||
146 | ||
147 | def cmd_dump_header(fd): | |
148 | h = QcowHeader(fd) | |
149 | h.dump() | |
150 | h.dump_extensions() | |
151 | ||
152 | def cmd_add_header_ext(fd, magic, data): | |
153 | try: | |
154 | magic = int(magic, 0) | |
155 | except: | |
156 | print "'%s' is not a valid magic number" % magic | |
157 | sys.exit(1) | |
158 | ||
159 | h = QcowHeader(fd) | |
160 | h.extensions.append(QcowHeaderExtension.create(magic, data)) | |
161 | h.update(fd) | |
162 | ||
163 | def cmd_del_header_ext(fd, magic): | |
164 | try: | |
165 | magic = int(magic, 0) | |
166 | except: | |
167 | print "'%s' is not a valid magic number" % magic | |
168 | sys.exit(1) | |
169 | ||
170 | h = QcowHeader(fd) | |
171 | found = False | |
172 | ||
173 | for ex in h.extensions: | |
174 | if ex.magic == magic: | |
175 | found = True | |
176 | h.extensions.remove(ex) | |
177 | ||
178 | if not found: | |
179 | print "No such header extension" | |
180 | return | |
181 | ||
182 | h.update(fd) | |
183 | ||
184 | cmds = [ | |
185 | [ 'dump-header', cmd_dump_header, 0, 'Dump image header and header extensions' ], | |
186 | [ 'add-header-ext', cmd_add_header_ext, 2, 'Add a header extension' ], | |
187 | [ 'del-header-ext', cmd_del_header_ext, 1, 'Delete a header extension' ], | |
188 | ] | |
189 | ||
190 | def main(filename, cmd, args): | |
191 | fd = open(filename, "r+b") | |
192 | try: | |
193 | for name, handler, num_args, desc in cmds: | |
194 | if name != cmd: | |
195 | continue | |
196 | elif len(args) != num_args: | |
197 | usage() | |
198 | return | |
199 | else: | |
200 | handler(fd, *args) | |
201 | return | |
202 | print "Unknown command '%s'" % cmd | |
203 | finally: | |
204 | fd.close() | |
205 | ||
206 | def usage(): | |
207 | print "Usage: %s <file> <cmd> [<arg>, ...]" % sys.argv[0] | |
208 | print "" | |
209 | print "Supported commands:" | |
210 | for name, handler, num_args, desc in cmds: | |
211 | print " %-20s - %s" % (name, desc) | |
212 | ||
213 | if len(sys.argv) < 3: | |
214 | usage() | |
215 | sys.exit(1) | |
216 | ||
217 | main(sys.argv[1], sys.argv[2], sys.argv[3:]) |