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