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'])
296 self.g.e2fsck_f(part_dev)
297 self.g.resize2fs_M(part_dev)
299 out = self.g.tune2fs_l(part_dev)
300 block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
301 block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
303 start = last_part['part_start'] / sector_size
304 end = start + (block_size * block_cnt) / sector_size - 1
306 if is_logical(last_part):
307 partitions = self.g.part_list(self.guestfs_device)
309 logical = [] # logical partitions
310 for partition in partitions:
311 if partition['part_num'] < 4:
314 'num': partition['part_num'],
315 'start': partition['part_start'] / sector_size,
316 'end': partition['part_end'] / sector_size,
317 'id': part_get_id(partition['part_num']),
318 'bootable': part_get_bootable(partition['part_num'])
321 logical[-1]['end'] = end # new end after resize
323 # Recreate the extended partition
324 extended = filter(is_extended, partitions)[0]
325 part_del(extended['part_num'])
326 part_add('e', extended['part_start'] / sector_size, end)
328 # Create all the logical partitions back
330 part_add('l', l['start'], l['end'])
331 part_set_id(l['num'], l['id'])
332 part_set_bootable(l['num'], l['bootable'])
334 # Recreate the last partition
335 if self.meta['PARTITION_TABLE'] == 'msdos':
336 last_part['id'] = part_get_id(last_part['part_num'])
338 last_part['bootable'] = part_get_bootable(last_part['part_num'])
339 part_del(last_part['part_num'])
340 part_add('p', start, end)
341 part_set_bootable(last_part['part_num'], last_part['bootable'])
343 if self.meta['PARTITION_TABLE'] == 'msdos':
344 part_set_id(last_part['part_num'], last_part['id'])
346 new_size = (end + 1) * sector_size
348 assert (new_size <= self.size)
350 if self.meta['PARTITION_TABLE'] == 'gpt':
351 ptable = GPTPartitionTable(self.device)
352 self.size = ptable.shrink(new_size, self.size)
354 self.size = min(new_size + 2048 * sector_size, self.size)
356 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
360 def dump(self, outfile):
361 """Dumps the content of the image into a file.
363 This method will only dump the actual payload, found by reading the
364 partition table. Empty space in the end of the device will be ignored.
367 blocksize = 4 * MB # 4MB
369 progr_size = (size + MB - 1) // MB # in MB
370 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
372 with open(self.device, 'r') as src:
373 with open(outfile, "w") as dst:
378 length = min(left, blocksize)
379 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
381 # Workaround for python-sendfile API change. In
382 # python-sendfile 1.2.x (py-sendfile) the returning value
383 # of sendfile is a tuple, where in version 2.x (pysendfile)
384 # it is just a sigle integer.
385 if isinstance(sent, tuple):
390 progressbar.goto((size - left) // MB)
391 progressbar.success('image file %s was successfully created' % outfile)
393 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :