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