1 # -*- coding: utf-8 -*-
3 # Copyright 2013 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
13 # 2. Redistributions in binary form must reproduce the above
14 # copyright notice, this list of conditions and the following
15 # disclaimer in the documentation and/or other materials
16 # provided with the distribution.
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
36 from image_creator.util import FatalError
37 from image_creator.gpt import GPTPartitionTable
38 from image_creator.os_type import os_cls
42 from sendfile import sendfile
46 """The instances of this class can create images out of block devices."""
48 def __init__(self, device, output, **kargs):
49 """Create a new Image instance"""
54 self.meta = kargs['meta'] if 'meta' in kargs else {}
55 self.sysprep_params = \
56 kargs['sysprep_params'] if 'sysprep_params' in kargs else {}
58 self.progress_bar = None
59 self.guestfs_device = None
62 self.g = guestfs.GuestFS()
63 self.guestfs_enabled = False
64 self.guestfs_version = self.g.version()
66 def check_guestfs_version(self, major, minor, release):
67 """Checks if the version of the used libguestfs is smaller, equal or
68 greater than the one specified by the major, minor and release triplet
71 < 0 if the installed version is smaller than the specified one
73 > 0 if the installed one is greater than the specified one
76 for (a, b) in (self.guestfs_version['major'], major), \
77 (self.guestfs_version['minor'], minor), \
78 (self.guestfs_version['release'], release):
85 """Enable a newly created Image instance"""
89 self.out.output('Inspecting Operating System ...', False)
90 roots = self.g.inspect_os()
92 raise FatalError("No operating system found")
94 raise FatalError("Multiple operating systems found."
95 "We only support images with one OS.")
97 self.guestfs_device = self.g.part_to_dev(self.root)
98 self.size = self.g.blockdev_getsize64(self.guestfs_device)
99 self.meta['PARTITION_TABLE'] = \
100 self.g.part_get_parttype(self.guestfs_device)
102 self.ostype = self.g.inspect_get_type(self.root)
103 self.distro = self.g.inspect_get_distro(self.root)
105 'found a(n) %s system' %
106 self.ostype if self.distro == "unknown" else self.distro)
108 def enable_guestfs(self):
109 """Enable the guestfs handler"""
111 if self.guestfs_enabled:
112 self.out.warn("Guestfs is already enabled")
115 # Before version 1.18.4 the behaviour of kill_subprocess was different
116 # and you need to reset the guestfs handler to relaunch a previously
117 # shut down qemu backend
118 if self.check_guestfs_version(1, 18, 4) < 0:
119 self.g = guestfs.GuestFS()
121 self.g.add_drive_opts(self.device, readonly=0, format="raw")
123 # Before version 1.17.14 the recovery process, which is a fork of the
124 # original process that called libguestfs, did not close its inherited
125 # file descriptors. This can cause problems especially if the parent
126 # process has opened pipes. Since the recovery process is an optional
127 # feature of libguestfs, it's better to disable it.
128 if self.check_guestfs_version(1, 17, 14) >= 0:
129 self.out.output("Enabling recovery proc")
130 self.g.set_recovery_proc(1)
132 self.g.set_recovery_proc(0)
135 #self.g.set_verbose(1)
137 self.out.output('Launching helper VM (may take a while) ...', False)
138 # self.progressbar = self.out.Progress(100, "Launching helper VM",
140 # eh = self.g.set_event_callback(self.progress_callback,
141 # guestfs.EVENT_PROGRESS)
143 self.guestfs_enabled = True
144 # self.g.delete_event_callback(eh)
145 # self.progressbar.success('done')
146 # self.progressbar = None
148 if self.check_guestfs_version(1, 18, 4) < 0:
149 self.g.inspect_os() # some calls need this
151 self.out.success('done')
153 def disable_guestfs(self):
154 """Disable the guestfs handler"""
156 if not self.guestfs_enabled:
157 self.out.warn("Guestfs is already disabled")
160 self.out.output("Shutting down helper VM ...", False)
162 # guestfs_shutdown which is the prefered way to shutdown the backend
163 # process was introduced in version 1.19.16
164 if self.check_guestfs_version(1, 19, 16) >= 0:
167 self.g.kill_subprocess()
169 self.guestfs_enabled = False
170 self.out.success('done')
173 """Return an OS class instance for this image"""
174 if hasattr(self, "_os"):
177 if not self.guestfs_enabled:
180 cls = os_cls(self.distro, self.ostype)
181 self._os = cls(self, sysprep_params=self.sysprep_params)
183 self._os.collect_metadata()
187 os = property(_get_os)
190 """Destroy this Image instance."""
192 # In new guestfs versions, there is a handy shutdown method for this
194 if self.guestfs_enabled:
198 # Close the guestfs handler if open
201 # def progress_callback(self, ev, eh, buf, array):
202 # position = array[2]
205 # self.progressbar.goto((position * 100) // total)
207 def _last_partition(self):
208 """Return the last partition of the image disk"""
209 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
210 msg = "Unsupported partition table: %s. Only msdos and gpt " \
211 "partition tables are supported" % self.meta['PARTITION_TABLE']
212 raise FatalError(msg)
214 is_extended = lambda p: \
215 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
217 is_logical = lambda p: \
218 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
220 partitions = self.g.part_list(self.guestfs_device)
221 last_partition = partitions[-1]
223 if is_logical(last_partition):
224 # The disk contains extended and logical partitions....
225 extended = filter(is_extended, partitions)[0]
226 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
228 # check if extended is the last primary partition
229 if last_primary['part_num'] > extended['part_num']:
230 last_partition = last_primary
232 return last_partition
237 This is accomplished by shrinking the last file system of the
238 image and then updating the partition table. The new disk size
239 (in bytes) is returned.
241 ATTENTION: make sure unmount is called before shrink
243 get_fstype = lambda p: \
244 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
245 is_logical = lambda p: \
246 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
247 is_extended = lambda p: \
248 self.meta['PARTITION_TABLE'] == 'msdos' and \
249 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
252 part_add = lambda ptype, start, stop: \
253 self.g.part_add(self.guestfs_device, ptype, start, stop)
254 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
255 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
256 part_set_id = lambda p, id: \
257 self.g.part_set_mbr_id(self.guestfs_device, p, id)
258 part_get_bootable = lambda p: \
259 self.g.part_get_bootable(self.guestfs_device, p)
260 part_set_bootable = lambda p, bootable: \
261 self.g.part_set_bootable(self.guestfs_device, p, bootable)
265 self.out.output("Shrinking image (this may take a while) ...", False)
267 sector_size = self.g.blockdev_getss(self.guestfs_device)
272 last_part = self._last_partition()
273 fstype = get_fstype(last_part)
276 self.meta['SWAP'] = "%d:%s" % \
277 (last_part['part_num'],
278 (last_part['part_size'] + MB - 1) // MB)
279 part_del(last_part['part_num'])
281 elif is_extended(last_part):
282 part_del(last_part['part_num'])
285 # Most disk manipulation programs leave 2048 sectors after the last
287 new_size = last_part['part_end'] + 1 + 2048 * sector_size
288 self.size = min(self.size, new_size)
291 if not re.match("ext[234]", fstype):
292 self.out.warn("Don't know how to shrink %s partitions." % fstype)
295 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
297 if self.check_guestfs_version(1, 15, 17) >= 0:
298 self.g.e2fsck(part_dev, forceall=1)
300 self.g.e2fsck_f(part_dev)
302 self.g.resize2fs_M(part_dev)
304 out = self.g.tune2fs_l(part_dev)
305 block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
306 block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
308 start = last_part['part_start'] / sector_size
309 end = start + (block_size * block_cnt) / sector_size - 1
311 if is_logical(last_part):
312 partitions = self.g.part_list(self.guestfs_device)
314 logical = [] # logical partitions
315 for partition in partitions:
316 if partition['part_num'] < 4:
319 'num': partition['part_num'],
320 'start': partition['part_start'] / sector_size,
321 'end': partition['part_end'] / sector_size,
322 'id': part_get_id(partition['part_num']),
323 'bootable': part_get_bootable(partition['part_num'])
326 logical[-1]['end'] = end # new end after resize
328 # Recreate the extended partition
329 extended = filter(is_extended, partitions)[0]
330 part_del(extended['part_num'])
331 part_add('e', extended['part_start'] / sector_size, end)
333 # Create all the logical partitions back
335 part_add('l', l['start'], l['end'])
336 part_set_id(l['num'], l['id'])
337 part_set_bootable(l['num'], l['bootable'])
339 # Recreate the last partition
340 if self.meta['PARTITION_TABLE'] == 'msdos':
341 last_part['id'] = part_get_id(last_part['part_num'])
343 last_part['bootable'] = part_get_bootable(last_part['part_num'])
344 part_del(last_part['part_num'])
345 part_add('p', start, end)
346 part_set_bootable(last_part['part_num'], last_part['bootable'])
348 if self.meta['PARTITION_TABLE'] == 'msdos':
349 part_set_id(last_part['part_num'], last_part['id'])
351 new_size = (end + 1) * sector_size
353 assert (new_size <= self.size)
355 if self.meta['PARTITION_TABLE'] == 'gpt':
356 ptable = GPTPartitionTable(self.device)
357 self.size = ptable.shrink(new_size, self.size)
359 self.size = min(new_size + 2048 * sector_size, self.size)
361 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
365 def dump(self, outfile):
366 """Dumps the content of the image into a file.
368 This method will only dump the actual payload, found by reading the
369 partition table. Empty space in the end of the device will be ignored.
372 blocksize = 4 * MB # 4MB
374 progr_size = (size + MB - 1) // MB # in MB
375 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
377 with open(self.device, 'r') as src:
378 with open(outfile, "w") as dst:
383 length = min(left, blocksize)
384 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
386 # Workaround for python-sendfile API change. In
387 # python-sendfile 1.2.x (py-sendfile) the returning value
388 # of sendfile is a tuple, where in version 2.x (pysendfile)
389 # it is just a sigle integer.
390 if isinstance(sent, tuple):
395 progressbar.goto((size - left) // MB)
396 progressbar.success('image file %s was successfully created' % outfile)
398 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :