cbc1e067edc8ee9b53871ef896f575c41b3c8dde
[tools/itest-core.git] / imgdiff / info.py
1 '''Get image information, such as partition table, block id fstab etc.
2 '''
3 import re
4 import os
5 import sys
6 from subprocess import check_output, CalledProcessError
7 from itertools import ifilter, islice, chain
8
9
10 def parted(img):
11     "Parse output of parted command"
12     column = re.compile(r'([A-Z][a-z\s]*?)((?=[A-Z])|$)')
13
14     def parse(output):
15         '''Example:
16         Model:  (file)
17         Disk /home/xxx/tmp/images/small.raw: 839909376B
18         Sector size (logical/physical): 512B/512B
19         Partition Table: msdos
20
21         Number  Start      End         Size        Type     File system  Flags
22          1      1048576B   34602495B   33553920B   primary  ext4         boot
23          2      34603008B  839909375B  805306368B  primary  ext4
24         '''
25         state = 'header'
26         headers = {}
27         parts = []
28         for line in output.splitlines():
29             if state == 'header':
30                 if line == '':
31                     state = 'title'
32                 else:
33                     key, val = line.split(':', 1)
34                     headers[key.lower()] = val.strip()
35             elif state == 'title':
36                 titles = []
37                 start = 0
38                 for col, _ in column.findall(line):
39                     title = col.rstrip().lower()
40                     getter = slice(start, start+len(col))
41                     titles.append((title, getter))
42                     start += len(col)
43                 state = 'parts'
44             elif line.strip():
45                 part = dict([(title, line[getter].strip())
46                              for title, getter in titles])
47                 for title in ('start',):  # start, end, size
48                     part[title] = int(part[title][:-1])  # remove tailing "B"
49                 part['number'] = int(part['number'])
50                 parts.append(part)
51         return parts
52
53     cmd = ['parted', img, '-s', 'unit B print']
54     output = check_output(cmd)
55     return parse(output)
56
57
58 def blkid(img, offset_in_bytes):
59     "Parse output of blkid command"
60     def parse(output):
61         '''Example:
62         sdb.raw: LABEL="boot" UUID="2995b233-ff79-4719-806d-d7f42b34a133" \
63              VERSION="1.0" TYPE="ext4" USAGE="filesystem"
64         '''
65         output = output.splitlines()[0].split(': ', 1)[1]
66         info = {}
67         for item in output.split():
68             key, val = item.split('=', 1)
69             info[key.lower()] = val[1:-1]  # remove double quotes
70         return info
71
72     cmd = ['blkid', '-p', '-O', str(offset_in_bytes), '-o', 'full', img]
73     output = check_output(cmd)
74     return parse(output)
75
76
77 def gdisk(img):
78     "Parse output of gdisk"
79     cmd = ['gdisk', '-l', img]
80
81     def parse(output):
82         """Example:
83         GPT fdisk (gdisk) version 0.8.1
84
85         Partition table scan:
86           MBR: protective
87           BSD: not present
88           APM: not present
89           GPT: present
90
91         Found valid GPT with protective MBR; using GPT.
92         Disk tizen_20131115.3_ivi-efi-i586-sdb.raw: 7809058 sectors, 3.7 GiB
93         Logical sector size: 512 bytes
94         Disk identifier (GUID): 4A6D60CE-C42D-4A81-B82B-120624CE867E
95         Partition table holds up to 128 entries
96         First usable sector is 34, last usable sector is 7809024
97         Partitions will be aligned on 2048-sector boundaries
98         Total free space is 2049 sectors (1.0 MiB)
99
100         Number  Start (sector)    End (sector)  Size       Code  Name
101            1            2048          133085   64.0 MiB    EF00  primary
102            2          133120         7809023   3.7 GiB     0700  primary
103         """
104         lines = output.splitlines()
105
106         line = [i for i in lines if i.startswith('Logical sector size:')]
107         if not line:
108             raise Exception("Can't find sector size from gdisk output:%s:%s"
109                             % (" ".join(cmd), output))
110         size = int(line[0].split(':', 1)[1].strip().split()[0])
111
112         parts = []
113         lines.reverse()
114         for line in lines:
115             if not line.startswith(' ') or \
116                     not line.lstrip().split()[0].isdigit():
117                 break
118             number, start, _ = line.lstrip().split(None, 2)
119             parts.append(dict(number=int(number), start=int(start)*size))
120         return parts
121
122     output = check_output(cmd)
123     return parse(output)
124
125
126 class FSTab(dict):
127     '''
128     A dict representing fstab file.
129     Key is <mount point>, corresponding value is its whole entry
130     '''
131     def __init__(self, filename):
132         with open(filename) as stream:
133             output = stream.read()
134         data = self._parse(output)
135         super(FSTab, self).__init__(data)
136
137     FS = re.compile(r'/dev/sd[a-z](\d+)|UUID=(.*)')
138
139     def _parse(self, output):
140         '''Parse fstab in this format:
141         <file system> <mount point> <type> <options> <dump> <pass>
142         '''
143         mountpoints = {}
144         for line in output.splitlines():
145             fstype, mountpoint, _ = line.split(None, 2)
146             mres = self.FS.match(fstype)
147             if not mres:
148                 continue
149
150             number, uuid = mres.group(1), mres.group(2)
151             if number:
152                 item = {"number": number}
153             else:
154                 item = {"uuid": uuid}
155             item["entry"] = line
156             mountpoints[mountpoint] = item
157         return mountpoints
158
159     @classmethod
160     def guess(cls, paths):
161         '''Guess fstab location from all partitions of the image
162         '''
163         guess1 = (os.path.join(path, 'etc', 'fstab') for path in paths)
164         guess2 = (os.path.join(path, 'fstab') for path in paths)
165         guesses = chain(guess1, guess2)
166         exists = ifilter(os.path.exists, guesses)
167         one = list(islice(exists, 1))
168         return cls(one[0]) if one else None
169
170
171 def get_partition_info(img):
172     '''Get partition table information of image'''
173     try:
174         parts = parted(img)
175     except CalledProcessError as err:
176         print >> sys.stderr, err
177         # Sometimes parted could failed with error
178         # like this, then we try gdisk.
179         # "Error during translation: Invalid or incomplete
180         # multibyte or wide character"
181         parts = gdisk(img)
182     for part in parts:
183         part['blkid'] = blkid(img, part['start'])
184     return parts