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 } 95 96 if not name in changed_names: 97 return False 98 99 if s_field in changed_names[name] and d_field in changed_names[name]: 100 return True 101 102 return False 103 104def get_changed_sec_name(sec): 105 # Section names can change -- see commit 292b1634 for an example. 106 changes = { 107 "ICH9 LPC": "ICH9-LPC", 108 "e1000-82540em": "e1000", 109 } 110 111 for item in changes: 112 if item == sec: 113 return changes[item] 114 if changes[item] == sec: 115 return item 116 return "" 117 118def exists_in_substruct(fields, item): 119 # Some QEMU versions moved a few fields inside a substruct. This 120 # kept the on-wire format the same. This function checks if 121 # something got shifted inside a substruct. For example, the 122 # change in commit 1f42d22233b4f3d1a2933ff30e8d6a6d9ee2d08f 123 124 if not "Description" in fields: 125 return False 126 127 if not "Fields" in fields["Description"]: 128 return False 129 130 substruct_fields = fields["Description"]["Fields"] 131 132 if substruct_fields == []: 133 return False 134 135 return check_fields_match(fields["Description"]["name"], 136 substruct_fields[0]["field"], item) 137 138def size_total(entry): 139 size = entry["size"] 140 if "num" not in entry: 141 return size 142 return size * entry["num"] 143 144def check_fields(src_fields, dest_fields, desc, sec): 145 # This function checks for all the fields in a section. If some 146 # fields got embedded into a substruct, this function will also 147 # attempt to check inside the substruct. 148 149 d_iter = iter(dest_fields) 150 s_iter = iter(src_fields) 151 152 # Using these lists as stacks to store previous value of s_iter 153 # and d_iter, so that when time comes to exit out of a substruct, 154 # we can go back one level up and continue from where we left off. 155 156 s_iter_list = [] 157 d_iter_list = [] 158 159 advance_src = True 160 advance_dest = True 161 unused_count = 0 162 163 while True: 164 if advance_src: 165 try: 166 s_item = next(s_iter) 167 except StopIteration: 168 if s_iter_list == []: 169 break 170 171 s_iter = s_iter_list.pop() 172 continue 173 else: 174 if unused_count == 0: 175 # We want to avoid advancing just once -- when entering a 176 # dest substruct, or when exiting one. 177 advance_src = True 178 179 if advance_dest: 180 try: 181 d_item = next(d_iter) 182 except StopIteration: 183 if d_iter_list == []: 184 # We were not in a substruct 185 print("Section \"" + sec + "\",", end=' ') 186 print("Description " + "\"" + desc + "\":", end=' ') 187 print("expected field \"" + s_item["field"] + "\",", end=' ') 188 print("while dest has no further fields") 189 bump_taint() 190 break 191 192 d_iter = d_iter_list.pop() 193 advance_src = False 194 continue 195 else: 196 if unused_count == 0: 197 advance_dest = True 198 199 if unused_count != 0: 200 if advance_dest == False: 201 unused_count = unused_count - s_item["size"] 202 if unused_count == 0: 203 advance_dest = True 204 continue 205 if unused_count < 0: 206 print("Section \"" + sec + "\",", end=' ') 207 print("Description \"" + desc + "\":", end=' ') 208 print("unused size mismatch near \"", end=' ') 209 print(s_item["field"] + "\"") 210 bump_taint() 211 break 212 continue 213 214 if advance_src == False: 215 unused_count = unused_count - d_item["size"] 216 if unused_count == 0: 217 advance_src = True 218 continue 219 if unused_count < 0: 220 print("Section \"" + sec + "\",", end=' ') 221 print("Description \"" + desc + "\":", end=' ') 222 print("unused size mismatch near \"", end=' ') 223 print(d_item["field"] + "\"") 224 bump_taint() 225 break 226 continue 227 228 if not check_fields_match(desc, s_item["field"], d_item["field"]): 229 # Some fields were put in substructs, keeping the 230 # on-wire format the same, but breaking static tools 231 # like this one. 232 233 # First, check if dest has a new substruct. 234 if exists_in_substruct(d_item, s_item["field"]): 235 # listiterators don't have a prev() function, so we 236 # have to store our current location, descend into the 237 # substruct, and ensure we come out as if nothing 238 # happened when the substruct is over. 239 # 240 # Essentially we're opening the substructs that got 241 # added which didn't change the wire format. 242 d_iter_list.append(d_iter) 243 substruct_fields = d_item["Description"]["Fields"] 244 d_iter = iter(substruct_fields) 245 advance_src = False 246 continue 247 248 # Next, check if src has substruct that dest removed 249 # (can happen in backward migration: 2.0 -> 1.5) 250 if exists_in_substruct(s_item, d_item["field"]): 251 s_iter_list.append(s_iter) 252 substruct_fields = s_item["Description"]["Fields"] 253 s_iter = iter(substruct_fields) 254 advance_dest = False 255 continue 256 257 if s_item["field"] == "unused" or d_item["field"] == "unused": 258 s_size = size_total(s_item) 259 d_size = size_total(d_item) 260 if s_size == d_size: 261 continue 262 263 if d_item["field"] == "unused": 264 advance_dest = False 265 unused_count = d_size - s_size; 266 continue 267 268 if s_item["field"] == "unused": 269 advance_src = False 270 unused_count = s_size - d_size 271 continue 272 273 print("Section \"" + sec + "\",", end=' ') 274 print("Description \"" + desc + "\":", end=' ') 275 print("expected field \"" + s_item["field"] + "\",", end=' ') 276 print("got \"" + d_item["field"] + "\"; skipping rest") 277 bump_taint() 278 break 279 280 check_version(s_item, d_item, sec, desc) 281 282 if not "Description" in s_item: 283 # Check size of this field only if it's not a VMSTRUCT entry 284 check_size(s_item, d_item, sec, desc, s_item["field"]) 285 286 check_description_in_list(s_item, d_item, sec, desc) 287 288 289def check_subsections(src_sub, dest_sub, desc, sec): 290 for s_item in src_sub: 291 found = False 292 for d_item in dest_sub: 293 if s_item["name"] != d_item["name"]: 294 continue 295 296 found = True 297 check_descriptions(s_item, d_item, sec) 298 299 if not found: 300 print("Section \"" + sec + "\", Description \"" + desc + "\":", end=' ') 301 print("Subsection \"" + s_item["name"] + "\" not found") 302 bump_taint() 303 304 305def check_description_in_list(s_item, d_item, sec, desc): 306 if not "Description" in s_item: 307 return 308 309 if not "Description" in d_item: 310 print("Section \"" + sec + "\", Description \"" + desc + "\",", end=' ') 311 print("Field \"" + s_item["field"] + "\": missing description") 312 bump_taint() 313 return 314 315 check_descriptions(s_item["Description"], d_item["Description"], sec) 316 317 318def check_descriptions(src_desc, dest_desc, sec): 319 check_version(src_desc, dest_desc, sec, src_desc["name"]) 320 321 if not check_fields_match(sec, src_desc["name"], dest_desc["name"]): 322 print("Section \"" + sec + "\":", end=' ') 323 print("Description \"" + src_desc["name"] + "\"", end=' ') 324 print("missing, got \"" + dest_desc["name"] + "\" instead; skipping") 325 bump_taint() 326 return 327 328 for f in src_desc: 329 if not f in dest_desc: 330 print("Section \"" + sec + "\"", end=' ') 331 print("Description \"" + src_desc["name"] + "\":", end=' ') 332 print("Entry \"" + f + "\" missing") 333 bump_taint() 334 continue 335 336 if f == 'Fields': 337 check_fields(src_desc[f], dest_desc[f], src_desc["name"], sec) 338 339 if f == 'Subsections': 340 check_subsections(src_desc[f], dest_desc[f], src_desc["name"], sec) 341 342 343def check_version(s, d, sec, desc=None): 344 if s["version_id"] > d["version_id"]: 345 print("Section \"" + sec + "\"", end=' ') 346 if desc: 347 print("Description \"" + desc + "\":", end=' ') 348 print("version error:", s["version_id"], ">", d["version_id"]) 349 bump_taint() 350 351 if not "minimum_version_id" in d: 352 return 353 354 if s["version_id"] < d["minimum_version_id"]: 355 print("Section \"" + sec + "\"", end=' ') 356 if desc: 357 print("Description \"" + desc + "\":", end=' ') 358 print("minimum version error:", s["version_id"], "<", end=' ') 359 print(d["minimum_version_id"]) 360 bump_taint() 361 362 363def check_size(s, d, sec, desc=None, field=None): 364 if s["size"] != d["size"]: 365 print("Section \"" + sec + "\"", end=' ') 366 if desc: 367 print("Description \"" + desc + "\"", end=' ') 368 if field: 369 print("Field \"" + field + "\"", end=' ') 370 print("size mismatch:", s["size"], ",", d["size"]) 371 bump_taint() 372 373 374def check_machine_type(s, d): 375 if s["Name"] != d["Name"]: 376 print("Warning: checking incompatible machine types:", end=' ') 377 print("\"" + s["Name"] + "\", \"" + d["Name"] + "\"") 378 379 380def main(): 381 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." 382 383 parser = argparse.ArgumentParser(description=help_text) 384 parser.add_argument('-s', '--src', type=argparse.FileType('r'), 385 required=True, 386 help='json dump from src qemu') 387 parser.add_argument('-d', '--dest', type=argparse.FileType('r'), 388 required=True, 389 help='json dump from dest qemu') 390 parser.add_argument('--reverse', required=False, default=False, 391 action='store_true', 392 help='reverse the direction') 393 args = parser.parse_args() 394 395 src_data = json.load(args.src) 396 dest_data = json.load(args.dest) 397 args.src.close() 398 args.dest.close() 399 400 if args.reverse: 401 temp = src_data 402 src_data = dest_data 403 dest_data = temp 404 405 for sec in src_data: 406 dest_sec = sec 407 if not dest_sec in dest_data: 408 # Either the section name got changed, or the section 409 # doesn't exist in dest. 410 dest_sec = get_changed_sec_name(sec) 411 if not dest_sec in dest_data: 412 print("Section \"" + sec + "\" does not exist in dest") 413 bump_taint() 414 continue 415 416 s = src_data[sec] 417 d = dest_data[dest_sec] 418 419 if sec == "vmschkmachine": 420 check_machine_type(s, d) 421 continue 422 423 check_version(s, d, sec) 424 425 for entry in s: 426 if not entry in d: 427 print("Section \"" + sec + "\": Entry \"" + entry + "\"", end=' ') 428 print("missing") 429 bump_taint() 430 continue 431 432 if entry == "Description": 433 check_descriptions(s[entry], d[entry], sec) 434 435 return taint 436 437 438if __name__ == '__main__': 439 sys.exit(main()) 440