1 # Copyright (c) 2012-2013 Intel, Inc.
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.
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.
13 This module implements the block map (bmap) creation functionality and provides
14 the corresponding API in form of the 'BmapCreate' class.
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.
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.
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.
37 This module uses the FIBMAP ioctl to detect holes.
40 # Disable the following pylint recommendations:
41 # * Too many instance attributes - R0902
42 # * Too few public methods - R0903
43 # pylint: disable=R0902,R0903
47 from mic.utils.misc import human_size
48 from mic.utils import Filemap
50 # The bmap format version we generate.
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"
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
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.
79 Besides the machine-readable data, this file contains useful commentaries
80 which contain human-readable information like image size, percentage of
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. -->
89 <!-- Image size in bytes: %s -->
90 <ImageSize> %u </ImageSize>
92 <!-- Size of a block in bytes -->
93 <BlockSize> %u </BlockSize>
95 <!-- Count of blocks in the image file -->
96 <BlocksCount> %u </BlocksCount>
100 class Error(Exception):
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.
108 class BmapCreate(object):
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:
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
117 Then you should invoke the 'generate()' method of this class. It will use
118 the FIEMAP ioctl to generate the bmap.
121 def __init__(self, image, bmap, chksum_type="sha256", log=None):
123 Initialize a class instance:
124 * image - full path or a file-like object of the image to create bmap
126 * bmap - full path or a file object to use for writing the resulting
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.
134 if self._log is None:
135 self._log = logging.getLogger(__name__)
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
146 self._mapped_count_pos1 = None
147 self._mapped_count_pos2 = None
148 self._chksum_pos = None
150 self._f_image_needs_close = False
151 self._f_bmap_needs_close = False
153 self._cs_type = chksum_type.lower()
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))
160 if hasattr(image, "read"):
161 self._f_image = image
162 self._image_path = image.name
164 self._image_path = image
165 self._open_image_file()
167 if hasattr(bmap, "read"):
169 self._bmap_path = bmap.name
171 self._bmap_path = bmap
172 self._open_bmap_file()
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)
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'"
185 self.block_size = self.filemap.block_size
186 self.blocks_cnt = self.filemap.blocks_cnt
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:
195 def _open_image_file(self):
196 """Open the image file."""
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))
203 self._f_image_needs_close = True
205 def _open_bmap_file(self):
206 """Open the bmap file."""
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))
213 self._f_bmap_needs_close = True
215 def _bmap_file_start(self):
217 A helper function which generates the starting contents of the block
218 map file: the header comment, image size, block size, etc.
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
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: "
230 self._f_bmap.write(xml)
231 self._mapped_count_pos1 = self._f_bmap.tell()
233 xml = "%s or %s -->\n" % (' ' * len(self.image_size_human),
235 xml += " <MappedBlocksCount> "
237 self._f_bmap.write(xml)
238 self._mapped_count_pos2 = self._f_bmap.tell()
240 xml = "%s </MappedBlocksCount>\n\n" % (' ' * len(str(self.blocks_cnt)))
242 # pylint: disable=C0301
243 xml += " <!-- Type of checksum used in this file -->\n"
244 xml += " <ChecksumType> %s </ChecksumType>\n\n" % self._cs_type
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> "
250 self._f_bmap.write(xml)
251 self._chksum_pos = self._f_bmap.tell()
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
260 self._f_bmap.write(xml)
262 def _bmap_file_end(self):
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
269 xml = " </BlockMap>\n"
272 self._f_bmap.write(xml)
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))
278 self._f_bmap.seek(self._mapped_count_pos2)
279 self._f_bmap.write("%u" % self.mapped_cnt)
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)
288 def _calculate_chksum(self, first, last):
290 A helper function which calculates checksum for the range of blocks of
291 the image file: from block 'first' to block 'last'.
294 start = first * self.block_size
295 end = (last + 1) * self.block_size
297 self._f_image.seek(start)
298 hash_obj = hashlib.new(self._cs_type)
300 chunk_size = 1024*1024
301 to_read = end - start
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)
311 return hash_obj.hexdigest()
313 def generate(self, include_checksums=True):
315 Generate bmap for the image file. If 'include_checksums' is 'True',
316 also generate checksums for block ranges.
319 # Save image file position in order to restore it at the end
320 image_pos = self._f_image.tell()
322 self._bmap_file_start()
324 # Generate the block map and write it to the XML block map
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
336 self._f_bmap.write(" <Range%s> %s-%s </Range>\n"
337 % (chksum, first, last))
339 self._f_bmap.write(" <Range%s> %s </Range>\n"
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
346 self._bmap_file_end()
350 except IOError as err:
351 raise Error("cannot flush the bmap file '%s': %s"
352 % (self._bmap_path, err))
354 self._f_image.seek(image_pos)