1 # -*- coding: utf-8 -*-
3 # Copyright 2012 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 """Module hosting the Disk class."""
38 from image_creator.util import get_command
39 from image_creator.util import try_fail_repeat
40 from image_creator.util import free_space
41 from image_creator.util import FatalError
42 from image_creator.bundle_volume import BundleVolume
43 from image_creator.image import Image
51 dd = get_command('dd')
52 dmsetup = get_command('dmsetup')
53 losetup = get_command('losetup')
54 blockdev = get_command('blockdev')
57 def get_tmp_dir(default=None):
58 """Check tmp directory candidates and return the one with the most
61 if default is not None:
64 TMP_CANDIDATES = ['/var/tmp', os.path.expanduser('~'), '/mnt']
66 space = map(free_space, TMP_CANDIDATES)
70 for i, val in zip(range(len(space)), space):
75 # Return the candidate path with more available space
76 return TMP_CANDIDATES[max_idx]
80 """This class represents a hard disk hosting an Operating System
82 A Disk instance never alters the source media it is created from.
83 Any change is done on a snapshot created by the device-mapper of
87 def __init__(self, source, output, tmp=None):
88 """Create a new Disk instance out of a source media. The source
89 media can be an image file, a block device or a directory.
91 self._cleanup_jobs = []
96 self.tmp = tempfile.mkdtemp(prefix='.snf_image_creator.',
99 self._add_cleanup(shutil.rmtree, self.tmp)
101 def _add_cleanup(self, job, *args):
102 """Add a new job in the cleanup list"""
103 self._cleanup_jobs.append((job, args))
105 def _losetup(self, fname):
106 """Setup a loop device and add it to the cleanup list. The loop device
107 will be detached when cleanup is called.
109 loop = losetup('-f', '--show', fname)
110 loop = loop.strip() # remove the new-line char
111 self._add_cleanup(try_fail_repeat, losetup, '-d', loop)
114 def _dir_to_disk(self):
115 """Create a disk out of a directory"""
116 if self.source == '/':
117 bundle = BundleVolume(self.out, self.meta)
118 image = '%s/%s.diskdump' % (self.tmp, uuid.uuid4().hex)
120 def check_unlink(path):
121 if os.path.exists(path):
124 self._add_cleanup(check_unlink, image)
125 bundle.create_image(image)
126 return self._losetup(image)
127 raise FatalError("Using a directory as media source is supported")
130 """Cleanup internal data. This needs to be called before the
134 while len(self._images):
135 image = self._images.pop()
138 # Make sure those are executed even if one of the device.destroy
139 # methods throws exeptions.
140 while len(self._cleanup_jobs):
141 job, args = self._cleanup_jobs.pop()
145 """Creates a snapshot of the original source media of the Disk
149 self.out.output("Examining source media `%s' ..." % self.source, False)
150 sourcedev = self.source
151 mode = os.stat(self.source).st_mode
152 if stat.S_ISDIR(mode):
153 self.out.success('looks like a directory')
154 return self._dir_to_disk()
155 elif stat.S_ISREG(mode):
156 self.out.success('looks like an image file')
157 sourcedev = self._losetup(self.source)
158 elif not stat.S_ISBLK(mode):
159 raise FatalError("Invalid media source. Only block devices, "
160 "regular files and directories are supported.")
162 self.out.success('looks like a block device')
164 # Take a snapshot and return it to the user
165 self.out.output("Snapshotting media source ...", False)
166 size = blockdev('--getsz', sourcedev)
167 cowfd, cow = tempfile.mkstemp(dir=self.tmp)
169 self._add_cleanup(os.unlink, cow)
170 # Create cow sparse file
171 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
172 cowdev = self._losetup(cow)
174 snapshot = uuid.uuid4().hex
175 tablefd, table = tempfile.mkstemp()
178 os.write(tablefd, "0 %d snapshot %s %s n 8" %
179 (int(size), sourcedev, cowdev))
183 dmsetup('create', snapshot, table)
184 self._add_cleanup(try_fail_repeat, dmsetup, 'remove', snapshot)
187 self.out.success('done')
188 return "/dev/mapper/%s" % snapshot
190 def get_image(self, media, **kargs):
191 """Returns a newly created Image instance."""
193 image = Image(media, self.out, **kargs)
194 self._images.append(image)
198 def destroy_image(self, image):
199 """Destroys an Image instance previously created by get_image method.
201 self._images.remove(image)
204 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :