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