Statistics
| Branch: | Tag: | Revision:

root / image_creator / disk.py @ 979096dd

History | View | Annotate | Download (11.5 kB)

1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from image_creator.util import get_command
35
from image_creator.util import warn, progress, success, output
36
from image_creator import FatalError
37

    
38
import stat
39
import os
40
import tempfile
41
import uuid
42
import re
43
import sys
44
import guestfs
45
import time
46
from sendfile import sendfile
47

    
48

    
49
class DiskError(Exception):
50
    pass
51

    
52
dd = get_command('dd')
53
dmsetup = get_command('dmsetup')
54
losetup = get_command('losetup')
55
blockdev = get_command('blockdev')
56

    
57

    
58
class Disk(object):
59
    """This class represents a hard disk hosting an Operating System
60

61
    A Disk instance never alters the source media it is created from.
62
    Any change is done on a snapshot created by the device-mapper of
63
    the Linux kernel.
64
    """
65

    
66
    def __init__(self, source):
67
        """Create a new Disk instance out of a source media. The source
68
        media can be an image file, a block device or a directory."""
69
        self._cleanup_jobs = []
70
        self._devices = []
71
        self.source = source
72

    
73
    def _add_cleanup(self, job, *args):
74
        self._cleanup_jobs.append((job, args))
75

    
76
    def _losetup(self, fname):
77
        loop = losetup('-f', '--show', fname)
78
        loop = loop.strip()  # remove the new-line char
79
        self._add_cleanup(losetup, '-d', loop)
80
        return loop
81

    
82
    def _dir_to_disk(self):
83
        raise NotImplementedError
84

    
85
    def cleanup(self):
86
        """Cleanup internal data. This needs to be called before the
87
        program ends.
88
        """
89
        while len(self._devices):
90
            device = self._devices.pop()
91
            device.destroy()
92

    
93
        while len(self._cleanup_jobs):
94
            job, args = self._cleanup_jobs.pop()
95
            job(*args)
96

    
97
    def get_device(self):
98
        """Returns a newly created DiskDevice instance.
99

100
        This instance is a snapshot of the original source media of
101
        the Disk instance.
102
        """
103

    
104
        output("Examining source media `%s'..." % self.source, False)
105
        sourcedev = self.source
106
        mode = os.stat(self.source).st_mode
107
        if stat.S_ISDIR(mode):
108
            success('looks like a directory')
109
            return self._losetup(self._dir_to_disk())
110
        elif stat.S_ISREG(mode):
111
            success('looks like an image file')
112
            sourcedev = self._losetup(self.source)
113
        elif not stat.S_ISBLK(mode):
114
            raise ValueError("Invalid media source. Only block devices, "
115
                            "regular files and directories are supported.")
116
        else:
117
            success('looks like a block device')
118

    
119
        # Take a snapshot and return it to the user
120
        output("Snapshotting media source...", False)
121
        size = blockdev('--getsize', sourcedev)
122
        cowfd, cow = tempfile.mkstemp()
123
        os.close(cowfd)
124
        self._add_cleanup(os.unlink, cow)
125
        # Create 1G cow sparse file
126
        dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', \
127
                                        'seek=%d' % (1024 * 1024))
128
        cowdev = self._losetup(cow)
129

    
130
        snapshot = uuid.uuid4().hex
131
        tablefd, table = tempfile.mkstemp()
132
        try:
133
            os.write(tablefd, "0 %d snapshot %s %s n 8" % \
134
                                        (int(size), sourcedev, cowdev))
135
            dmsetup('create', snapshot, table)
136
            self._add_cleanup(dmsetup, 'remove', snapshot)
137
            # Sometimes dmsetup remove fails with Device or resource busy,
138
            # although everything is cleaned up and the snapshot is not
139
            # used by anyone. Add a 2 seconds delay to be on the safe side.
140
            self._add_cleanup(time.sleep, 2)
141

    
142
        finally:
143
            os.unlink(table)
144
        success('done')
145
        new_device = DiskDevice("/dev/mapper/%s" % snapshot)
146
        self._devices.append(new_device)
147
        new_device.enable()
148
        return new_device
149

    
150
    def destroy_device(self, device):
151
        """Destroys a DiskDevice instance previously created by
152
        get_device method.
153
        """
154
        self._devices.remove(device)
155
        device.destroy()
156

    
157

    
158
class DiskDevice(object):
159
    """This class represents a block device hosting an Operating System
160
    as created by the device-mapper.
161
    """
162

    
163
    def __init__(self, device, bootable=True):
164
        """Create a new DiskDevice."""
165

    
166
        self.device = device
167
        self.bootable = bootable
168
        self.progress_bar = None
169

    
170
        self.g = guestfs.GuestFS()
171
        self.g.add_drive_opts(self.device, readonly=0)
172

    
173
        #self.g.set_trace(1)
174
        #self.g.set_verbose(1)
175

    
176
        self.guestfs_enabled = False
177

    
178
    def enable(self):
179
        """Enable a newly created DiskDevice"""
180
        self.progressbar = progress("Launching helper VM: ")
181
        self.progressbar.next()
182
        eh = self.g.set_event_callback(self.progress_callback,
183
                                                    guestfs.EVENT_PROGRESS)
184
        self.g.launch()
185
        self.guestfs_enabled = True
186
        self.g.delete_event_callback(eh)
187
        if self.progressbar is not None:
188
            self.progressbar.send(100)
189
            self.progressbar = None
190

    
191
        output('Inspecting Operating System...', False)
192
        roots = self.g.inspect_os()
193
        if len(roots) == 0:
194
            raise FatalError("No operating system found")
195
        if len(roots) > 1:
196
            raise FatalError("Multiple operating systems found."
197
                            "We only support images with one filesystem.")
198
        self.root = roots[0]
199
        self.ostype = self.g.inspect_get_type(self.root)
200
        self.distro = self.g.inspect_get_distro(self.root)
201
        success('found a %s system' % self.distro)
202

    
203
    def destroy(self):
204
        """Destroy this DiskDevice instance."""
205

    
206
        if self.guestfs_enabled:
207
            self.g.umount_all()
208
            self.g.sync()
209

    
210
        # Close the guestfs handler if open
211
        self.g.close()
212

    
213
    def progress_callback(self, ev, eh, buf, array):
214
        position = array[2]
215
        total = array[3]
216

    
217
        self.progressbar.send((position * 100) // total)
218

    
219
        if position == total:
220
            self.progressbar = None
221

    
222
    def mount(self):
223
        """Mount all disk partitions in a correct order."""
224

    
225
        output("Mounting image...", False)
226
        mps = self.g.inspect_get_mountpoints(self.root)
227

    
228
        # Sort the keys to mount the fs in a correct order.
229
        # / should be mounted befor /boot, etc
230
        def compare(a, b):
231
            if len(a[0]) > len(b[0]):
232
                return 1
233
            elif len(a[0]) == len(b[0]):
234
                return 0
235
            else:
236
                return -1
237
        mps.sort(compare)
238
        for mp, dev in mps:
239
            try:
240
                self.g.mount(dev, mp)
241
            except RuntimeError as msg:
242
                warn("%s (ignored)" % msg)
243
        success("done")
244

    
245
    def umount(self):
246
        """Umount all mounted filesystems."""
247
        self.g.umount_all()
248

    
249
    def shrink(self):
250
        """Shrink the disk.
251

252
        This is accomplished by shrinking the last filesystem in the
253
        disk and then updating the partition table. The new disk size
254
        (in bytes) is returned.
255
        """
256
        output("Shrinking image (this may take a while)...", False)
257

    
258
        dev = self.g.part_to_dev(self.root)
259
        parttype = self.g.part_get_parttype(dev)
260
        if parttype != 'msdos':
261
            raise FatalError("You have a %s partition table. "
262
                "Only msdos partitions are supported" % parttype)
263

    
264
        last_partition = self.g.part_list(dev)[-1]
265

    
266
        if last_partition['part_num'] > 4:
267
            raise FatalError("This disk contains logical partitions. "
268
                "Only primary partitions are supported.")
269

    
270
        part_dev = "%s%d" % (dev, last_partition['part_num'])
271
        fs_type = self.g.vfs_type(part_dev)
272
        if not re.match("ext[234]", fs_type):
273
            warn("Don't know how to resize %s partitions." % vfs_type)
274
            return
275

    
276
        self.g.e2fsck_f(part_dev)
277
        self.g.resize2fs_M(part_dev)
278

    
279
        output = self.g.tune2fs_l(part_dev)
280
        block_size = int(
281
            filter(lambda x: x[0] == 'Block size', output)[0][1])
282
        block_cnt = int(
283
            filter(lambda x: x[0] == 'Block count', output)[0][1])
284

    
285
        sector_size = self.g.blockdev_getss(dev)
286

    
287
        start = last_partition['part_start'] / sector_size
288
        end = start + (block_size * block_cnt) / sector_size - 1
289

    
290
        self.g.part_del(dev, last_partition['part_num'])
291
        self.g.part_add(dev, 'p', start, end)
292

    
293
        new_size = (end + 1) * sector_size
294
        success("new image size is %dMB" %
295
                            ((new_size + 2 ** 20 - 1) // 2 ** 20))
296
        return new_size
297

    
298
    def size(self):
299
        """Returns the "payload" size of the device.
300

301
        The size returned by this method is the size of the space occupied by
302
        the partitions (including the space before the first partition).
303
        """
304
        dev = self.g.part_to_dev(self.root)
305
        last = self.g.part_list(dev)[-1]
306

    
307
        return last['part_end'] + 1
308

    
309
    def dump(self, outfile):
310
        """Dumps the content of device into a file.
311

312
        This method will only dump the actual payload, found by reading the
313
        partition table. Empty space in the end of the device will be ignored.
314
        """
315
        blocksize = 2 ** 22  # 4MB
316
        size = self.size()
317
        progress_size = (size + 2 ** 20 - 1) // 2 ** 20  # in MB
318
        progressbar = progress("Dumping image file: ", progress_size)
319

    
320
        source = open(self.device, "r")
321
        try:
322
            dest = open(outfile, "w")
323
            try:
324
                left = size
325
                offset = 0
326
                progressbar.next()
327
                while left > 0:
328
                    length = min(left, blocksize)
329
                    sent = sendfile(dest.fileno(), source.fileno(), offset,
330
                                                                        length)
331
                    offset += sent
332
                    left -= sent
333
                    for i in range((length + 2 ** 20 - 1) // 2 ** 20):
334
                        progressbar.next()
335
            finally:
336
                dest.close()
337
        finally:
338
            source.close()
339

    
340
        success('Image file %s was successfully created' % outfile)
341

    
342
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :