disk.py 6.94 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 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.
Nikos Skalkotos's avatar
Nikos Skalkotos committed
33

Nikos Skalkotos's avatar
Nikos Skalkotos committed
34
from image_creator.util import get_command
35
from image_creator.util import try_fail_repeat
36
from image_creator.util import free_space
37
from image_creator.util import FatalError
38
from image_creator.bundle_volume import BundleVolume
39
from image_creator.image import Image
40

Nikos Skalkotos's avatar
Nikos Skalkotos committed
41
42
43
44
import stat
import os
import tempfile
import uuid
Nikos Skalkotos's avatar
Nikos Skalkotos committed
45
import shutil
Nikos Skalkotos's avatar
Nikos Skalkotos committed
46

47
48
49
50
dd = get_command('dd')
dmsetup = get_command('dmsetup')
losetup = get_command('losetup')
blockdev = get_command('blockdev')
51
52


53
54
55
TMP_CANDIDATES = ['/var/tmp', os.path.expanduser('~'), '/mnt']


Nikos Skalkotos's avatar
Nikos Skalkotos committed
56
class Disk(object):
57
58
59
60
61
62
    """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.
    """
Nikos Skalkotos's avatar
Nikos Skalkotos committed
63

64
    def __init__(self, source, output, tmp=None):
65
        """Create a new Disk instance out of a source media. The source
66
67
        media can be an image file, a block device or a directory.
        """
Nikos Skalkotos's avatar
Nikos Skalkotos committed
68
        self._cleanup_jobs = []
69
        self._images = []
Nikos Skalkotos's avatar
Nikos Skalkotos committed
70
        self.source = source
71
        self.out = output
72
        self.meta = {}
73
74
75
        self.tmp = tempfile.mkdtemp(prefix='.snf_image_creator.',
                                    dir=self._get_tmp_dir(tmp))

Nikos Skalkotos's avatar
Nikos Skalkotos committed
76
        self._add_cleanup(shutil.rmtree, self.tmp)
77
78

    def _get_tmp_dir(self, default=None):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
79
80
81
        """Check tmp directory candidates and return the one with the most
        available space.
        """
82
83
84
85
86
87
88
89
90
91
92
93
94
95
        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]
Nikos Skalkotos's avatar
Nikos Skalkotos committed
96
97

    def _add_cleanup(self, job, *args):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
98
        """Add a new job in the cleanup list"""
Nikos Skalkotos's avatar
Nikos Skalkotos committed
99
100
101
        self._cleanup_jobs.append((job, args))

    def _losetup(self, fname):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
102
103
104
        """Setup a loop device and add it to the cleanup list. The loop device
        will be detached when cleanup is called.
        """
105
        loop = losetup('-f', '--show', fname)
106
        loop = loop.strip()  # remove the new-line char
107
        self._add_cleanup(try_fail_repeat, losetup, '-d', loop)
108
        return loop
Nikos Skalkotos's avatar
Nikos Skalkotos committed
109
110

    def _dir_to_disk(self):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
111
        """Create a disk out of a directory"""
Nikos Skalkotos's avatar
Nikos Skalkotos committed
112
        if self.source == '/':
113
            bundle = BundleVolume(self.out, self.meta)
114
            image = '%s/%s.diskdump' % (self.tmp, uuid.uuid4().hex)
115
116
117
118
119
120
121

            def check_unlink(path):
                if os.path.exists(path):
                    os.unlink(path)

            self._add_cleanup(check_unlink, image)
            bundle.create_image(image)
122
            return self._losetup(image)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
123
        raise FatalError("Using a directory as media source is supported")
Nikos Skalkotos's avatar
Nikos Skalkotos committed
124
125

    def cleanup(self):
126
127
128
        """Cleanup internal data. This needs to be called before the
        program ends.
        """
129
        try:
130
131
132
            while len(self._images):
                image = self._images.pop()
                image.destroy()
133
134
135
136
137
138
        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)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
139

140
141
142
    def snapshot(self):
        """Creates a snapshot of the original source media of the Disk
        instance.
143
        """
Nikos Skalkotos's avatar
Nikos Skalkotos committed
144

145
        self.out.output("Examining source media `%s' ..." % self.source, False)
146
147
148
        sourcedev = self.source
        mode = os.stat(self.source).st_mode
        if stat.S_ISDIR(mode):
149
            self.out.success('looks like a directory')
150
            return self._dir_to_disk()
151
        elif stat.S_ISREG(mode):
152
            self.out.success('looks like an image file')
153
154
            sourcedev = self._losetup(self.source)
        elif not stat.S_ISBLK(mode):
155
            raise FatalError("Invalid media source. Only block devices, "
Nikos Skalkotos's avatar
Nikos Skalkotos committed
156
                             "regular files and directories are supported.")
157
        else:
158
            self.out.success('looks like a block device')
Nikos Skalkotos's avatar
Nikos Skalkotos committed
159
160

        # Take a snapshot and return it to the user
161
        self.out.output("Snapshotting media source...", False)
162
        size = blockdev('--getsz', sourcedev)
163
        cowfd, cow = tempfile.mkstemp(dir=self.tmp)
164
165
        os.close(cowfd)
        self._add_cleanup(os.unlink, cow)
166
167
        # Create cow sparse file
        dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
168
169
170
171
172
        cowdev = self._losetup(cow)

        snapshot = uuid.uuid4().hex
        tablefd, table = tempfile.mkstemp()
        try:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
173
174
            os.write(tablefd, "0 %d snapshot %s %s n 8" %
                              (int(size), sourcedev, cowdev))
175
            dmsetup('create', snapshot, table)
176
            self._add_cleanup(try_fail_repeat, dmsetup, 'remove', snapshot)
177
178
179

        finally:
            os.unlink(table)
180
        self.out.success('done')
181
182
        return "/dev/mapper/%s" % snapshot

183
184
    def get_image(self, media):
        """Returns a newly created ImageCreator instance."""
185

186
187
188
189
        image = Image(media, self.out)
        self._images.append(image)
        image.enable()
        return image
Nikos Skalkotos's avatar
Nikos Skalkotos committed
190

191
192
193
    def destroy_image(self, image):
        """Destroys an ImageCreator instance previously created by
        get_image_creator method.
194
        """
195
196
        self._images.remove(image)
        image.destroy()
197

Nikos Skalkotos's avatar
Nikos Skalkotos committed
198
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :