1 # Copyright 2012 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 get_command
35 from image_creator.util import FatalError
36 from image_creator.gpt import GPTPartitionTable
45 from sendfile import sendfile
48 dd = get_command('dd')
49 dmsetup = get_command('dmsetup')
50 losetup = get_command('losetup')
51 blockdev = get_command('blockdev')
55 """This class represents a hard disk hosting an Operating System
57 A Disk instance never alters the source media it is created from.
58 Any change is done on a snapshot created by the device-mapper of
62 def __init__(self, source, output):
63 """Create a new Disk instance out of a source media. The source
64 media can be an image file, a block device or a directory."""
65 self._cleanup_jobs = []
70 def _add_cleanup(self, job, *args):
71 self._cleanup_jobs.append((job, args))
73 def _losetup(self, fname):
74 loop = losetup('-f', '--show', fname)
75 loop = loop.strip() # remove the new-line char
76 self._add_cleanup(losetup, '-d', loop)
79 def _dir_to_disk(self):
80 raise FatalError("Using a directory as media source is not supported "
84 """Cleanup internal data. This needs to be called before the
87 while len(self._devices):
88 device = self._devices.pop()
91 while len(self._cleanup_jobs):
92 job, args = self._cleanup_jobs.pop()
96 """Creates a snapshot of the original source media of the Disk
100 self.out.output("Examining source media `%s'..." % self.source, False)
101 sourcedev = self.source
102 mode = os.stat(self.source).st_mode
103 if stat.S_ISDIR(mode):
104 self.out.success('looks like a directory')
105 return self._losetup(self._dir_to_disk())
106 elif stat.S_ISREG(mode):
107 self.out.success('looks like an image file')
108 sourcedev = self._losetup(self.source)
109 elif not stat.S_ISBLK(mode):
110 raise ValueError("Invalid media source. Only block devices, "
111 "regular files and directories are supported.")
113 self.out.success('looks like a block device')
115 # Take a snapshot and return it to the user
116 self.out.output("Snapshotting media source...", False)
117 size = blockdev('--getsize', sourcedev)
118 cowfd, cow = tempfile.mkstemp()
120 self._add_cleanup(os.unlink, cow)
121 # Create 1G cow sparse file
122 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
123 cowdev = self._losetup(cow)
125 snapshot = uuid.uuid4().hex
126 tablefd, table = tempfile.mkstemp()
128 os.write(tablefd, "0 %d snapshot %s %s n 8" %
129 (int(size), sourcedev, cowdev))
130 dmsetup('create', snapshot, table)
131 self._add_cleanup(dmsetup, 'remove', snapshot)
132 # Sometimes dmsetup remove fails with Device or resource busy,
133 # although everything is cleaned up and the snapshot is not
134 # used by anyone. Add a 2 seconds delay to be on the safe side.
135 self._add_cleanup(time.sleep, 2)
139 self.out.success('done')
140 return "/dev/mapper/%s" % snapshot
142 def get_device(self, media):
143 """Returns a newly created DiskDevice instance."""
145 new_device = DiskDevice(media, self.out)
146 self._devices.append(new_device)
150 def destroy_device(self, device):
151 """Destroys a DiskDevice instance previously created by
154 self._devices.remove(device)
158 class DiskDevice(object):
159 """This class represents a block device hosting an Operating System
160 as created by the device-mapper.
163 def __init__(self, device, output, bootable=True):
164 """Create a new DiskDevice."""
166 self.real_device = device
168 self.bootable = bootable
169 self.progress_bar = None
170 self.guestfs_device = None
173 self.g = guestfs.GuestFS()
174 self.g.add_drive_opts(self.real_device, readonly=0)
176 # Before version 1.17.14 the recovery process, which is a fork of the
177 # original process that called libguestfs, did not close its inherited
178 # file descriptors. This can cause problems especially if the parent
179 # process has opened pipes. Since the recovery process is an optional
180 # feature of libguestfs, it's better to disable it.
181 self.g.set_recovery_proc(0)
182 version = self.g.version()
183 if version['major'] > 1 or \
184 (version['major'] == 1 and (version['minor'] >= 18 or
185 (version['minor'] == 17 and
186 version['release'] >= 14))):
187 self.g.set_recovery_proc(1)
188 self.out.output("Enabling recovery proc")
191 #self.g.set_verbose(1)
193 self.guestfs_enabled = False
196 """Enable a newly created DiskDevice"""
197 self.progressbar = self.out.Progress(100, "Launching helper VM",
199 eh = self.g.set_event_callback(self.progress_callback,
200 guestfs.EVENT_PROGRESS)
202 self.guestfs_enabled = True
203 self.g.delete_event_callback(eh)
204 self.progressbar.success('done')
205 self.progressbar = None
207 self.out.output('Inspecting Operating System...', False)
208 roots = self.g.inspect_os()
210 raise FatalError("No operating system found")
212 raise FatalError("Multiple operating systems found."
213 "We only support images with one OS.")
215 self.guestfs_device = self.g.part_to_dev(self.root)
216 self.meta['SIZE'] = self.g.blockdev_getsize64(self.guestfs_device)
217 self.meta['PARTITION_TABLE'] = \
218 self.g.part_get_parttype(self.guestfs_device)
220 self.ostype = self.g.inspect_get_type(self.root)
221 self.distro = self.g.inspect_get_distro(self.root)
222 self.out.success('found a(n) %s system' % self.distro)
225 """Destroy this DiskDevice instance."""
227 if self.guestfs_enabled:
231 # Close the guestfs handler if open
234 def progress_callback(self, ev, eh, buf, array):
238 self.progressbar.goto((position * 100) // total)
240 def mount(self, readonly=False):
241 """Mount all disk partitions in a correct order."""
243 mount = self.g.mount_ro if readonly else self.g.mount
244 self.out.output("Mounting image...", False)
245 mps = self.g.inspect_get_mountpoints(self.root)
247 # Sort the keys to mount the fs in a correct order.
248 # / should be mounted befor /boot, etc
250 if len(a[0]) > len(b[0]):
252 elif len(a[0]) == len(b[0]):
260 except RuntimeError as msg:
261 self.out.warn("%s (ignored)" % msg)
262 self.out.success("done")
265 """Umount all mounted filesystems."""
268 def _last_partition(self):
269 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
270 msg = "Unsupported partition table: %s. Only msdos and gpt " \
271 "partition tables are supported" % self.meta['PARTITION_TABLE']
272 raise FatalError(msg)
274 is_extended = lambda p: \
275 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
276 is_logical = lambda p: \
277 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
279 partitions = self.g.part_list(self.guestfs_device)
280 last_partition = partitions[-1]
282 if is_logical(last_partition):
283 # The disk contains extended and logical partitions....
284 extended = [p for p in partitions if is_extended(p)][0]
285 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
287 # check if extended is the last primary partition
288 if last_primary['part_num'] > extended['part_num']:
289 last_partition = last_primary
291 return last_partition
296 This is accomplished by shrinking the last filesystem in the
297 disk and then updating the partition table. The new disk size
298 (in bytes) is returned.
300 ATTENTION: make sure unmount is called before shrink
302 get_fstype = lambda p: \
303 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
304 is_logical = lambda p: \
305 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
306 is_extended = lambda p: \
307 self.meta['PARTITION_TABLE'] == 'msdos' and \
308 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
310 part_add = lambda ptype, start, stop: \
311 self.g.part_add(self.guestfs_device, ptype, start, stop)
312 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
313 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
314 part_set_id = lambda p, id: \
315 self.g.part_set_mbr_id(self.guestfs_device, p, id)
316 part_get_bootable = lambda p: \
317 self.g.part_get_bootable(self.guestfs_device, p)
318 part_set_bootable = lambda p, bootable: \
319 self.g.part_set_bootable(self.guestfs_device, p, bootable)
323 self.out.output("Shrinking image (this may take a while)...", False)
325 sector_size = self.g.blockdev_getss(self.guestfs_device)
330 last_part = self._last_partition()
331 fstype = get_fstype(last_part)
334 self.meta['SWAP'] = "%d:%s" % \
335 (last_part['part_num'],
336 (last_part['part_size'] + MB - 1) // MB)
337 part_del(last_part['part_num'])
339 elif is_extended(last_part):
340 part_del(last_part['part_num'])
343 # Most disk manipulation programs leave 2048 sectors after the last
345 new_size = last_part['part_end'] + 1 + 2048 * sector_size
346 self.meta['SIZE'] = min(self.meta['SIZE'], new_size)
349 if not re.match("ext[234]", fstype):
350 self.out.warn("Don't know how to resize %s partitions." % fstype)
351 return self.meta['SIZE']
353 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
354 self.g.e2fsck_f(part_dev)
355 self.g.resize2fs_M(part_dev)
357 out = self.g.tune2fs_l(part_dev)
359 filter(lambda x: x[0] == 'Block size', out)[0][1])
361 filter(lambda x: x[0] == 'Block count', out)[0][1])
363 start = last_part['part_start'] / sector_size
364 end = start + (block_size * block_cnt) / sector_size - 1
366 if is_logical(last_part):
367 partitions = self.g.part_list(self.guestfs_device)
369 logical = [] # logical partitions
370 for partition in partitions:
371 if partition['part_num'] < 4:
374 'num': partition['part_num'],
375 'start': partition['part_start'] / sector_size,
376 'end': partition['part_end'] / sector_size,
377 'id': part_get_(partition['part_num']),
378 'bootable': part_get_bootable(partition['part_num'])
381 logical[-1]['end'] = end # new end after resize
383 # Recreate the extended partition
384 extended = [p for p in partitions if self._is_extended(p)][0]
385 part_del(extended['part_num'])
386 part_add('e', extended['part_start'], end)
388 # Create all the logical partitions back
390 part_add('l', l['start'], l['end'])
391 part_set_id(l['num'], l['id'])
392 part_set_bootable(l['num'], l['bootable'])
394 # Recreate the last partition
395 if self.meta['PARTITION_TABLE'] == 'msdos':
396 last_part['id'] = part_get_id(last_part['part_num'])
398 last_part['bootable'] = part_get_bootable(last_part['part_num'])
399 part_del(last_part['part_num'])
400 part_add('p', start, end)
401 part_set_bootable(last_part['part_num'], last_part['bootable'])
403 if self.meta['PARTITION_TABLE'] == 'msdos':
404 part_set_id(last_part['part_num'], last_part['id'])
406 new_size = (end + 1) * sector_size
408 assert (new_size <= self.meta['SIZE'])
410 if self.meta['PARTITION_TABLE'] == 'gpt':
411 ptable = GPTPartitionTable(self.real_device)
412 self.meta['SIZE'] = ptable.shrink(new_size, self.meta['SIZE'])
414 self.meta['SIZE'] = min(new_size + 2048 * sector_size,
417 self.out.success("new size is %dMB" %
418 ((self.meta['SIZE'] + MB - 1) // MB))
420 return self.meta['SIZE']
422 def dump(self, outfile):
423 """Dumps the content of device into a file.
425 This method will only dump the actual payload, found by reading the
426 partition table. Empty space in the end of the device will be ignored.
429 blocksize = 4 * MB # 4MB
430 size = self.meta['SIZE']
431 progr_size = (size + MB - 1) // MB # in MB
432 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
434 with open(self.real_device, 'r') as src:
435 with open(outfile, "w") as dst:
440 length = min(left, blocksize)
441 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
444 progressbar.goto((size - left) // MB)
445 progressbar.success('image file %s was successfully created' % outfile)
447 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :