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