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