-#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# 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.
+
+"""Module hosting the Disk class."""
+
+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
-import sys
-import guestfs
+import shutil
-from pbs import dmsetup
-from pbs import blockdev
-from pbs import dd
+dd = get_command('dd')
+dmsetup = get_command('dmsetup')
+losetup = get_command('losetup')
+blockdev = get_command('blockdev')
+
+
+def get_tmp_dir(default=None):
+ """Check tmp directory candidates and return the one with the most
+ available space.
+ """
+ if default is not None:
+ return default
+
+ TMP_CANDIDATES = ['/var/tmp', os.path.expanduser('~'), '/mnt']
+
+ 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]
-class DiskError(Exception): pass
class Disk(object):
+ """This class represents a hard disk hosting an Operating System
- def __init__(self, source):
+ 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, 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=get_tmp_dir(tmp))
+
+ self._add_cleanup(shutil.rmtree, self.tmp)
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 check_unlink(path):
+ if os.path.exists(path):
+ os.unlink(path)
+
+ 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):
- while len(self._devices):
- device = self._devices.pop()
- device.destroy()
-
- while len(self._cleanup_jobs):
- job, args = self._cleanup_jobs.pop()
- job(*args)
-
- def get_device(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))
+ try:
+ os.write(tablefd, "0 %d snapshot %s %s n 8" %
+ (int(size), sourcedev, cowdev))
+ finally:
+ os.close(tablefd)
+
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("/dev/mapper/%s" % snapshot)
- self._devices.append(new_device)
- return new_device
-
- def destroy_device(self, device):
- self._devices.remove(device)
- device.destroy()
-
-class DiskDevice(object):
-
- def __init__(self, device, bootable = True):
- self.device = device
- self.bootable = bootable
-
- self.g = guestfs.GuestFS()
- self.g.add_drive_opts(device, readonly = 0)
- self.g.launch()
- roots = self.g.inspect_os()
- if len(roots) == 0:
- raise DiskError("No operating system found")
- if len(roots) > 1:
- raise DiskError("Multiple operating systems found")
-
- self.root = roots[0]
-
- def destroy(self):
- self.g.umount_all()
- self.g.sync()
- # Close the guestfs handler
- del self.g
-
- def get_image_metadata(self):
- meta = {}
- meta["OSFAMILY"] = self.g.inspect_get_type(self.root)
- meta["OS"] = self.g.inspect_get_distro(self.root)
- meta["description"] = self.g.inspect_get_product_name(self.root)
- return meta
-
- def mount(self):
- mps = g.inspect_get_mountpoints(self.root)
- # Sort the keys to mount the fs in a correct order.
- # / should be mounted befor /boot, etc
- def compare (a, b):
- if len(a[0]) > len(b[0]): return 1
- elif len(a[0]) == len(b[0]): return 0
- else: return -1
- mps.sort(compare)
- for mp, dev in mps:
- try:
- self.g.mount(dev, mp)
- except RuntimeError as msg:
- print "%s (ignored)" % msg
+ self.out.success('done')
+ return "/dev/mapper/%s" % snapshot
+
+ def get_image(self, media, **kargs):
+ """Returns a newly created Image instance."""
+
+ image = Image(media, self.out, **kargs)
+ 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 :