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