X-Git-Url: https://code.grnet.gr/git/snf-image-creator/blobdiff_plain/333ff548f55ea9a59d49da0fb6e38b30a364309f..ee0719db894b7d2aaa6e6a7e3f97d477dd8dca4f:/image_creator/disk.py diff --git a/image_creator/disk.py b/image_creator/disk.py index 8bba0fc..b4311f8 100644 --- a/image_creator/disk.py +++ b/image_creator/disk.py @@ -1,115 +1,197 @@ -#!/usr/bin/env python +# Copyright 2012 GRNET S.A. All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and +# documentation are those of the authors and should not be +# interpreted as representing official policies, either expressed +# or implied, of GRNET S.A. + +from image_creator.util import get_command +from image_creator.util import try_fail_repeat +from image_creator.util import free_space +from image_creator.util import FatalError +from image_creator.bundle_volume import BundleVolume +from image_creator.image import Image -import losetup import stat import os import tempfile import uuid -import re -from pbs import dmsetup -from pbs import blockdev -from pbs import dd -from pbs import kpartx -from pbs import mount -from pbs import umount +import shutil + +dd = get_command('dd') +dmsetup = get_command('dmsetup') +losetup = get_command('losetup') +blockdev = get_command('blockdev') + + +TMP_CANDIDATES = ['/var/tmp', os.path.expanduser('~'), '/mnt'] + class Disk(object): + """This class represents a hard disk hosting an Operating System + + A Disk instance never alters the source media it is created from. + Any change is done on a snapshot created by the device-mapper of + the Linux kernel. + """ - def __init__(self, source): + def __init__(self, source, output, tmp=None): + """Create a new Disk instance out of a source media. The source + media can be an image file, a block device or a directory. + """ self._cleanup_jobs = [] - self._devices = [] + self._images = [] self.source = source + self.out = output + self.meta = {} + self.tmp = tempfile.mkdtemp(prefix='.snf_image_creator.', + dir=self._get_tmp_dir(tmp)) + + self._add_cleanup(shutil.rmtree, self.tmp) + + def _get_tmp_dir(self, default=None): + """Check tmp directory candidates and return the one with the most + available space. + """ + if default is not None: + return default + + space = map(free_space, TMP_CANDIDATES) + + max_idx = 0 + max_val = space[0] + for i, val in zip(range(len(space)), space): + if val > max_val: + max_val = val + max_idx = i + + # Return the candidate path with more available space + return TMP_CANDIDATES[max_idx] def _add_cleanup(self, job, *args): + """Add a new job in the cleanup list""" self._cleanup_jobs.append((job, args)) def _losetup(self, fname): - loop = losetup.find_unused_loop_device() - loop.mount(fname) - self._add_cleanup(loop.unmount) - return loop.device + """Setup a loop device and add it to the cleanup list. The loop device + will be detached when cleanup is called. + """ + loop = losetup('-f', '--show', fname) + loop = loop.strip() # remove the new-line char + self._add_cleanup(try_fail_repeat, losetup, '-d', loop) + return loop def _dir_to_disk(self): - raise NotImplementedError + """Create a disk out of a directory""" + if self.source == '/': + bundle = BundleVolume(self.out, self.meta) + image = '%s/%s.diskdump' % (self.tmp, uuid.uuid4().hex) - def cleanup(self): - while len(self._cleanup_jobs): - job, args = self._cleanup_jobs.pop() - job(*args) + def check_unlink(path): + if os.path.exists(path): + os.unlink(path) - def get_device(self): + self._add_cleanup(check_unlink, image) + bundle.create_image(image) + return self._losetup(image) + raise FatalError("Using a directory as media source is supported") + + def cleanup(self): + """Cleanup internal data. This needs to be called before the + program ends. + """ + try: + while len(self._images): + image = self._images.pop() + image.destroy() + finally: + # Make sure those are executed even if one of the device.destroy + # methods throws exeptions. + while len(self._cleanup_jobs): + job, args = self._cleanup_jobs.pop() + job(*args) + + def snapshot(self): + """Creates a snapshot of the original source media of the Disk + instance. + """ + + self.out.output("Examining source media `%s' ..." % self.source, False) sourcedev = self.source mode = os.stat(self.source).st_mode if stat.S_ISDIR(mode): - return self._losetup(self._dir_to_disk()) + self.out.success('looks like a directory') + return self._dir_to_disk() elif stat.S_ISREG(mode): + self.out.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 FatalError("Invalid media source. Only block devices, " + "regular files and directories are supported.") + else: + self.out.success('looks like a block device') # Take a snapshot and return it to the user - size = blockdev('--getsize', sourcedev) - cowfd, cow = tempfile.mkstemp() + self.out.output("Snapshotting media source...", False) + size = blockdev('--getsz', sourcedev) + cowfd, cow = tempfile.mkstemp(dir=self.tmp) + os.close(cowfd) self._add_cleanup(os.unlink, cow) - # Create 1G cow file - dd('if=/dev/null', 'of=%s' % cow, 'bs=1k' ,'seek=%d' % (1024*1024)) + # Create cow sparse file + dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size)) cowdev = self._losetup(cow) snapshot = uuid.uuid4().hex tablefd, table = tempfile.mkstemp() try: - os.write(tablefd, "0 %d snapshot %s %s n 8" % \ - (int(size), sourcedev, cowdev)) + os.write(tablefd, "0 %d snapshot %s %s n 8" % + (int(size), sourcedev, cowdev)) dmsetup('create', snapshot, table) - self._add_cleanup(dmsetup, 'remove', snapshot) + self._add_cleanup(try_fail_repeat, dmsetup, 'remove', snapshot) + finally: os.unlink(table) - - new_device = DiskDevice(self, "/dev/mapper/%s" % snapshot) - self._devices.append(new_device) - return new_device - -class DiskDevice(object): - - def __init__(self, disk, device, bootable = True): - self.disk = disk - self.device = device - self.is_bootable = bootable - self.partitions_mapped = False - self.magic_number = uuid.uuid4().hex - - def list_partitions(self): - if not self.partitions_mapped: - kpartx("-a", "-p", self.magic_number, self.dev) - self.disk._cleanup_jobs.append(kpartx, "-d", "-p", - self.magic_number, self.dev) - self.partitions_mapped = True - - output = kpartx("-l", "-p", self.magic_number, self.dev) - return [ "/dev/mapper/%s" % x for x in - re.findall('^\S+', str(output), flags=re.MULTILINE)] - - def mount(self, partition): - if not self.partitions_mapped: - self.list_partitions() - kpartx("-a", "-p", self.magic_number, self.dev) - self.disk._cleanup_jobs.append(kpartx, "-d", "-p", - self.magic_number, self.dev) - self.partitions_mapped = True - - targetfd, target = tempfile.mkdtemp() - try: - mount(dev, partition) - except: - os.rmdir(table) - raise - return target - - def unmount(self, partition): - umount(target) - - mode = os.stat(self.source).st_mode - if stat.S_ISDIR(mode): - os.rmdir(target) + self.out.success('done') + return "/dev/mapper/%s" % snapshot + + def get_image(self, media): + """Returns a newly created Image instance.""" + + image = Image(media, self.out) + self._images.append(image) + image.enable() + return image + + def destroy_image(self, image): + """Destroys an Image instance previously created by get_image method. + """ + self._images.remove(image) + image.destroy() # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :