1 # Copyright 2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
34 from image_creator.util import FatalError
35 from image_creator.gpt import GPTPartitionTable
36 from image_creator.os_type import os_cls
40 from sendfile import sendfile
44 """The instances of this class can create images out of block devices."""
46 def __init__(self, device, output, bootable=True, meta={}):
47 """Create a new Image instance"""
51 self.bootable = bootable
53 self.progress_bar = None
54 self.guestfs_device = None
58 self.g = guestfs.GuestFS()
59 self.g.add_drive_opts(self.device, readonly=0, format="raw")
61 # Before version 1.17.14 the recovery process, which is a fork of the
62 # original process that called libguestfs, did not close its inherited
63 # file descriptors. This can cause problems especially if the parent
64 # process has opened pipes. Since the recovery process is an optional
65 # feature of libguestfs, it's better to disable it.
66 self.g.set_recovery_proc(0)
67 version = self.g.version()
68 if version['major'] > 1 or \
69 (version['major'] == 1 and (version['minor'] >= 18 or
70 (version['minor'] == 17 and
71 version['release'] >= 14))):
72 self.g.set_recovery_proc(1)
73 self.out.output("Enabling recovery proc")
76 #self.g.set_verbose(1)
78 self.guestfs_enabled = False
81 """Enable a newly created Image instance"""
83 self.out.output('Launching helper VM (may take a while) ...', False)
84 # self.progressbar = self.out.Progress(100, "Launching helper VM",
86 # eh = self.g.set_event_callback(self.progress_callback,
87 # guestfs.EVENT_PROGRESS)
89 self.guestfs_enabled = True
90 # self.g.delete_event_callback(eh)
91 # self.progressbar.success('done')
92 # self.progressbar = None
93 self.out.success('done')
95 self.out.output('Inspecting Operating System ...', False)
96 roots = self.g.inspect_os()
98 raise FatalError("No operating system found")
100 raise FatalError("Multiple operating systems found."
101 "We only support images with one OS.")
103 self.guestfs_device = self.g.part_to_dev(self.root)
104 self.size = self.g.blockdev_getsize64(self.guestfs_device)
105 self.meta['PARTITION_TABLE'] = \
106 self.g.part_get_parttype(self.guestfs_device)
108 self.ostype = self.g.inspect_get_type(self.root)
109 self.distro = self.g.inspect_get_distro(self.root)
111 'found a(n) %s system' %
112 self.ostype if self.distro == "unknown" else self.distro)
115 """Return an OS class instance for this image"""
116 if hasattr(self, "_os"):
119 if not self.guestfs_enabled:
124 self.mount(readonly=True)
129 cls = os_cls(self.distro, self.ostype)
130 self._os = cls(self.root, self.g, self.out)
138 os = property(_get_os)
141 """Destroy this Image instance."""
143 # In new guestfs versions, there is a handy shutdown method for this
145 if self.guestfs_enabled:
149 # Close the guestfs handler if open
152 # def progress_callback(self, ev, eh, buf, array):
153 # position = array[2]
156 # self.progressbar.goto((position * 100) // total)
158 def mount(self, readonly=False):
159 """Mount all disk partitions in a correct order."""
161 mount = self.g.mount_ro if readonly else self.g.mount
162 msg = " read-only" if readonly else ""
163 self.out.output("Mounting the media%s ..." % msg, False)
164 mps = self.g.inspect_get_mountpoints(self.root)
166 # Sort the keys to mount the fs in a correct order.
167 # / should be mounted befor /boot, etc
169 if len(a[0]) > len(b[0]):
171 elif len(a[0]) == len(b[0]):
179 except RuntimeError as msg:
180 self.out.warn("%s (ignored)" % msg)
183 self.out.success("done")
186 """Umount all mounted filesystems."""
190 def _last_partition(self):
191 """Return the last partition of the image disk"""
192 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
193 msg = "Unsupported partition table: %s. Only msdos and gpt " \
194 "partition tables are supported" % self.meta['PARTITION_TABLE']
195 raise FatalError(msg)
197 is_extended = lambda p: \
198 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
200 is_logical = lambda p: \
201 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
203 partitions = self.g.part_list(self.guestfs_device)
204 last_partition = partitions[-1]
206 if is_logical(last_partition):
207 # The disk contains extended and logical partitions....
208 extended = filter(is_extended, partitions)[0]
209 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
211 # check if extended is the last primary partition
212 if last_primary['part_num'] > extended['part_num']:
213 last_partition = last_primary
215 return last_partition
220 This is accomplished by shrinking the last file system of the
221 image and then updating the partition table. The new disk size
222 (in bytes) is returned.
224 ATTENTION: make sure unmount is called before shrink
226 get_fstype = lambda p: \
227 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
228 is_logical = lambda p: \
229 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
230 is_extended = lambda p: \
231 self.meta['PARTITION_TABLE'] == 'msdos' and \
232 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
235 part_add = lambda ptype, start, stop: \
236 self.g.part_add(self.guestfs_device, ptype, start, stop)
237 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
238 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
239 part_set_id = lambda p, id: \
240 self.g.part_set_mbr_id(self.guestfs_device, p, id)
241 part_get_bootable = lambda p: \
242 self.g.part_get_bootable(self.guestfs_device, p)
243 part_set_bootable = lambda p, bootable: \
244 self.g.part_set_bootable(self.guestfs_device, p, bootable)
248 self.out.output("Shrinking image (this may take a while) ...", False)
250 sector_size = self.g.blockdev_getss(self.guestfs_device)
255 last_part = self._last_partition()
256 fstype = get_fstype(last_part)
259 self.meta['SWAP'] = "%d:%s" % \
260 (last_part['part_num'],
261 (last_part['part_size'] + MB - 1) // MB)
262 part_del(last_part['part_num'])
264 elif is_extended(last_part):
265 part_del(last_part['part_num'])
268 # Most disk manipulation programs leave 2048 sectors after the last
270 new_size = last_part['part_end'] + 1 + 2048 * sector_size
271 self.size = min(self.size, new_size)
274 if not re.match("ext[234]", fstype):
275 self.out.warn("Don't know how to resize %s partitions." % fstype)
278 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
279 self.g.e2fsck_f(part_dev)
280 self.g.resize2fs_M(part_dev)
282 out = self.g.tune2fs_l(part_dev)
283 block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
284 block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
286 start = last_part['part_start'] / sector_size
287 end = start + (block_size * block_cnt) / sector_size - 1
289 if is_logical(last_part):
290 partitions = self.g.part_list(self.guestfs_device)
292 logical = [] # logical partitions
293 for partition in partitions:
294 if partition['part_num'] < 4:
297 'num': partition['part_num'],
298 'start': partition['part_start'] / sector_size,
299 'end': partition['part_end'] / sector_size,
300 'id': part_get_id(partition['part_num']),
301 'bootable': part_get_bootable(partition['part_num'])
304 logical[-1]['end'] = end # new end after resize
306 # Recreate the extended partition
307 extended = filter(is_extended, partitions)[0]
308 part_del(extended['part_num'])
309 part_add('e', extended['part_start'] / sector_size, end)
311 # Create all the logical partitions back
313 part_add('l', l['start'], l['end'])
314 part_set_id(l['num'], l['id'])
315 part_set_bootable(l['num'], l['bootable'])
317 # Recreate the last partition
318 if self.meta['PARTITION_TABLE'] == 'msdos':
319 last_part['id'] = part_get_id(last_part['part_num'])
321 last_part['bootable'] = part_get_bootable(last_part['part_num'])
322 part_del(last_part['part_num'])
323 part_add('p', start, end)
324 part_set_bootable(last_part['part_num'], last_part['bootable'])
326 if self.meta['PARTITION_TABLE'] == 'msdos':
327 part_set_id(last_part['part_num'], last_part['id'])
329 new_size = (end + 1) * sector_size
331 assert (new_size <= self.size)
333 if self.meta['PARTITION_TABLE'] == 'gpt':
334 ptable = GPTPartitionTable(self.device)
335 self.size = ptable.shrink(new_size, self.size)
337 self.size = min(new_size + 2048 * sector_size, self.size)
339 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
343 def dump(self, outfile):
344 """Dumps the content of the image into a file.
346 This method will only dump the actual payload, found by reading the
347 partition table. Empty space in the end of the device will be ignored.
350 blocksize = 4 * MB # 4MB
352 progr_size = (size + MB - 1) // MB # in MB
353 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
355 with open(self.device, 'r') as src:
356 with open(outfile, "w") as dst:
361 length = min(left, blocksize)
362 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
364 # Workaround for python-sendfile API change. In
365 # python-sendfile 1.2.x (py-sendfile) the returning value
366 # of sendfile is a tuple, where in version 2.x (pysendfile)
367 # it is just a sigle integer.
368 if isinstance(sent, tuple):
373 progressbar.goto((size - left) // MB)
374 progressbar.success('image file %s was successfully created' % outfile)
376 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :