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