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