xref: /openbmc/qemu/scripts/analyze-migration.py (revision 0d66549c)
1#!/usr/bin/env python3
2#
3#  Migration Stream Analyzer
4#
5#  Copyright (c) 2015 Alexander Graf <agraf@suse.de>
6#
7# This library is free software; you can redistribute it and/or
8# modify it under the terms of the GNU Lesser General Public
9# License as published by the Free Software Foundation; either
10# version 2.1 of the License, or (at your option) any later version.
11#
12# This library is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15# Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, see <http://www.gnu.org/licenses/>.
19
20import json
21import os
22import argparse
23import collections
24import struct
25import sys
26
27
28def mkdir_p(path):
29    try:
30        os.makedirs(path)
31    except OSError:
32        pass
33
34
35class MigrationFile(object):
36    def __init__(self, filename):
37        self.filename = filename
38        self.file = open(self.filename, "rb")
39
40    def read64(self):
41        return int.from_bytes(self.file.read(8), byteorder='big', signed=False)
42
43    def read32(self):
44        return int.from_bytes(self.file.read(4), byteorder='big', signed=False)
45
46    def read16(self):
47        return int.from_bytes(self.file.read(2), byteorder='big', signed=False)
48
49    def read8(self):
50        return int.from_bytes(self.file.read(1), byteorder='big', signed=True)
51
52    def readstr(self, len = None):
53        return self.readvar(len).decode('utf-8')
54
55    def readvar(self, size = None):
56        if size is None:
57            size = self.read8()
58        if size == 0:
59            return ""
60        value = self.file.read(size)
61        if len(value) != size:
62            raise Exception("Unexpected end of %s at 0x%x" % (self.filename, self.file.tell()))
63        return value
64
65    def tell(self):
66        return self.file.tell()
67
68    # The VMSD description is at the end of the file, after EOF. Look for
69    # the last NULL byte, then for the beginning brace of JSON.
70    def read_migration_debug_json(self):
71        QEMU_VM_VMDESCRIPTION = 0x06
72
73        # Remember the offset in the file when we started
74        entrypos = self.file.tell()
75
76        # Read the last 10MB
77        self.file.seek(0, os.SEEK_END)
78        endpos = self.file.tell()
79        self.file.seek(max(-endpos, -10 * 1024 * 1024), os.SEEK_END)
80        datapos = self.file.tell()
81        data = self.file.read()
82        # The full file read closed the file as well, reopen it
83        self.file = open(self.filename, "rb")
84
85        # Find the last NULL byte, then the first brace after that. This should
86        # be the beginning of our JSON data.
87        nulpos = data.rfind(b'\0')
88        jsonpos = data.find(b'{', nulpos)
89
90        # Check backwards from there and see whether we guessed right
91        self.file.seek(datapos + jsonpos - 5, 0)
92        if self.read8() != QEMU_VM_VMDESCRIPTION:
93            raise Exception("No Debug Migration device found")
94
95        jsonlen = self.read32()
96
97        # Seek back to where we were at the beginning
98        self.file.seek(entrypos, 0)
99
100        # explicit decode() needed for Python 3.5 compatibility
101        return data[jsonpos:jsonpos + jsonlen].decode("utf-8")
102
103    def close(self):
104        self.file.close()
105
106class RamSection(object):
107    RAM_SAVE_FLAG_COMPRESS = 0x02
108    RAM_SAVE_FLAG_MEM_SIZE = 0x04
109    RAM_SAVE_FLAG_PAGE     = 0x08
110    RAM_SAVE_FLAG_EOS      = 0x10
111    RAM_SAVE_FLAG_CONTINUE = 0x20
112    RAM_SAVE_FLAG_XBZRLE   = 0x40
113    RAM_SAVE_FLAG_HOOK     = 0x80
114    RAM_SAVE_FLAG_COMPRESS_PAGE = 0x100
115    RAM_SAVE_FLAG_MULTIFD_FLUSH = 0x200
116
117    def __init__(self, file, version_id, ramargs, section_key):
118        if version_id != 4:
119            raise Exception("Unknown RAM version %d" % version_id)
120
121        self.file = file
122        self.section_key = section_key
123        self.TARGET_PAGE_SIZE = ramargs['page_size']
124        self.dump_memory = ramargs['dump_memory']
125        self.write_memory = ramargs['write_memory']
126        self.ignore_shared = ramargs['ignore_shared']
127        self.sizeinfo = collections.OrderedDict()
128        self.data = collections.OrderedDict()
129        self.data['section sizes'] = self.sizeinfo
130        self.name = ''
131        if self.write_memory:
132            self.files = { }
133        if self.dump_memory:
134            self.memory = collections.OrderedDict()
135            self.data['memory'] = self.memory
136
137    def __repr__(self):
138        return self.data.__repr__()
139
140    def __str__(self):
141        return self.data.__str__()
142
143    def getDict(self):
144        return self.data
145
146    def read(self):
147        # Read all RAM sections
148        while True:
149            addr = self.file.read64()
150            flags = addr & (self.TARGET_PAGE_SIZE - 1)
151            addr &= ~(self.TARGET_PAGE_SIZE - 1)
152
153            if flags & self.RAM_SAVE_FLAG_MEM_SIZE:
154                while True:
155                    namelen = self.file.read8()
156                    # We assume that no RAM chunk is big enough to ever
157                    # hit the first byte of the address, so when we see
158                    # a zero here we know it has to be an address, not the
159                    # length of the next block.
160                    if namelen == 0:
161                        self.file.file.seek(-1, 1)
162                        break
163                    self.name = self.file.readstr(len = namelen)
164                    len = self.file.read64()
165                    self.sizeinfo[self.name] = '0x%016x' % len
166                    if self.write_memory:
167                        print(self.name)
168                        mkdir_p('./' + os.path.dirname(self.name))
169                        f = open('./' + self.name, "wb")
170                        f.truncate(0)
171                        f.truncate(len)
172                        self.files[self.name] = f
173                    if self.ignore_shared:
174                        mr_addr = self.file.read64()
175                flags &= ~self.RAM_SAVE_FLAG_MEM_SIZE
176
177            if flags & self.RAM_SAVE_FLAG_COMPRESS:
178                if flags & self.RAM_SAVE_FLAG_CONTINUE:
179                    flags &= ~self.RAM_SAVE_FLAG_CONTINUE
180                else:
181                    self.name = self.file.readstr()
182                fill_char = self.file.read8()
183                # The page in question is filled with fill_char now
184                if self.write_memory and fill_char != 0:
185                    self.files[self.name].seek(addr, os.SEEK_SET)
186                    self.files[self.name].write(chr(fill_char) * self.TARGET_PAGE_SIZE)
187                if self.dump_memory:
188                    self.memory['%s (0x%016x)' % (self.name, addr)] = 'Filled with 0x%02x' % fill_char
189                flags &= ~self.RAM_SAVE_FLAG_COMPRESS
190            elif flags & self.RAM_SAVE_FLAG_PAGE:
191                if flags & self.RAM_SAVE_FLAG_CONTINUE:
192                    flags &= ~self.RAM_SAVE_FLAG_CONTINUE
193                else:
194                    self.name = self.file.readstr()
195
196                if self.write_memory or self.dump_memory:
197                    data = self.file.readvar(size = self.TARGET_PAGE_SIZE)
198                else: # Just skip RAM data
199                    self.file.file.seek(self.TARGET_PAGE_SIZE, 1)
200
201                if self.write_memory:
202                    self.files[self.name].seek(addr, os.SEEK_SET)
203                    self.files[self.name].write(data)
204                if self.dump_memory:
205                    hexdata = " ".join("{0:02x}".format(ord(c)) for c in data)
206                    self.memory['%s (0x%016x)' % (self.name, addr)] = hexdata
207
208                flags &= ~self.RAM_SAVE_FLAG_PAGE
209            elif flags & self.RAM_SAVE_FLAG_XBZRLE:
210                raise Exception("XBZRLE RAM compression is not supported yet")
211            elif flags & self.RAM_SAVE_FLAG_HOOK:
212                raise Exception("RAM hooks don't make sense with files")
213            if flags & self.RAM_SAVE_FLAG_MULTIFD_FLUSH:
214                continue
215
216            # End of RAM section
217            if flags & self.RAM_SAVE_FLAG_EOS:
218                break
219
220            if flags != 0:
221                raise Exception("Unknown RAM flags: %x" % flags)
222
223    def __del__(self):
224        if self.write_memory:
225            for key in self.files:
226                self.files[key].close()
227
228
229class HTABSection(object):
230    HASH_PTE_SIZE_64       = 16
231
232    def __init__(self, file, version_id, device, section_key):
233        if version_id != 1:
234            raise Exception("Unknown HTAB version %d" % version_id)
235
236        self.file = file
237        self.section_key = section_key
238
239    def read(self):
240
241        header = self.file.read32()
242
243        if (header == -1):
244            # "no HPT" encoding
245            return
246
247        if (header > 0):
248            # First section, just the hash shift
249            return
250
251        # Read until end marker
252        while True:
253            index = self.file.read32()
254            n_valid = self.file.read16()
255            n_invalid = self.file.read16()
256
257            if index == 0 and n_valid == 0 and n_invalid == 0:
258                break
259
260            self.file.readvar(n_valid * self.HASH_PTE_SIZE_64)
261
262    def getDict(self):
263        return ""
264
265
266class S390StorageAttributes(object):
267    STATTR_FLAG_EOS   = 0x01
268    STATTR_FLAG_MORE  = 0x02
269    STATTR_FLAG_ERROR = 0x04
270    STATTR_FLAG_DONE  = 0x08
271
272    def __init__(self, file, version_id, device, section_key):
273        if version_id != 0:
274            raise Exception("Unknown storage_attributes version %d" % version_id)
275
276        self.file = file
277        self.section_key = section_key
278
279    def read(self):
280        while True:
281            addr_flags = self.file.read64()
282            flags = addr_flags & 0xfff
283            if (flags & (self.STATTR_FLAG_DONE | self.STATTR_FLAG_EOS)):
284                return
285            if (flags & self.STATTR_FLAG_ERROR):
286                raise Exception("Error in migration stream")
287            count = self.file.read64()
288            self.file.readvar(count)
289
290    def getDict(self):
291        return ""
292
293
294class ConfigurationSection(object):
295    def __init__(self, file, desc):
296        self.file = file
297        self.desc = desc
298        self.caps = []
299
300    def parse_capabilities(self, vmsd_caps):
301        if not vmsd_caps:
302            return
303
304        ncaps = vmsd_caps.data['caps_count'].data
305        self.caps = vmsd_caps.data['capabilities']
306
307        if type(self.caps) != list:
308            self.caps = [self.caps]
309
310        if len(self.caps) != ncaps:
311            raise Exception("Number of capabilities doesn't match "
312                            "caps_count field")
313
314    def has_capability(self, cap):
315        return any([str(c) == cap for c in self.caps])
316
317    def read(self):
318        if self.desc:
319            version_id = self.desc['version']
320            section = VMSDSection(self.file, version_id, self.desc,
321                                  'configuration')
322            section.read()
323            self.parse_capabilities(
324                section.data.get("configuration/capabilities"))
325        else:
326            # backward compatibility for older streams that don't have
327            # the configuration section in the json
328            name_len = self.file.read32()
329            name = self.file.readstr(len = name_len)
330
331class VMSDFieldGeneric(object):
332    def __init__(self, desc, file):
333        self.file = file
334        self.desc = desc
335        self.data = ""
336
337    def __repr__(self):
338        return str(self.__str__())
339
340    def __str__(self):
341        return " ".join("{0:02x}".format(c) for c in self.data)
342
343    def getDict(self):
344        return self.__str__()
345
346    def read(self):
347        size = int(self.desc['size'])
348        self.data = self.file.readvar(size)
349        return self.data
350
351class VMSDFieldCap(object):
352    def __init__(self, desc, file):
353        self.file = file
354        self.desc = desc
355        self.data = ""
356
357    def __repr__(self):
358        return self.data
359
360    def __str__(self):
361        return self.data
362
363    def read(self):
364        len = self.file.read8()
365        self.data = self.file.readstr(len)
366
367
368class VMSDFieldInt(VMSDFieldGeneric):
369    def __init__(self, desc, file):
370        super(VMSDFieldInt, self).__init__(desc, file)
371        self.size = int(desc['size'])
372        self.format = '0x%%0%dx' % (self.size * 2)
373        self.sdtype = '>i%d' % self.size
374        self.udtype = '>u%d' % self.size
375
376    def __repr__(self):
377        if self.data < 0:
378            return ('%s (%d)' % ((self.format % self.udata), self.data))
379        else:
380            return self.format % self.data
381
382    def __str__(self):
383        return self.__repr__()
384
385    def getDict(self):
386        return self.__str__()
387
388    def read(self):
389        super(VMSDFieldInt, self).read()
390        self.sdata = int.from_bytes(self.data, byteorder='big', signed=True)
391        self.udata = int.from_bytes(self.data, byteorder='big', signed=False)
392        self.data = self.sdata
393        return self.data
394
395class VMSDFieldUInt(VMSDFieldInt):
396    def __init__(self, desc, file):
397        super(VMSDFieldUInt, self).__init__(desc, file)
398
399    def read(self):
400        super(VMSDFieldUInt, self).read()
401        self.data = self.udata
402        return self.data
403
404class VMSDFieldIntLE(VMSDFieldInt):
405    def __init__(self, desc, file):
406        super(VMSDFieldIntLE, self).__init__(desc, file)
407        self.dtype = '<i%d' % self.size
408
409class VMSDFieldBool(VMSDFieldGeneric):
410    def __init__(self, desc, file):
411        super(VMSDFieldBool, self).__init__(desc, file)
412
413    def __repr__(self):
414        return self.data.__repr__()
415
416    def __str__(self):
417        return self.data.__str__()
418
419    def getDict(self):
420        return self.data
421
422    def read(self):
423        super(VMSDFieldBool, self).read()
424        if self.data[0] == 0:
425            self.data = False
426        else:
427            self.data = True
428        return self.data
429
430class VMSDFieldStruct(VMSDFieldGeneric):
431    QEMU_VM_SUBSECTION    = 0x05
432
433    def __init__(self, desc, file):
434        super(VMSDFieldStruct, self).__init__(desc, file)
435        self.data = collections.OrderedDict()
436
437        # When we see compressed array elements, unfold them here
438        new_fields = []
439        for field in self.desc['struct']['fields']:
440            if not 'array_len' in field:
441                new_fields.append(field)
442                continue
443            array_len = field.pop('array_len')
444            field['index'] = 0
445            new_fields.append(field)
446            for i in range(1, array_len):
447                c = field.copy()
448                c['index'] = i
449                new_fields.append(c)
450
451        self.desc['struct']['fields'] = new_fields
452
453    def __repr__(self):
454        return self.data.__repr__()
455
456    def __str__(self):
457        return self.data.__str__()
458
459    def read(self):
460        for field in self.desc['struct']['fields']:
461            try:
462                reader = vmsd_field_readers[field['type']]
463            except:
464                reader = VMSDFieldGeneric
465
466            field['data'] = reader(field, self.file)
467            field['data'].read()
468
469            if 'index' in field:
470                if field['name'] not in self.data:
471                    self.data[field['name']] = []
472                a = self.data[field['name']]
473                if len(a) != int(field['index']):
474                    raise Exception("internal index of data field unmatched (%d/%d)" % (len(a), int(field['index'])))
475                a.append(field['data'])
476            else:
477                self.data[field['name']] = field['data']
478
479        if 'subsections' in self.desc['struct']:
480            for subsection in self.desc['struct']['subsections']:
481                if self.file.read8() != self.QEMU_VM_SUBSECTION:
482                    raise Exception("Subsection %s not found at offset %x" % ( subsection['vmsd_name'], self.file.tell()))
483                name = self.file.readstr()
484                version_id = self.file.read32()
485                self.data[name] = VMSDSection(self.file, version_id, subsection, (name, 0))
486                self.data[name].read()
487
488    def getDictItem(self, value):
489       # Strings would fall into the array category, treat
490       # them specially
491       if value.__class__ is ''.__class__:
492           return value
493
494       try:
495           return self.getDictOrderedDict(value)
496       except:
497           try:
498               return self.getDictArray(value)
499           except:
500               try:
501                   return value.getDict()
502               except:
503                   return value
504
505    def getDictArray(self, array):
506        r = []
507        for value in array:
508           r.append(self.getDictItem(value))
509        return r
510
511    def getDictOrderedDict(self, dict):
512        r = collections.OrderedDict()
513        for (key, value) in dict.items():
514            r[key] = self.getDictItem(value)
515        return r
516
517    def getDict(self):
518        return self.getDictOrderedDict(self.data)
519
520vmsd_field_readers = {
521    "bool" : VMSDFieldBool,
522    "int8" : VMSDFieldInt,
523    "int16" : VMSDFieldInt,
524    "int32" : VMSDFieldInt,
525    "int32 equal" : VMSDFieldInt,
526    "int32 le" : VMSDFieldIntLE,
527    "int64" : VMSDFieldInt,
528    "uint8" : VMSDFieldUInt,
529    "uint16" : VMSDFieldUInt,
530    "uint32" : VMSDFieldUInt,
531    "uint32 equal" : VMSDFieldUInt,
532    "uint64" : VMSDFieldUInt,
533    "int64 equal" : VMSDFieldInt,
534    "uint8 equal" : VMSDFieldInt,
535    "uint16 equal" : VMSDFieldInt,
536    "float64" : VMSDFieldGeneric,
537    "timer" : VMSDFieldGeneric,
538    "buffer" : VMSDFieldGeneric,
539    "unused_buffer" : VMSDFieldGeneric,
540    "bitmap" : VMSDFieldGeneric,
541    "struct" : VMSDFieldStruct,
542    "capability": VMSDFieldCap,
543    "unknown" : VMSDFieldGeneric,
544}
545
546class VMSDSection(VMSDFieldStruct):
547    def __init__(self, file, version_id, device, section_key):
548        self.file = file
549        self.data = ""
550        self.vmsd_name = ""
551        self.section_key = section_key
552        desc = device
553        if 'vmsd_name' in device:
554            self.vmsd_name = device['vmsd_name']
555
556        # A section really is nothing but a FieldStruct :)
557        super(VMSDSection, self).__init__({ 'struct' : desc }, file)
558
559###############################################################################
560
561class MigrationDump(object):
562    QEMU_VM_FILE_MAGIC    = 0x5145564d
563    QEMU_VM_FILE_VERSION  = 0x00000003
564    QEMU_VM_EOF           = 0x00
565    QEMU_VM_SECTION_START = 0x01
566    QEMU_VM_SECTION_PART  = 0x02
567    QEMU_VM_SECTION_END   = 0x03
568    QEMU_VM_SECTION_FULL  = 0x04
569    QEMU_VM_SUBSECTION    = 0x05
570    QEMU_VM_VMDESCRIPTION = 0x06
571    QEMU_VM_CONFIGURATION = 0x07
572    QEMU_VM_SECTION_FOOTER= 0x7e
573
574    def __init__(self, filename):
575        self.section_classes = {
576            ( 'ram', 0 ) : [ RamSection, None ],
577            ( 's390-storage_attributes', 0 ) : [ S390StorageAttributes, None],
578            ( 'spapr/htab', 0) : ( HTABSection, None )
579        }
580        self.filename = filename
581        self.vmsd_desc = None
582
583    def read(self, desc_only = False, dump_memory = False, write_memory = False):
584        # Read in the whole file
585        file = MigrationFile(self.filename)
586
587        # File magic
588        data = file.read32()
589        if data != self.QEMU_VM_FILE_MAGIC:
590            raise Exception("Invalid file magic %x" % data)
591
592        # Version (has to be v3)
593        data = file.read32()
594        if data != self.QEMU_VM_FILE_VERSION:
595            raise Exception("Invalid version number %d" % data)
596
597        self.load_vmsd_json(file)
598
599        # Read sections
600        self.sections = collections.OrderedDict()
601
602        if desc_only:
603            return
604
605        ramargs = {}
606        ramargs['page_size'] = self.vmsd_desc['page_size']
607        ramargs['dump_memory'] = dump_memory
608        ramargs['write_memory'] = write_memory
609        ramargs['ignore_shared'] = False
610        self.section_classes[('ram',0)][1] = ramargs
611
612        while True:
613            section_type = file.read8()
614            if section_type == self.QEMU_VM_EOF:
615                break
616            elif section_type == self.QEMU_VM_CONFIGURATION:
617                config_desc = self.vmsd_desc.get('configuration')
618                section = ConfigurationSection(file, config_desc)
619                section.read()
620                ramargs['ignore_shared'] = section.has_capability('x-ignore-shared')
621            elif section_type == self.QEMU_VM_SECTION_START or section_type == self.QEMU_VM_SECTION_FULL:
622                section_id = file.read32()
623                name = file.readstr()
624                instance_id = file.read32()
625                version_id = file.read32()
626                section_key = (name, instance_id)
627                classdesc = self.section_classes[section_key]
628                section = classdesc[0](file, version_id, classdesc[1], section_key)
629                self.sections[section_id] = section
630                section.read()
631            elif section_type == self.QEMU_VM_SECTION_PART or section_type == self.QEMU_VM_SECTION_END:
632                section_id = file.read32()
633                self.sections[section_id].read()
634            elif section_type == self.QEMU_VM_SECTION_FOOTER:
635                read_section_id = file.read32()
636                if read_section_id != section_id:
637                    raise Exception("Mismatched section footer: %x vs %x" % (read_section_id, section_id))
638            else:
639                raise Exception("Unknown section type: %d" % section_type)
640        file.close()
641
642    def load_vmsd_json(self, file):
643        vmsd_json = file.read_migration_debug_json()
644        self.vmsd_desc = json.loads(vmsd_json, object_pairs_hook=collections.OrderedDict)
645        for device in self.vmsd_desc['devices']:
646            key = (device['name'], device['instance_id'])
647            value = ( VMSDSection, device )
648            self.section_classes[key] = value
649
650    def getDict(self):
651        r = collections.OrderedDict()
652        for (key, value) in self.sections.items():
653           key = "%s (%d)" % ( value.section_key[0], key )
654           r[key] = value.getDict()
655        return r
656
657###############################################################################
658
659class JSONEncoder(json.JSONEncoder):
660    def default(self, o):
661        if isinstance(o, VMSDFieldGeneric):
662            return str(o)
663        return json.JSONEncoder.default(self, o)
664
665parser = argparse.ArgumentParser()
666parser.add_argument("-f", "--file", help='migration dump to read from', required=True)
667parser.add_argument("-m", "--memory", help='dump RAM contents as well', action='store_true')
668parser.add_argument("-d", "--dump", help='what to dump ("state" or "desc")', default='state')
669parser.add_argument("-x", "--extract", help='extract contents into individual files', action='store_true')
670args = parser.parse_args()
671
672jsonenc = JSONEncoder(indent=4, separators=(',', ': '))
673
674if args.extract:
675    dump = MigrationDump(args.file)
676
677    dump.read(desc_only = True)
678    print("desc.json")
679    f = open("desc.json", "w")
680    f.truncate()
681    f.write(jsonenc.encode(dump.vmsd_desc))
682    f.close()
683
684    dump.read(write_memory = True)
685    dict = dump.getDict()
686    print("state.json")
687    f = open("state.json", "w")
688    f.truncate()
689    f.write(jsonenc.encode(dict))
690    f.close()
691elif args.dump == "state":
692    dump = MigrationDump(args.file)
693    dump.read(dump_memory = args.memory)
694    dict = dump.getDict()
695    print(jsonenc.encode(dict))
696elif args.dump == "desc":
697    dump = MigrationDump(args.file)
698    dump.read(desc_only = True)
699    print(jsonenc.encode(dump.vmsd_desc))
700else:
701    raise Exception("Please specify either -x, -d state or -d desc")
702