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
57 self.mounted_ro = False
59 self.g = guestfs.GuestFS()
60 self.g.add_drive_opts(self.device, readonly=0, format="raw")
62 # Before version 1.17.14 the recovery process, which is a fork of the
63 # original process that called libguestfs, did not close its inherited
64 # file descriptors. This can cause problems especially if the parent
65 # process has opened pipes. Since the recovery process is an optional
66 # feature of libguestfs, it's better to disable it.
67 self.g.set_recovery_proc(0)
68 version = self.g.version()
69 if version['major'] > 1 or \
70 (version['major'] == 1 and (version['minor'] >= 18 or
71 (version['minor'] == 17 and
72 version['release'] >= 14))):
73 self.g.set_recovery_proc(1)
74 self.out.output("Enabling recovery proc")
77 #self.g.set_verbose(1)
79 self.guestfs_enabled = False
82 """Enable a newly created Image instance"""
84 self.out.output('Launching helper VM (may take a while) ...', False)
85 # self.progressbar = self.out.Progress(100, "Launching helper VM",
87 # eh = self.g.set_event_callback(self.progress_callback,
88 # guestfs.EVENT_PROGRESS)
90 self.guestfs_enabled = True
91 # self.g.delete_event_callback(eh)
92 # self.progressbar.success('done')
93 # self.progressbar = None
94 self.out.success('done')
96 self.out.output('Inspecting Operating System ...', False)
97 roots = self.g.inspect_os()
99 raise FatalError("No operating system found")
101 raise FatalError("Multiple operating systems found."
102 "We only support images with one OS.")
104 self.guestfs_device = self.g.part_to_dev(self.root)
105 self.size = self.g.blockdev_getsize64(self.guestfs_device)
106 self.meta['PARTITION_TABLE'] = \
107 self.g.part_get_parttype(self.guestfs_device)
109 self.ostype = self.g.inspect_get_type(self.root)
110 self.distro = self.g.inspect_get_distro(self.root)
112 'found a(n) %s system' %
113 self.ostype if self.distro == "unknown" else self.distro)
116 """Return an OS class instance for this image"""
117 if hasattr(self, "_os"):
120 if not self.guestfs_enabled:
125 self.mount(readonly=True)
130 cls = os_cls(self.distro, self.ostype)
131 self._os = cls(self.root, self.g, self.out)
139 os = property(_get_os)
142 """Destroy this Image instance."""
144 # In new guestfs versions, there is a handy shutdown method for this
146 if self.guestfs_enabled:
150 # Close the guestfs handler if open
153 # def progress_callback(self, ev, eh, buf, array):
154 # position = array[2]
157 # self.progressbar.goto((position * 100) // total)
159 def mount(self, readonly=False):
160 """Mount all disk partitions in a correct order."""
162 msg = "Mounting the media%s ..." % (" read-only" if readonly else "")
163 self.out.output(msg, False)
165 #If something goes wrong when mounting rw, remount the filesystem ro
167 rw_mpoints = ('/', '/etc', '/root', '/home', '/var')
169 # Sort the keys to mount the fs in a correct order.
170 # / should be mounted befor /boot, etc
172 if len(a[0]) > len(b[0]):
174 elif len(a[0]) == len(b[0]):
178 mps = self.g.inspect_get_mountpoints(self.root)
181 mopts = 'ro' if readonly else 'rw'
183 if self.ostype == 'freebsd':
184 # libguestfs can't handle correct freebsd partitions on GUID
185 # Partition Table. We have to do the translation to linux
186 # device names ourselves
187 m = re.match('^/dev/((?:ada)|(?:vtbd))(\d+)p(\d+)$', dev)
191 dev = '/dev/sd%c%d' % (chr(ord('a') + m2), m3)
193 self.g.mount_options(mopts, dev, mp)
194 except RuntimeError as msg:
195 if self.ostype == 'freebsd':
196 freebsd_mopts = "ufstype=ufs2,%s" % mopts
198 self.g.mount_vfs(freebsd_mopts, 'ufs', dev, mp)
199 except RuntimeError as msg:
200 if readonly is False and mp in rw_mpoints:
203 elif readonly is False and mp in rw_mpoints:
207 self.out.warn("%s (ignored)" % msg)
209 self.out.warn("Unable to mount %s read-write. "
210 "Remounting everything read-only..." % mp)
215 self.mounted_ro = readonly
216 self.out.success("done")
219 """Umount all mounted filesystems."""
223 def _last_partition(self):
224 """Return the last partition of the image disk"""
225 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
226 msg = "Unsupported partition table: %s. Only msdos and gpt " \
227 "partition tables are supported" % self.meta['PARTITION_TABLE']
228 raise FatalError(msg)
230 is_extended = lambda p: \
231 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
233 is_logical = lambda p: \
234 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
236 partitions = self.g.part_list(self.guestfs_device)
237 last_partition = partitions[-1]
239 if is_logical(last_partition):
240 # The disk contains extended and logical partitions....
241 extended = filter(is_extended, partitions)[0]
242 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
244 # check if extended is the last primary partition
245 if last_primary['part_num'] > extended['part_num']:
246 last_partition = last_primary
248 return last_partition
253 This is accomplished by shrinking the last file system of the
254 image and then updating the partition table. The new disk size
255 (in bytes) is returned.
257 ATTENTION: make sure unmount is called before shrink
259 get_fstype = lambda p: \
260 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
261 is_logical = lambda p: \
262 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
263 is_extended = lambda p: \
264 self.meta['PARTITION_TABLE'] == 'msdos' and \
265 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
268 part_add = lambda ptype, start, stop: \
269 self.g.part_add(self.guestfs_device, ptype, start, stop)
270 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
271 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
272 part_set_id = lambda p, id: \
273 self.g.part_set_mbr_id(self.guestfs_device, p, id)
274 part_get_bootable = lambda p: \
275 self.g.part_get_bootable(self.guestfs_device, p)
276 part_set_bootable = lambda p, bootable: \
277 self.g.part_set_bootable(self.guestfs_device, p, bootable)
281 self.out.output("Shrinking image (this may take a while) ...", False)
283 sector_size = self.g.blockdev_getss(self.guestfs_device)
288 last_part = self._last_partition()
289 fstype = get_fstype(last_part)
292 self.meta['SWAP'] = "%d:%s" % \
293 (last_part['part_num'],
294 (last_part['part_size'] + MB - 1) // MB)
295 part_del(last_part['part_num'])
297 elif is_extended(last_part):
298 part_del(last_part['part_num'])
301 # Most disk manipulation programs leave 2048 sectors after the last
303 new_size = last_part['part_end'] + 1 + 2048 * sector_size
304 self.size = min(self.size, new_size)
307 if not re.match("ext[234]", fstype):
308 self.out.warn("Don't know how to shrink %s partitions." % fstype)
311 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
312 self.g.e2fsck_f(part_dev)
313 self.g.resize2fs_M(part_dev)
315 out = self.g.tune2fs_l(part_dev)
316 block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
317 block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
319 start = last_part['part_start'] / sector_size
320 end = start + (block_size * block_cnt) / sector_size - 1
322 if is_logical(last_part):
323 partitions = self.g.part_list(self.guestfs_device)
325 logical = [] # logical partitions
326 for partition in partitions:
327 if partition['part_num'] < 4:
330 'num': partition['part_num'],
331 'start': partition['part_start'] / sector_size,
332 'end': partition['part_end'] / sector_size,
333 'id': part_get_id(partition['part_num']),
334 'bootable': part_get_bootable(partition['part_num'])
337 logical[-1]['end'] = end # new end after resize
339 # Recreate the extended partition
340 extended = filter(is_extended, partitions)[0]
341 part_del(extended['part_num'])
342 part_add('e', extended['part_start'] / sector_size, end)
344 # Create all the logical partitions back
346 part_add('l', l['start'], l['end'])
347 part_set_id(l['num'], l['id'])
348 part_set_bootable(l['num'], l['bootable'])
350 # Recreate the last partition
351 if self.meta['PARTITION_TABLE'] == 'msdos':
352 last_part['id'] = part_get_id(last_part['part_num'])
354 last_part['bootable'] = part_get_bootable(last_part['part_num'])
355 part_del(last_part['part_num'])
356 part_add('p', start, end)
357 part_set_bootable(last_part['part_num'], last_part['bootable'])
359 if self.meta['PARTITION_TABLE'] == 'msdos':
360 part_set_id(last_part['part_num'], last_part['id'])
362 new_size = (end + 1) * sector_size
364 assert (new_size <= self.size)
366 if self.meta['PARTITION_TABLE'] == 'gpt':
367 ptable = GPTPartitionTable(self.device)
368 self.size = ptable.shrink(new_size, self.size)
370 self.size = min(new_size + 2048 * sector_size, self.size)
372 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
376 def dump(self, outfile):
377 """Dumps the content of the image into a file.
379 This method will only dump the actual payload, found by reading the
380 partition table. Empty space in the end of the device will be ignored.
383 blocksize = 4 * MB # 4MB
385 progr_size = (size + MB - 1) // MB # in MB
386 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
388 with open(self.device, 'r') as src:
389 with open(outfile, "w") as dst:
394 length = min(left, blocksize)
395 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
397 # Workaround for python-sendfile API change. In
398 # python-sendfile 1.2.x (py-sendfile) the returning value
399 # of sendfile is a tuple, where in version 2.x (pysendfile)
400 # it is just a sigle integer.
401 if isinstance(sent, tuple):
406 progressbar.goto((size - left) // MB)
407 progressbar.success('image file %s was successfully created' % outfile)
409 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :