Bmap: sync with the latest bmap-tools
[tools/mic.git] / mic / utils / BmapCreate.py
1 # Copyright (c) 2012-2013 Intel, Inc.
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License, version 2,
5 # as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 # General Public License for more details.
11
12 """
13 This module implements the block map (bmap) creation functionality and provides
14 the corresponding API in form of the 'BmapCreate' class.
15
16 The idea is that while images files may generally be very large (e.g., 4GiB),
17 they may nevertheless contain only little real data, e.g., 512MiB. This data
18 are files, directories, file-system meta-data, partition table, etc. When
19 copying the image to the target device, you do not have to copy all the 4GiB of
20 data, you can copy only 512MiB of it, which is 4 times less, so copying should
21 presumably be 4 times faster.
22
23 The block map file is an XML file which contains a list of blocks which have to
24 be copied to the target device. The other blocks are not used and there is no
25 need to copy them. The XML file also contains some additional information like
26 block size, image size, count of mapped blocks, etc. There are also many
27 commentaries, so it is human-readable.
28
29 The image has to be a sparse file. Generally, this means that when you generate
30 this image file, you should start with a huge sparse file which contains a
31 single hole spanning the entire file. Then you should partition it, write all
32 the data (probably by means of loop-back mounting the image or parts of it),
33 etc. The end result should be a sparse file where mapped areas represent useful
34 parts of the image and holes represent useless parts of the image, which do not
35 have to be copied when copying the image to the target device.
36
37 This module uses the FIBMAP ioctl to detect holes.
38 """
39
40 # Disable the following pylint recommendations:
41 #   *  Too many instance attributes - R0902
42 #   *  Too few public methods - R0903
43 # pylint: disable=R0902,R0903
44
45 import hashlib
46 import logging
47 from mic.utils.misc import human_size
48 from mic.utils import Filemap
49
50 # The bmap format version we generate.
51 #
52 # Changelog:
53 # o 1.3 -> 2.0:
54 #   Support SHA256 and SHA512 checksums, in 1.3 only SHA1 was supported.
55 #   "BmapFileChecksum" is used instead of "BmapFileSHA1", and "chksum="
56 #   attribute is used instead "sha1=". Introduced "ChecksumType" tag. This is
57 #   an incompatible change.
58 #   Note, bmap format 1.4 is identical to 2.0. Version 1.4 was a mistake,
59 #   instead of incrementing the major version number, we incremented minor
60 #   version number. Unfortunately, the mistake slipped into bmap-tools version
61 #   3.0, and was only fixed in bmap-tools v3.1.
62 SUPPORTED_BMAP_VERSION = "2.0"
63
64 _BMAP_START_TEMPLATE = \
65 """<?xml version="1.0" ?>
66 <!-- This file contains the block map for an image file, which is basically
67      a list of useful (mapped) block numbers in the image file. In other words,
68      it lists only those blocks which contain data (boot sector, partition
69      table, file-system metadata, files, directories, extents, etc). These
70      blocks have to be copied to the target device. The other blocks do not
71      contain any useful data and do not have to be copied to the target
72      device.
73
74      The block map an optimization which allows to copy or flash the image to
75      the image quicker than copying of flashing the entire image. This is
76      because with bmap less data is copied: <MappedBlocksCount> blocks instead
77      of <BlocksCount> blocks.
78
79      Besides the machine-readable data, this file contains useful commentaries
80      which contain human-readable information like image size, percentage of
81      mapped data, etc.
82
83      The 'version' attribute is the block map file format version in the
84      'major.minor' format. The version major number is increased whenever an
85      incompatible block map format change is made. The minor number changes
86      in case of minor backward-compatible changes. -->
87
88 <bmap version="%s">
89     <!-- Image size in bytes: %s -->
90     <ImageSize> %u </ImageSize>
91
92     <!-- Size of a block in bytes -->
93     <BlockSize> %u </BlockSize>
94
95     <!-- Count of blocks in the image file -->
96     <BlocksCount> %u </BlocksCount>
97
98 """
99
100 class Error(Exception):
101     """
102     A class for exceptions generated by this module. We currently support only
103     one type of exceptions, and we basically throw human-readable problem
104     description in case of errors.
105     """
106     pass
107
108 class BmapCreate(object):
109     """
110     This class implements the bmap creation functionality. To generate a bmap
111     for an image (which is supposedly a sparse file), you should first create
112     an instance of 'BmapCreate' and provide:
113
114     * full path or a file-like object of the image to create bmap for
115     * full path or a file object to use for writing the results to
116
117     Then you should invoke the 'generate()' method of this class. It will use
118     the FIEMAP ioctl to generate the bmap.
119     """
120
121     def __init__(self, image, bmap, chksum_type="sha256", log=None):
122         """
123         Initialize a class instance:
124         * image  - full path or a file-like object of the image to create bmap
125                    for
126         * bmap   - full path or a file object to use for writing the resulting
127                    bmap to
128         * chksum - type of the check sum to use in the bmap file (all checksum
129                    types which python's "hashlib" module supports are allowed).
130         * log     - the logger object to use for printing messages.
131         """
132
133         self._log = log
134         if self._log is None:
135             self._log = logging.getLogger(__name__)
136
137         self.image_size = None
138         self.image_size_human = None
139         self.block_size = None
140         self.blocks_cnt = None
141         self.mapped_cnt = None
142         self.mapped_size = None
143         self.mapped_size_human = None
144         self.mapped_percent = None
145
146         self._mapped_count_pos1 = None
147         self._mapped_count_pos2 = None
148         self._chksum_pos = None
149
150         self._f_image_needs_close = False
151         self._f_bmap_needs_close = False
152
153         self._cs_type = chksum_type.lower()
154         try:
155             self._cs_len = len(hashlib.new(self._cs_type).hexdigest())
156         except ValueError as err:
157             raise Error("cannot initialize hash function \"%s\": %s" %
158                         (self._cs_type, err))
159
160         if hasattr(image, "read"):
161             self._f_image = image
162             self._image_path = image.name
163         else:
164             self._image_path = image
165             self._open_image_file()
166
167         if hasattr(bmap, "read"):
168             self._f_bmap = bmap
169             self._bmap_path = bmap.name
170         else:
171             self._bmap_path = bmap
172             self._open_bmap_file()
173
174         try:
175             self.filemap = Filemap.filemap(self._f_image, self._log)
176         except (Filemap.Error, Filemap.ErrorNotSupp) as err:
177             raise Error("cannot generate bmap: %s" % err)
178
179         self.image_size = self.filemap.image_size
180         self.image_size_human = human_size(self.image_size)
181         if self.image_size == 0:
182             raise Error("cannot generate bmap for zero-sized image file '%s'"
183                         % self._image_path)
184
185         self.block_size = self.filemap.block_size
186         self.blocks_cnt = self.filemap.blocks_cnt
187
188     def __del__(self):
189         """The class destructor which closes the opened files."""
190         if self._f_image_needs_close:
191             self._f_image.close()
192         if self._f_bmap_needs_close:
193             self._f_bmap.close()
194
195     def _open_image_file(self):
196         """Open the image file."""
197         try:
198             self._f_image = open(self._image_path, 'rb')
199         except IOError as err:
200             raise Error("cannot open image file '%s': %s"
201                         % (self._image_path, err))
202
203         self._f_image_needs_close = True
204
205     def _open_bmap_file(self):
206         """Open the bmap file."""
207         try:
208             self._f_bmap = open(self._bmap_path, 'w+')
209         except IOError as err:
210             raise Error("cannot open bmap file '%s': %s"
211                         % (self._bmap_path, err))
212
213         self._f_bmap_needs_close = True
214
215     def _bmap_file_start(self):
216         """
217         A helper function which generates the starting contents of the block
218         map file: the header comment, image size, block size, etc.
219         """
220
221         # We do not know the amount of mapped blocks at the moment, so just put
222         # whitespaces instead of real numbers. Assume the longest possible
223         # numbers.
224
225         xml = _BMAP_START_TEMPLATE \
226                % (SUPPORTED_BMAP_VERSION, self.image_size_human,
227                   self.image_size, self.block_size, self.blocks_cnt)
228         xml += "    <!-- Count of mapped blocks: "
229
230         self._f_bmap.write(xml)
231         self._mapped_count_pos1 = self._f_bmap.tell()
232
233         xml  = "%s or %s   -->\n" % (' ' * len(self.image_size_human),
234                                    ' ' * len("100.0%"))
235         xml += "    <MappedBlocksCount> "
236
237         self._f_bmap.write(xml)
238         self._mapped_count_pos2 = self._f_bmap.tell()
239
240         xml  = "%s </MappedBlocksCount>\n\n" % (' ' * len(str(self.blocks_cnt)))
241
242         # pylint: disable=C0301
243         xml += "    <!-- Type of checksum used in this file -->\n"
244         xml += "    <ChecksumType> %s </ChecksumType>\n\n" % self._cs_type
245
246         xml += "    <!-- The checksum of this bmap file. When it is calculated, the value of\n"
247         xml += "         the checksum has be zero (all ASCII \"0\" symbols).  -->\n"
248         xml += "    <BmapFileChecksum> "
249
250         self._f_bmap.write(xml)
251         self._chksum_pos = self._f_bmap.tell()
252
253         xml = "0" * self._cs_len + " </BmapFileChecksum>\n\n"
254         xml += "    <!-- The block map which consists of elements which may either be a\n"
255         xml += "         range of blocks or a single block. The 'chksum' attribute\n"
256         xml += "         (if present) is the checksum of this blocks range. -->\n"
257         xml += "    <BlockMap>\n"
258         # pylint: enable=C0301
259
260         self._f_bmap.write(xml)
261
262     def _bmap_file_end(self):
263         """
264         A helper function which generates the final parts of the block map
265         file: the ending tags and the information about the amount of mapped
266         blocks.
267         """
268
269         xml =  "    </BlockMap>\n"
270         xml += "</bmap>\n"
271
272         self._f_bmap.write(xml)
273
274         self._f_bmap.seek(self._mapped_count_pos1)
275         self._f_bmap.write("%s or %.1f%%"
276                            % (self.mapped_size_human, self.mapped_percent))
277
278         self._f_bmap.seek(self._mapped_count_pos2)
279         self._f_bmap.write("%u" % self.mapped_cnt)
280
281         self._f_bmap.seek(0)
282         hash_obj = hashlib.new(self._cs_type)
283         hash_obj.update(self._f_bmap.read())
284         chksum = hash_obj.hexdigest()
285         self._f_bmap.seek(self._chksum_pos)
286         self._f_bmap.write("%s" % chksum)
287
288     def _calculate_chksum(self, first, last):
289         """
290         A helper function which calculates checksum for the range of blocks of
291         the image file: from block 'first' to block 'last'.
292         """
293
294         start = first * self.block_size
295         end = (last + 1) * self.block_size
296
297         self._f_image.seek(start)
298         hash_obj = hashlib.new(self._cs_type)
299
300         chunk_size = 1024*1024
301         to_read = end - start
302         read = 0
303
304         while read < to_read:
305             if read + chunk_size > to_read:
306                 chunk_size = to_read - read
307             chunk = self._f_image.read(chunk_size)
308             hash_obj.update(chunk)
309             read += chunk_size
310
311         return hash_obj.hexdigest()
312
313     def generate(self, include_checksums=True):
314         """
315         Generate bmap for the image file. If 'include_checksums' is 'True',
316         also generate checksums for block ranges.
317         """
318
319         # Save image file position in order to restore it at the end
320         image_pos = self._f_image.tell()
321
322         self._bmap_file_start()
323
324         # Generate the block map and write it to the XML block map
325         # file as we go.
326         self.mapped_cnt = 0
327         for first, last in self.filemap.get_mapped_ranges(0, self.blocks_cnt):
328             self.mapped_cnt += last - first + 1
329             if include_checksums:
330                 chksum = self._calculate_chksum(first, last)
331                 chksum = " chksum=\"%s\"" % chksum
332             else:
333                 chksum = ""
334
335             if first != last:
336                 self._f_bmap.write("        <Range%s> %s-%s </Range>\n"
337                                    % (chksum, first, last))
338             else:
339                 self._f_bmap.write("        <Range%s> %s </Range>\n"
340                                    % (chksum, first))
341
342         self.mapped_size = self.mapped_cnt * self.block_size
343         self.mapped_size_human = human_size(self.mapped_size)
344         self.mapped_percent = (self.mapped_cnt * 100.0) /  self.blocks_cnt
345
346         self._bmap_file_end()
347
348         try:
349             self._f_bmap.flush()
350         except IOError as err:
351             raise Error("cannot flush the bmap file '%s': %s"
352                         % (self._bmap_path, err))
353
354         self._f_image.seek(image_pos)