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