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, check_guestfs_version
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, bootable=True, meta={}):
49 """Create a new Image instance"""
53 self.bootable = bootable
55 self.progress_bar = None
56 self.guestfs_device = None
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 if check_guestfs_version(self.g, 1, 17, 14) >= 0:
68 self.out.output("Enabling recovery proc")
69 self.g.set_recovery_proc(1)
71 self.g.set_recovery_proc(0)
74 #self.g.set_verbose(1)
76 self.guestfs_enabled = False
79 """Enable a newly created Image instance"""
81 self.out.output('Launching helper VM (may take a while) ...', False)
82 # self.progressbar = self.out.Progress(100, "Launching helper VM",
84 # eh = self.g.set_event_callback(self.progress_callback,
85 # guestfs.EVENT_PROGRESS)
87 self.guestfs_enabled = True
88 # self.g.delete_event_callback(eh)
89 # self.progressbar.success('done')
90 # self.progressbar = None
91 self.out.success('done')
93 self.out.output('Inspecting Operating System ...', False)
94 roots = self.g.inspect_os()
96 raise FatalError("No operating system found")
98 raise FatalError("Multiple operating systems found."
99 "We only support images with one OS.")
101 self.guestfs_device = self.g.part_to_dev(self.root)
102 self.size = self.g.blockdev_getsize64(self.guestfs_device)
103 self.meta['PARTITION_TABLE'] = \
104 self.g.part_get_parttype(self.guestfs_device)
106 self.ostype = self.g.inspect_get_type(self.root)
107 self.distro = self.g.inspect_get_distro(self.root)
109 'found a(n) %s system' %
110 self.ostype if self.distro == "unknown" else self.distro)
113 """Return an OS class instance for this image"""
114 if hasattr(self, "_os"):
117 if not self.guestfs_enabled:
120 cls = os_cls(self.distro, self.ostype)
121 self._os = cls(self.root, self.g, self.out)
123 self._os.collect_metadata()
127 os = property(_get_os)
130 """Destroy this Image instance."""
132 # In new guestfs versions, there is a handy shutdown method for this
134 if self.guestfs_enabled:
138 # Close the guestfs handler if open
141 # def progress_callback(self, ev, eh, buf, array):
142 # position = array[2]
145 # self.progressbar.goto((position * 100) // total)
147 def _last_partition(self):
148 """Return the last partition of the image disk"""
149 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
150 msg = "Unsupported partition table: %s. Only msdos and gpt " \
151 "partition tables are supported" % self.meta['PARTITION_TABLE']
152 raise FatalError(msg)
154 is_extended = lambda p: \
155 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
157 is_logical = lambda p: \
158 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
160 partitions = self.g.part_list(self.guestfs_device)
161 last_partition = partitions[-1]
163 if is_logical(last_partition):
164 # The disk contains extended and logical partitions....
165 extended = filter(is_extended, partitions)[0]
166 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
168 # check if extended is the last primary partition
169 if last_primary['part_num'] > extended['part_num']:
170 last_partition = last_primary
172 return last_partition
177 This is accomplished by shrinking the last file system of the
178 image and then updating the partition table. The new disk size
179 (in bytes) is returned.
181 ATTENTION: make sure unmount is called before shrink
183 get_fstype = lambda p: \
184 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
185 is_logical = lambda p: \
186 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
187 is_extended = lambda p: \
188 self.meta['PARTITION_TABLE'] == 'msdos' and \
189 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
192 part_add = lambda ptype, start, stop: \
193 self.g.part_add(self.guestfs_device, ptype, start, stop)
194 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
195 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
196 part_set_id = lambda p, id: \
197 self.g.part_set_mbr_id(self.guestfs_device, p, id)
198 part_get_bootable = lambda p: \
199 self.g.part_get_bootable(self.guestfs_device, p)
200 part_set_bootable = lambda p, bootable: \
201 self.g.part_set_bootable(self.guestfs_device, p, bootable)
205 self.out.output("Shrinking image (this may take a while) ...", False)
207 sector_size = self.g.blockdev_getss(self.guestfs_device)
212 last_part = self._last_partition()
213 fstype = get_fstype(last_part)
216 self.meta['SWAP'] = "%d:%s" % \
217 (last_part['part_num'],
218 (last_part['part_size'] + MB - 1) // MB)
219 part_del(last_part['part_num'])
221 elif is_extended(last_part):
222 part_del(last_part['part_num'])
225 # Most disk manipulation programs leave 2048 sectors after the last
227 new_size = last_part['part_end'] + 1 + 2048 * sector_size
228 self.size = min(self.size, new_size)
231 if not re.match("ext[234]", fstype):
232 self.out.warn("Don't know how to shrink %s partitions." % fstype)
235 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
236 self.g.e2fsck_f(part_dev)
237 self.g.resize2fs_M(part_dev)
239 out = self.g.tune2fs_l(part_dev)
240 block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
241 block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
243 start = last_part['part_start'] / sector_size
244 end = start + (block_size * block_cnt) / sector_size - 1
246 if is_logical(last_part):
247 partitions = self.g.part_list(self.guestfs_device)
249 logical = [] # logical partitions
250 for partition in partitions:
251 if partition['part_num'] < 4:
254 'num': partition['part_num'],
255 'start': partition['part_start'] / sector_size,
256 'end': partition['part_end'] / sector_size,
257 'id': part_get_id(partition['part_num']),
258 'bootable': part_get_bootable(partition['part_num'])
261 logical[-1]['end'] = end # new end after resize
263 # Recreate the extended partition
264 extended = filter(is_extended, partitions)[0]
265 part_del(extended['part_num'])
266 part_add('e', extended['part_start'] / sector_size, end)
268 # Create all the logical partitions back
270 part_add('l', l['start'], l['end'])
271 part_set_id(l['num'], l['id'])
272 part_set_bootable(l['num'], l['bootable'])
274 # Recreate the last partition
275 if self.meta['PARTITION_TABLE'] == 'msdos':
276 last_part['id'] = part_get_id(last_part['part_num'])
278 last_part['bootable'] = part_get_bootable(last_part['part_num'])
279 part_del(last_part['part_num'])
280 part_add('p', start, end)
281 part_set_bootable(last_part['part_num'], last_part['bootable'])
283 if self.meta['PARTITION_TABLE'] == 'msdos':
284 part_set_id(last_part['part_num'], last_part['id'])
286 new_size = (end + 1) * sector_size
288 assert (new_size <= self.size)
290 if self.meta['PARTITION_TABLE'] == 'gpt':
291 ptable = GPTPartitionTable(self.device)
292 self.size = ptable.shrink(new_size, self.size)
294 self.size = min(new_size + 2048 * sector_size, self.size)
296 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
300 def dump(self, outfile):
301 """Dumps the content of the image into a file.
303 This method will only dump the actual payload, found by reading the
304 partition table. Empty space in the end of the device will be ignored.
307 blocksize = 4 * MB # 4MB
309 progr_size = (size + MB - 1) // MB # in MB
310 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
312 with open(self.device, 'r') as src:
313 with open(outfile, "w") as dst:
318 length = min(left, blocksize)
319 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
321 # Workaround for python-sendfile API change. In
322 # python-sendfile 1.2.x (py-sendfile) the returning value
323 # of sendfile is a tuple, where in version 2.x (pysendfile)
324 # it is just a sigle integer.
325 if isinstance(sent, tuple):
330 progressbar.goto((size - left) // MB)
331 progressbar.success('image file %s was successfully created' % outfile)
333 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :