# or implied, of GRNET S.A.
from image_creator.util import get_command
-from image_creator import FatalError
-from clint.textui import progress
-
+from image_creator.util import warn, progress, success, output, FatalError
+from image_creator.gpt import GPTPartitionTable
import stat
import os
import tempfile
import sys
import guestfs
import time
+from sendfile import sendfile
class DiskError(Exception):
job, args = self._cleanup_jobs.pop()
job(*args)
- def get_device(self):
- """Returns a newly created DiskDevice instance.
-
- This instance is a snapshot of the original source media of
- the Disk instance.
+ def snapshot(self):
+ """Creates a snapshot of the original source media of the Disk
+ instance.
"""
+
+ output("Examining source media `%s'..." % self.source, False)
sourcedev = self.source
mode = os.stat(self.source).st_mode
if stat.S_ISDIR(mode):
+ success('looks like a directory')
return self._losetup(self._dir_to_disk())
elif stat.S_ISREG(mode):
+ success('looks like an image file')
sourcedev = self._losetup(self.source)
elif not stat.S_ISBLK(mode):
- raise ValueError("Value for self.source is invalid")
+ raise ValueError("Invalid media source. Only block devices, "
+ "regular files and directories are supported.")
+ else:
+ success('looks like a block device')
# Take a snapshot and return it to the user
+ output("Snapshotting media source...", False)
size = blockdev('--getsize', sourcedev)
cowfd, cow = tempfile.mkstemp()
os.close(cowfd)
self._add_cleanup(os.unlink, cow)
# Create 1G cow sparse file
- dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
+ dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', \
+ 'seek=%d' % (1024 * 1024))
cowdev = self._losetup(cow)
snapshot = uuid.uuid4().hex
(int(size), sourcedev, cowdev))
dmsetup('create', snapshot, table)
self._add_cleanup(dmsetup, 'remove', snapshot)
+ # Sometimes dmsetup remove fails with Device or resource busy,
+ # although everything is cleaned up and the snapshot is not
+ # used by anyone. Add a 2 seconds delay to be on the safe side.
+ self._add_cleanup(time.sleep, 2)
+
finally:
os.unlink(table)
- new_device = DiskDevice("/dev/mapper/%s" % snapshot)
+ success('done')
+ return "/dev/mapper/%s" % snapshot
+
+ def get_device(self, media):
+ """Returns a newly created DiskDevice instance."""
+
+ new_device = DiskDevice(media)
self._devices.append(new_device)
new_device.enable()
return new_device
device.destroy()
-def progress_generator(label=''):
- position = 0
- for i in progress.bar(range(100), label):
- if i < position:
- continue
- position = yield
- yield # suppress the StopIteration exception
-
-
class DiskDevice(object):
"""This class represents a block device hosting an Operating System
as created by the device-mapper.
def enable(self):
"""Enable a newly created DiskDevice"""
-
- self.progressbar = progress_generator("VM lauch: ")
- self.progressbar.next()
+ self.progressbar = progress("Launching helper VM: ", "percent")
+ self.progressbar.max = 100
+ self.progressbar.goto(1)
eh = self.g.set_event_callback(self.progress_callback,
- guestfs.EVENT_PROGRESS)
+ guestfs.EVENT_PROGRESS)
self.g.launch()
self.guestfs_enabled = True
self.g.delete_event_callback(eh)
if self.progressbar is not None:
- self.progressbar.send(100)
+ output("\rLaunching helper VM...\033[K", False)
+ success("done")
self.progressbar = None
+ output('Inspecting Operating System...', False)
roots = self.g.inspect_os()
if len(roots) == 0:
raise FatalError("No operating system found")
if len(roots) > 1:
- raise FatalError("Multiple operating systems found")
-
+ raise FatalError("Multiple operating systems found."
+ "We only support images with one filesystem.")
self.root = roots[0]
+ self.gdev = self.g.part_to_dev(self.root)
+ self.parttype = self.g.part_get_parttype(self.gdev)
+
self.ostype = self.g.inspect_get_type(self.root)
self.distro = self.g.inspect_get_distro(self.root)
+ success('found a(n) %s system' % self.distro)
def destroy(self):
"""Destroy this DiskDevice instance."""
position = array[2]
total = array[3]
- self.progressbar.send((position * 100) // total)
-
- if position == total:
- self.progressbar = None
+ self.progressbar.goto((position * 100) // total)
def mount(self):
"""Mount all disk partitions in a correct order."""
+
+ output("Mounting image...", False)
mps = self.g.inspect_get_mountpoints(self.root)
# Sort the keys to mount the fs in a correct order.
try:
self.g.mount(dev, mp)
except RuntimeError as msg:
- print "%s (ignored)" % msg
+ warn("%s (ignored)" % msg)
+ success("done")
def umount(self):
"""Umount all mounted filesystems."""
disk and then updating the partition table. The new disk size
(in bytes) is returned.
"""
- dev = self.g.part_to_dev(self.root)
- parttype = self.g.part_get_parttype(dev)
- if parttype != 'msdos':
+ output("Shrinking image (this may take a while)...", False)
+
+ if self.parttype not in 'msdos' 'gpt':
raise FatalError("You have a %s partition table. "
- "Only msdos partitions are supported" % parttype)
+ "Only msdos and gpt partitions are supported" % self.parttype)
- last_partition = self.g.part_list(dev)[-1]
+ last_partition = self.g.part_list(self.gdev)[-1]
if last_partition['part_num'] > 4:
raise FatalError("This disk contains logical partitions. "
"Only primary partitions are supported.")
- part_dev = "%s%d" % (dev, last_partition['part_num'])
+ part_dev = "%s%d" % (self.gdev, last_partition['part_num'])
fs_type = self.g.vfs_type(part_dev)
if not re.match("ext[234]", fs_type):
- print "Warning: Don't know how to resize %s partitions." % vfs_type
- return
+ warn("Don't know how to resize %s partitions." % vfs_type)
+ return self.size()
self.g.e2fsck_f(part_dev)
self.g.resize2fs_M(part_dev)
- output = self.g.tune2fs_l(part_dev)
- block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
- block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
- sector_size = self.g.blockdev_getss(dev)
+ out = self.g.tune2fs_l(part_dev)
+ block_size = int(
+ filter(lambda x: x[0] == 'Block size', out)[0][1])
+ block_cnt = int(
+ filter(lambda x: x[0] == 'Block count', out)[0][1])
+
+ sector_size = self.g.blockdev_getss(self.gdev)
start = last_partition['part_start'] / sector_size
end = start + (block_size * block_cnt) / sector_size - 1
- self.g.part_del(dev, last_partition['part_num'])
- self.g.part_add(dev, 'p', start, end)
+ self.g.part_del(self.gdev, last_partition['part_num'])
+ self.g.part_add(self.gdev, 'p', start, end)
+
+ new_size = (end + 1) * sector_size
+ success("new image size is %dMB" %
+ ((new_size + 2 ** 20 - 1) // 2 ** 20))
- return (end + 1) * sector_size
+ if self.parttype == 'gpt':
+ ptable = GPTPartitionTable(self.device)
+ return ptable.shrink(new_size)
+
+ return new_size
def size(self):
"""Returns the "payload" size of the device.
The size returned by this method is the size of the space occupied by
the partitions (including the space before the first partition).
"""
- dev = self.g.part_to_dev(self.root)
- last = self.g.part_list(dev)[-1]
- return last['part_end']
+ if self.parttype == 'msdos':
+ dev = self.g.part_to_dev(self.root)
+ last = self.g.part_list(dev)[-1]
+ return last['part_end'] + 1
+ elif self.parttype == 'gpt':
+ ptable = GPTPartitionTable(self.device)
+ return ptable.size()
+ else:
+ raise FatalError("Unsupported partition table type: %s" % parttype)
+
+ def dump(self, outfile):
+ """Dumps the content of device into a file.
+
+ This method will only dump the actual payload, found by reading the
+ partition table. Empty space in the end of the device will be ignored.
+ """
+ blocksize = 2 ** 22 # 4MB
+ size = self.size()
+ progress_size = (size + 2 ** 20 - 1) // 2 ** 20 # in MB
+ progressbar = progress("Dumping image file: ", 'mb')
+ progressbar.max = progress_size
+ source = open(self.device, "r")
+ try:
+ dest = open(outfile, "w")
+ try:
+ left = size
+ offset = 0
+ progressbar.next()
+ while left > 0:
+ length = min(left, blocksize)
+ sent = sendfile(dest.fileno(), source.fileno(), offset,
+ length)
+ offset += sent
+ left -= sent
+ progressbar.goto((size - left) // 2 ** 20)
+ finally:
+ dest.close()
+ finally:
+ source.close()
+ output("\rDumping image file...\033[K", False)
+ success('image file %s was successfully created' % outfile)
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :