Statistics
| Branch: | Tag: | Revision:

root / image_creator / disk.py @ 550d4a49

History | View | Annotate | Download (16.3 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 FatalError
36
from image_creator.gpt import GPTPartitionTable
37
import stat
38
import os
39
import tempfile
40
import uuid
41
import re
42
import sys
43
import guestfs
44
import time
45
from sendfile import sendfile
46

    
47

    
48
class DiskError(Exception):
49
    pass
50

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

    
56

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

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

    
65
    def __init__(self, source, output):
66
        """Create a new Disk instance out of a source media. The source
67
        media can be an image file, a block device or a directory."""
68
        self._cleanup_jobs = []
69
        self._devices = []
70
        self.source = source
71
        self.out = output
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 snapshot(self):
98
        """Creates a snapshot of the original source media of the Disk
99
        instance.
100
        """
101

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

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

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

    
139
        finally:
140
            os.unlink(table)
141
        self.out.success('done')
142
        return "/dev/mapper/%s" % snapshot
143

    
144
    def get_device(self, media):
145
        """Returns a newly created DiskDevice instance."""
146

    
147
        new_device = DiskDevice(media, self.out)
148
        self._devices.append(new_device)
149
        new_device.enable()
150
        return new_device
151

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

    
159

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

    
165
    def __init__(self, device, output, bootable=True):
166
        """Create a new DiskDevice."""
167

    
168
        self.real_device = device
169
        self.out = output
170
        self.bootable = bootable
171
        self.progress_bar = None
172
        self.guestfs_device = None
173
        self.meta = {}
174

    
175
        self.g = guestfs.GuestFS()
176
        self.g.add_drive_opts(self.real_device, readonly=0)
177

    
178
        # Before version 1.17.14 the recovery process, which is a fork of the
179
        # original process that called libguestfs, did not close its inherited
180
        # file descriptors. This can cause problems especially if the parent
181
        # process has opened pipes. Since the recovery process is an optional
182
        # feature of libguestfs, it's better to disable it.
183
        self.g.set_recovery_proc(0)
184
        version = self.g.version()
185
        if version['major'] > 1 or (version['major'] == 1 and
186
            (version['minor'] >= 18 or \
187
            (version['minor'] == 17 and version['release'] >= 14))):
188
            self.g.set_recovery_proc(1)
189
            self.out.output("Enabling recovery proc")
190

    
191
        #self.g.set_trace(1)
192
        #self.g.set_verbose(1)
193

    
194
        self.guestfs_enabled = False
195

    
196
    def enable(self):
197
        """Enable a newly created DiskDevice"""
198
        self.progressbar = self.out.Progress(100, "Launching helper VM",
199
                                             "percent")
200
        eh = self.g.set_event_callback(self.progress_callback,
201
                                                    guestfs.EVENT_PROGRESS)
202
        self.g.launch()
203
        self.guestfs_enabled = True
204
        self.g.delete_event_callback(eh)
205
        self.progressbar.success('done')
206
        self.progressbar = None
207

    
208
        self.out.output('Inspecting Operating System...', False)
209
        roots = self.g.inspect_os()
210
        if len(roots) == 0:
211
            raise FatalError("No operating system found")
212
        if len(roots) > 1:
213
            raise FatalError("Multiple operating systems found."
214
                            "We only support images with one filesystem.")
215
        self.root = roots[0]
216
        self.guestfs_device = self.g.part_to_dev(self.root)
217
        self.meta['SIZE'] = self.g.blockdev_getsize64(self.guestfs_device)
218
        self.meta['PARTITION_TABLE'] = \
219
                                self.g.part_get_parttype(self.guestfs_device)
220

    
221
        self.ostype = self.g.inspect_get_type(self.root)
222
        self.distro = self.g.inspect_get_distro(self.root)
223
        self.out.success('found a(n) %s system' % self.distro)
224

    
225
    def destroy(self):
226
        """Destroy this DiskDevice instance."""
227

    
228
        if self.guestfs_enabled:
229
            self.g.umount_all()
230
            self.g.sync()
231

    
232
        # Close the guestfs handler if open
233
        self.g.close()
234

    
235
    def progress_callback(self, ev, eh, buf, array):
236
        position = array[2]
237
        total = array[3]
238

    
239
        self.progressbar.goto((position * 100) // total)
240

    
241
    def mount(self, readonly=False):
242
        """Mount all disk partitions in a correct order."""
243

    
244
        mount = self.g.mount_ro if readonly else self.g.mount
245
        self.out.output("Mounting image...", False)
246
        mps = self.g.inspect_get_mountpoints(self.root)
247

    
248
        # Sort the keys to mount the fs in a correct order.
249
        # / should be mounted befor /boot, etc
250
        def compare(a, b):
251
            if len(a[0]) > len(b[0]):
252
                return 1
253
            elif len(a[0]) == len(b[0]):
254
                return 0
255
            else:
256
                return -1
257
        mps.sort(compare)
258
        for mp, dev in mps:
259
            try:
260
                mount(dev, mp)
261
            except RuntimeError as msg:
262
                self.out.warn("%s (ignored)" % msg)
263
        self.out.success("done")
264

    
265
    def umount(self):
266
        """Umount all mounted filesystems."""
267
        self.g.umount_all()
268

    
269
    def _last_partition(self):
270
        if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
271
            msg = "Unsupported partition table: %s. Only msdos and gpt " \
272
            "partition tables are supported" % self.meta['PARTITION_TABLE']
273
            raise FatalError(msg)
274

    
275
        is_extended = lambda p: self.g.part_get_mbr_id(
276
                                    self.guestfs_device, p['part_num']) == 5
277
        is_logical = lambda p: self.meta['PARTITION_TABLE'] != 'msdos' and \
278
                                                            p['part_num'] > 4
279

    
280
        partitions = self.g.part_list(self.guestfs_device)
281
        last_partition = partitions[-1]
282

    
283
        if is_logical(last_partition):
284
            # The disk contains extended and logical partitions....
285
            extended = [p for p in partitions if is_extended(p)][0]
286
            last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
287

    
288
            # check if extended is the last primary partition
289
            if last_primary['part_num'] > extended['part_num']:
290
                last_partition = last_primary
291

    
292
        return last_partition
293

    
294
    def shrink(self):
295
        """Shrink the disk.
296

297
        This is accomplished by shrinking the last filesystem in the
298
        disk and then updating the partition table. The new disk size
299
        (in bytes) is returned.
300

301
        ATTENTION: make sure unmount is called before shrink
302
        """
303
        get_fstype = lambda p: self.g.vfs_type("%s%d" % \
304
                                        (self.guestfs_device, p['part_num']))
305
        is_logical = lambda p: self.meta['PARTITION_TABLE'] == 'msdos' and \
306
                                                            p['part_num'] > 4
307
        is_extended = lambda p: self.meta['PARTITION_TABLE'] == 'msdos' and \
308
                self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
309

    
310
        part_add = lambda ptype, start, stop: \
311
                    self.g.part_add(self.guestfs_device, ptype, start, stop)
312
        part_del = lambda p: self.g.part_del(self.guestfs_device, p)
313
        part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
314
        part_set_id = lambda p, id: self.g.part_set_mbr_id(
315
                                                    self.guestfs_device, p, id)
316
        part_get_bootable = lambda p: self.g.part_get_bootable(
317
                                                        self.guestfs_device, p)
318
        part_set_bootable = lambda p, bootable: self.g.part_set_bootable(
319
                                            self.guestfs_device, p, bootable)
320

    
321
        MB = 2 ** 20
322

    
323
        self.out.output("Shrinking image (this may take a while)...", False)
324

    
325
        last_part = None
326
        fstype = None
327
        while True:
328
            last_part = self._last_partition()
329
            fstype = get_fstype(last_part)
330

    
331
            if fstype == 'swap':
332
                self.meta['SWAP'] = "%d:%s" % \
333
                        (last_part['part_num'],
334
                        (last_part['part_size'] + MB - 1) // MB)
335
                part_del(last_part['part_num'])
336
                continue
337
            elif is_extended(last_part):
338
                part_del(last_part['part_num'])
339
                continue
340

    
341
            self.meta['SIZE'] = last_part['part_end'] + 1
342
            break
343

    
344
        if not re.match("ext[234]", fstype):
345
            self.out.warn("Don't know how to resize %s partitions." % fstype)
346
            return self.meta['SIZE']
347

    
348
        part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
349
        self.g.e2fsck_f(part_dev)
350
        self.g.resize2fs_M(part_dev)
351

    
352
        out = self.g.tune2fs_l(part_dev)
353
        block_size = int(
354
            filter(lambda x: x[0] == 'Block size', out)[0][1])
355
        block_cnt = int(
356
            filter(lambda x: x[0] == 'Block count', out)[0][1])
357

    
358
        sector_size = self.g.blockdev_getss(self.guestfs_device)
359
        start = last_part['part_start'] / sector_size
360
        end = start + (block_size * block_cnt) / sector_size - 1
361

    
362
        if is_logical(last_part):
363
            partitions = self.g.part_list(self.guestfs_device)
364

    
365
            logical = []  # logical partitions
366
            for partition in partitions:
367
                if partition['part_num'] < 4:
368
                    continue
369
                logical.append({
370
                    'num': partition['part_num'],
371
                    'start': partition['part_start'] / sector_size,
372
                    'end': partition['part_end'] / sector_size,
373
                    'id': part_get_(partition['part_num']),
374
                    'bootable': part_get_bootable(partition['part_num'])
375
                })
376

    
377
            logical[-1]['end'] = end  # new end after resize
378

    
379
            # Recreate the extended partition
380
            extended = [p for p in partitions if self._is_extended(p)][0]
381
            part_del(extended['part_num'])
382
            part_add('e', extended['part_start'], end)
383

    
384
            # Create all the logical partitions back
385
            for l in logical:
386
                part_add('l', l['start'], l['end'])
387
                part_set_id(l['num'], l['id'])
388
                part_set_bootable(l['num'], l['bootable'])
389
        else:
390
            # Recreate the last partition
391
            if self.meta['PARTITION_TABLE'] == 'msdos':
392
                last_part['id'] = part_get_id(last_part['part_num'])
393

    
394
            last_part['bootable'] = part_get_bootable(last_part['part_num'])
395
            part_del(last_part['part_num'])
396
            part_add('p', start, end)
397
            part_set_bootable(last_part['part_num'], last_part['bootable'])
398

    
399
            if self.meta['PARTITION_TABLE'] == 'msdos':
400
                part_set_id(last_part['part_num'], last_part['id'])
401

    
402
        new_size = (end + 1) * sector_size
403
        self.out.success("new size is %dMB" % ((new_size + MB - 1) // MB))
404

    
405
        if self.meta['PARTITION_TABLE'] == 'gpt':
406
            ptable = GPTPartitionTable(self.real_device)
407
            self.meta['SIZE'] = ptable.shrink(new_size)
408
        else:
409
            self.meta['SIZE'] = new_size
410

    
411
        return self.meta['SIZE']
412

    
413
    def dump(self, outfile):
414
        """Dumps the content of device into a file.
415

416
        This method will only dump the actual payload, found by reading the
417
        partition table. Empty space in the end of the device will be ignored.
418
        """
419
        MB = 2 ** 20
420
        blocksize = 4 * MB  # 4MB
421
        size = self.meta['SIZE']
422
        progr_size = (size + MB - 1) // MB  # in MB
423
        progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
424

    
425
        with open(self.real_device, 'r') as src:
426
            with open(outfile, "w") as dst:
427
                left = size
428
                offset = 0
429
                progressbar.next()
430
                while left > 0:
431
                    length = min(left, blocksize)
432
                    sent = sendfile(dst.fileno(), src.fileno(), offset, length)
433
                    offset += sent
434
                    left -= sent
435
                    progressbar.goto((size - left) // MB)
436
        progressbar.success('image file %s was successfully created' % outfile)
437

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