master
py 441 lines 16.1 KB
Raw
1 #!/usr/bin/env python3
2 #
3 # Compares vmstate information stored in JSON format, obtained from
4 # the -dump-vmstate QEMU command.
5 #
6 # Copyright 2014 Amit Shah <amit.shah@redhat.com>
7 # Copyright 2014 Red Hat, Inc.
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License along
20 # with this program; if not, see <http://www.gnu.org/licenses/>.
21
22 import argparse
23 import json
24 import pathlib
25 import sys
26
27 # Count the number of errors found
28 taint = 0
29
30 def bump_taint():
31 global taint
32
33 # Ensure we don't wrap around or reset to 0 -- the shell only has
34 # an 8-bit return value.
35 if taint < 255:
36 taint = taint + 1
37
38
39 def check_fields_match(name, s_field, d_field):
40 if s_field == d_field:
41 return True
42
43 # Some fields changed names between qemu versions. This list
44 # is used to allow such changes in each section / description.
45 changed_names = {
46 'acpi-ghes': ['ghes_addr_le', 'hw_error_le'],
47 'apic': ['timer', 'timer_expiry'],
48 'e1000': ['dev', 'parent_obj'],
49 'ehci': ['dev', 'pcidev'],
50 'I440FX': ['dev', 'parent_obj'],
51 'ich9_ahci': ['card', 'parent_obj'],
52 'ich9-ahci': ['ahci', 'ich9_ahci'],
53 'ioh3420': ['PCIDevice', 'PCIEDevice'],
54 'ioh-3240-express-root-port': ['port.br.dev',
55 'parent_obj.parent_obj.parent_obj',
56 'port.br.dev.exp.aer_log',
57 'parent_obj.parent_obj.parent_obj.exp.aer_log'],
58 'cirrus_vga': ['hw_cursor_x', 'vga.hw_cursor_x',
59 'hw_cursor_y', 'vga.hw_cursor_y'],
60 'lsiscsi': ['dev', 'parent_obj'],
61 'mch': ['d', 'parent_obj'],
62 'pci_bridge': ['bridge.dev', 'parent_obj', 'bridge.dev.shpc', 'shpc'],
63 'pcnet': ['pci_dev', 'parent_obj'],
64 'PIIX3': ['pci_irq_levels', 'pci_irq_levels_vmstate'],
65 'piix4_pm': ['dev', 'parent_obj', 'pci0_status',
66 'acpi_pci_hotplug.acpi_pcihp_pci_status[0x0]',
67 'pm1a.sts', 'ar.pm1.evt.sts', 'pm1a.en', 'ar.pm1.evt.en',
68 'pm1_cnt.cnt', 'ar.pm1.cnt.cnt',
69 'tmr.timer', 'ar.tmr.timer',
70 'tmr.overflow_time', 'ar.tmr.overflow_time',
71 'gpe', 'ar.gpe'],
72 'rtl8139': ['dev', 'parent_obj'],
73 'qxl': ['num_surfaces', 'ssd.num_surfaces'],
74 'usb-ccid': ['abProtocolDataStructure', 'abProtocolDataStructure.data'],
75 'usb-host': ['dev', 'parent_obj'],
76 'usb-mouse': ['usb-ptr-queue', 'HIDPointerEventQueue'],
77 'usb-tablet': ['usb-ptr-queue', 'HIDPointerEventQueue'],
78 'vmware_vga': ['card', 'parent_obj'],
79 'vmware_vga_internal': ['depth', 'new_depth'],
80 'xhci': ['pci_dev', 'parent_obj'],
81 'x3130-upstream': ['PCIDevice', 'PCIEDevice'],
82 'xio3130-express-downstream-port': ['port.br.dev',
83 'parent_obj.parent_obj.parent_obj',
84 'port.br.dev.exp.aer_log',
85 'parent_obj.parent_obj.parent_obj.exp.aer_log'],
86 'xio3130-downstream': ['PCIDevice', 'PCIEDevice'],
87 'xio3130-express-upstream-port': ['br.dev', 'parent_obj.parent_obj',
88 'br.dev.exp.aer_log',
89 'parent_obj.parent_obj.exp.aer_log'],
90 'spapr_pci': ['dma_liobn[0]', 'mig_liobn',
91 'mem_win_addr', 'mig_mem_win_addr',
92 'mem_win_size', 'mig_mem_win_size',
93 'io_win_addr', 'mig_io_win_addr',
94 'io_win_size', 'mig_io_win_size'],
95 'hpet': ['num_timers', 'num_timers_save'],
96 }
97
98 if not name in changed_names:
99 return False
100
101 if s_field in changed_names[name] and d_field in changed_names[name]:
102 return True
103
104 return False
105
106 def get_changed_sec_name(sec):
107 # Section names can change -- see commit 292b1634 for an example.
108 changes = {
109 "ICH9 LPC": "ICH9-LPC",
110 "e1000-82540em": "e1000",
111 }
112
113 for item in changes:
114 if item == sec:
115 return changes[item]
116 if changes[item] == sec:
117 return item
118 return ""
119
120 def exists_in_substruct(fields, item):
121 # Some QEMU versions moved a few fields inside a substruct. This
122 # kept the on-wire format the same. This function checks if
123 # something got shifted inside a substruct. For example, the
124 # change in commit 1f42d22233b4f3d1a2933ff30e8d6a6d9ee2d08f
125
126 if not "Description" in fields:
127 return False
128
129 if not "Fields" in fields["Description"]:
130 return False
131
132 substruct_fields = fields["Description"]["Fields"]
133
134 if substruct_fields == []:
135 return False
136
137 return check_fields_match(fields["Description"]["name"],
138 substruct_fields[0]["field"], item)
139
140 def size_total(entry):
141 size = entry["size"]
142 if "num" not in entry:
143 return size
144 return size * entry["num"]
145
146 def check_fields(src_fields, dest_fields, desc, sec):
147 # This function checks for all the fields in a section. If some
148 # fields got embedded into a substruct, this function will also
149 # attempt to check inside the substruct.
150
151 d_iter = iter(dest_fields)
152 s_iter = iter(src_fields)
153
154 # Using these lists as stacks to store previous value of s_iter
155 # and d_iter, so that when time comes to exit out of a substruct,
156 # we can go back one level up and continue from where we left off.
157
158 s_iter_list = []
159 d_iter_list = []
160
161 advance_src = True
162 advance_dest = True
163 unused_count = 0
164
165 while True:
166 if advance_src:
167 try:
168 s_item = next(s_iter)
169 except StopIteration:
170 if s_iter_list == []:
171 break
172
173 s_iter = s_iter_list.pop()
174 continue
175 else:
176 if unused_count == 0:
177 # We want to avoid advancing just once -- when entering a
178 # dest substruct, or when exiting one.
179 advance_src = True
180
181 if advance_dest:
182 try:
183 d_item = next(d_iter)
184 except StopIteration:
185 if d_iter_list == []:
186 # We were not in a substruct
187 print("Section \"" + sec + "\",", end=' ')
188 print("Description " + "\"" + desc + "\":", end=' ')
189 print("expected field \"" + s_item["field"] + "\",", end=' ')
190 print("while dest has no further fields")
191 bump_taint()
192 break
193
194 d_iter = d_iter_list.pop()
195 advance_src = False
196 continue
197 else:
198 if unused_count == 0:
199 advance_dest = True
200
201 if unused_count != 0:
202 if advance_dest == False:
203 unused_count = unused_count - s_item["size"]
204 if unused_count == 0:
205 advance_dest = True
206 continue
207 if unused_count < 0:
208 print("Section \"" + sec + "\",", end=' ')
209 print("Description \"" + desc + "\":", end=' ')
210 print("unused size mismatch near \"", end=' ')
211 print(s_item["field"] + "\"")
212 bump_taint()
213 break
214 continue
215
216 if advance_src == False:
217 unused_count = unused_count - d_item["size"]
218 if unused_count == 0:
219 advance_src = True
220 continue
221 if unused_count < 0:
222 print("Section \"" + sec + "\",", end=' ')
223 print("Description \"" + desc + "\":", end=' ')
224 print("unused size mismatch near \"", end=' ')
225 print(d_item["field"] + "\"")
226 bump_taint()
227 break
228 continue
229
230 if not check_fields_match(desc, s_item["field"], d_item["field"]):
231 # Some fields were put in substructs, keeping the
232 # on-wire format the same, but breaking static tools
233 # like this one.
234
235 # First, check if dest has a new substruct.
236 if exists_in_substruct(d_item, s_item["field"]):
237 # listiterators don't have a prev() function, so we
238 # have to store our current location, descend into the
239 # substruct, and ensure we come out as if nothing
240 # happened when the substruct is over.
241 #
242 # Essentially we're opening the substructs that got
243 # added which didn't change the wire format.
244 d_iter_list.append(d_iter)
245 substruct_fields = d_item["Description"]["Fields"]
246 d_iter = iter(substruct_fields)
247 advance_src = False
248 continue
249
250 # Next, check if src has substruct that dest removed
251 # (can happen in backward migration: 2.0 -> 1.5)
252 if exists_in_substruct(s_item, d_item["field"]):
253 s_iter_list.append(s_iter)
254 substruct_fields = s_item["Description"]["Fields"]
255 s_iter = iter(substruct_fields)
256 advance_dest = False
257 continue
258
259 if s_item["field"] == "unused" or d_item["field"] == "unused":
260 s_size = size_total(s_item)
261 d_size = size_total(d_item)
262 if s_size == d_size:
263 continue
264
265 if d_item["field"] == "unused":
266 advance_dest = False
267 unused_count = d_size - s_size;
268 continue
269
270 if s_item["field"] == "unused":
271 advance_src = False
272 unused_count = s_size - d_size
273 continue
274
275 print("Section \"" + sec + "\",", end=' ')
276 print("Description \"" + desc + "\":", end=' ')
277 print("expected field \"" + s_item["field"] + "\",", end=' ')
278 print("got \"" + d_item["field"] + "\"; skipping rest")
279 bump_taint()
280 break
281
282 check_version(s_item, d_item, sec, desc)
283
284 if not "Description" in s_item:
285 # Check size of this field only if it's not a VMSTRUCT entry
286 check_size(s_item, d_item, sec, desc, s_item["field"])
287
288 check_description_in_list(s_item, d_item, sec, desc)
289
290
291 def check_subsections(src_sub, dest_sub, desc, sec):
292 for s_item in src_sub:
293 found = False
294 for d_item in dest_sub:
295 if s_item["name"] != d_item["name"]:
296 continue
297
298 found = True
299 check_descriptions(s_item, d_item, sec)
300
301 if not found:
302 print("Section \"" + sec + "\", Description \"" + desc + "\":", end=' ')
303 print("Subsection \"" + s_item["name"] + "\" not found")
304 bump_taint()
305
306
307 def check_description_in_list(s_item, d_item, sec, desc):
308 if not "Description" in s_item:
309 return
310
311 if not "Description" in d_item:
312 print("Section \"" + sec + "\", Description \"" + desc + "\",", end=' ')
313 print("Field \"" + s_item["field"] + "\": missing description")
314 bump_taint()
315 return
316
317 check_descriptions(s_item["Description"], d_item["Description"], sec)
318
319
320 def check_descriptions(src_desc, dest_desc, sec):
321 check_version(src_desc, dest_desc, sec, src_desc["name"])
322
323 if not check_fields_match(sec, src_desc["name"], dest_desc["name"]):
324 print("Section \"" + sec + "\":", end=' ')
325 print("Description \"" + src_desc["name"] + "\"", end=' ')
326 print("missing, got \"" + dest_desc["name"] + "\" instead; skipping")
327 bump_taint()
328 return
329
330 for f in src_desc:
331 if not f in dest_desc:
332 print("Section \"" + sec + "\"", end=' ')
333 print("Description \"" + src_desc["name"] + "\":", end=' ')
334 print("Entry \"" + f + "\" missing")
335 bump_taint()
336 continue
337
338 if f == 'Fields':
339 check_fields(src_desc[f], dest_desc[f], src_desc["name"], sec)
340
341 if f == 'Subsections':
342 check_subsections(src_desc[f], dest_desc[f], src_desc["name"], sec)
343
344
345 def check_version(s, d, sec, desc=None):
346 if s["version_id"] > d["version_id"]:
347 print("Section \"" + sec + "\"", end=' ')
348 if desc:
349 print("Description \"" + desc + "\":", end=' ')
350 print("version error:", s["version_id"], ">", d["version_id"])
351 bump_taint()
352
353 if not "minimum_version_id" in d:
354 return
355
356 if s["version_id"] < d["minimum_version_id"]:
357 print("Section \"" + sec + "\"", end=' ')
358 if desc:
359 print("Description \"" + desc + "\":", end=' ')
360 print("minimum version error:", s["version_id"], "<", end=' ')
361 print(d["minimum_version_id"])
362 bump_taint()
363
364
365 def check_size(s, d, sec, desc=None, field=None):
366 if s["size"] != d["size"]:
367 print("Section \"" + sec + "\"", end=' ')
368 if desc:
369 print("Description \"" + desc + "\"", end=' ')
370 if field:
371 print("Field \"" + field + "\"", end=' ')
372 print("size mismatch:", s["size"], ",", d["size"])
373 bump_taint()
374
375
376 def check_machine_type(s, d):
377 if s["Name"] != d["Name"]:
378 print("Warning: checking incompatible machine types:", end=' ')
379 print("\"" + s["Name"] + "\", \"" + d["Name"] + "\"")
380
381
382 def main():
383 help_text = "Parse JSON-formatted vmstate dumps from QEMU in files SRC and DEST. Checks whether migration from SRC to DEST QEMU versions would break based on the VMSTATE information contained within the JSON outputs. The JSON output is created from a QEMU invocation with the -dump-vmstate parameter and a filename argument to it. Other parameters to QEMU do not matter, except the -M (machine type) parameter."
384
385 parser = argparse.ArgumentParser(description=help_text)
386 parser.add_argument('-s', '--src', type=pathlib.Path,
387 required=True,
388 help='json dump from src qemu')
389 parser.add_argument('-d', '--dest', type=pathlib.Path,
390 required=True,
391 help='json dump from dest qemu')
392 parser.add_argument('--reverse', required=False, default=False,
393 action='store_true',
394 help='reverse the direction')
395 args = parser.parse_args()
396
397 with open(args.src, 'r', encoding='utf-8') as src_fh:
398 src_data = json.load(src_fh)
399 with open(args.dest, 'r', encoding='utf-8') as dst_fh:
400 dest_data = json.load(dst_fh)
401
402 if args.reverse:
403 temp = src_data
404 src_data = dest_data
405 dest_data = temp
406
407 for sec in src_data:
408 dest_sec = sec
409 if not dest_sec in dest_data:
410 # Either the section name got changed, or the section
411 # doesn't exist in dest.
412 dest_sec = get_changed_sec_name(sec)
413 if not dest_sec in dest_data:
414 print("Section \"" + sec + "\" does not exist in dest")
415 bump_taint()
416 continue
417
418 s = src_data[sec]
419 d = dest_data[dest_sec]
420
421 if sec == "vmschkmachine":
422 check_machine_type(s, d)
423 continue
424
425 check_version(s, d, sec)
426
427 for entry in s:
428 if not entry in d:
429 print("Section \"" + sec + "\": Entry \"" + entry + "\"", end=' ')
430 print("missing")
431 bump_taint()
432 continue
433
434 if entry == "Description":
435 check_descriptions(s[entry], d[entry], sec)
436
437 return taint
438
439
440 if __name__ == '__main__':
441 sys.exit(main())