Statistics
| Branch: | Tag: | Revision:

root / image_creator / bundle_volume.py @ 83fe59dd

History | View | Annotate | Download (14.7 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
import os
35
import re
36
import uuid
37
import tempfile
38
import time
39
from collections import namedtuple
40

    
41
import parted
42

    
43
from image_creator.rsync import Rsync
44
from image_creator.util import get_command
45
from image_creator.util import FatalError
46

    
47
findfs = get_command('findfs')
48
truncate = get_command('truncate')
49
dd = get_command('dd')
50
dmsetup = get_command('dmsetup')
51
losetup = get_command('losetup')
52
mount = get_command('mount')
53
umount = get_command('umount')
54
blkid = get_command('blkid')
55

    
56
MKFS_OPTS = {
57
    'ext2': ['-F'],
58
    'ext3': ['-F'],
59
    'ext4': ['-F'],
60
    'reiserfs': ['-ff'],
61
    'btrfs': [],
62
    'minix': [],
63
    'xfs': ['-f'],
64
    'jfs': ['-f'],
65
    'ntfs': ['-F'],
66
    'msdos': [],
67
    'vfat': []
68
    }
69

    
70

    
71
class BundleVolume():
72

    
73
    def __init__(self, out, meta):
74
        self.out = out
75
        self.meta = meta
76

    
77
        self.out.output('Searching for root device ...', False)
78
        root = self._get_root_partition()
79

    
80
        if root.startswith("UUID=") or root.startswith("LABEL="):
81
            root = findfs(root).stdout.strip()
82

    
83
        if not re.match('/dev/[hsv]d[a-z][1-9]*$', root):
84
            raise FatalError("Don't know how to handle root device: %s" % root)
85

    
86
        out.success(root)
87

    
88
        disk_file = re.split('[0-9]', root)[0]
89
        device = parted.Device(disk_file)
90
        self.disk = parted.Disk(device)
91

    
92
    def _read_fstable(self, f):
93

    
94
        if not os.path.isfile(f):
95
            raise FatalError("Unable to open: `%s'. File is missing." % f)
96

    
97
        FileSystemTableEntry = namedtuple('FileSystemTableEntry',
98
                                     'dev mpoint fs opts freq passno')
99
        with open(f) as table:
100
            for line in iter(table):
101
                entry = line.split('#')[0].strip().split()
102
                if len(entry) != 6:
103
                    continue
104
                yield FileSystemTableEntry(*entry)
105

    
106
    def _get_root_partition(self):
107
        for entry in self._read_fstable('/etc/fstab'):
108
            if entry.mpoint == '/':
109
                return entry.dev
110

    
111
        raise FatalError("Unable to find root device in /etc/fstab")
112

    
113
    def _is_mpoint(self, path):
114
        for entry in self._read_fstable('/proc/mounts'):
115
            if entry.mpoint == path:
116
                return True
117
        return False
118

    
119
    def _get_mount_options(self, device):
120
        for entry in self._read_fstable('/proc/mounts'):
121
            if not entry.dev.startswith('/'):
122
                continue
123

    
124
            if os.path.realpath(entry.dev) == os.path.realpath(device):
125
                return entry
126

    
127
        return None
128

    
129
    def _create_partition_table(self, image):
130

    
131
        if self.disk.type != 'msdos':
132
            raise FatalError('Only msdos partition tables are supported')
133

    
134
        # Copy the MBR and the space between the MBR and the first partition.
135
        # In Grub version 1 Stage 1.5 is located there.
136
        first_sector = self.disk.getPrimaryPartitions()[0].geometry.start
137

    
138
        dd('if=%s' % self.disk.device.path, 'of=%s' % image,
139
           'bs=%d' % self.disk.device.sectorSize,
140
           'count=%d' % first_sector, 'conv=notrunc')
141

    
142
        # Create the Extended boot records (EBRs) in the image
143
        extended = self.disk.getExtendedPartition()
144
        if not extended:
145
            return
146

    
147
        # Extended boot records precede the logical partitions they describe
148
        logical = self.disk.getLogicalPartitions()
149
        start = extended.geometry.start
150
        for i in range(len(logical)):
151
            end = logical[i].geometry.start - 1
152
            dd('if=%s' % self.disk.device.path, 'of=%s' % image,
153
               'count=%d' % (end - start + 1), 'conv=notrunc',
154
               'seek=%d' % start, 'skip=%d' % start)
155
            start = logical[i].geometry.end + 1
156

    
157
    def _get_partitions(self, disk):
158
        Partition = namedtuple('Partition', 'num start end type fs')
159

    
160
        partitions = []
161
        for p in disk.partitions:
162
            num = p.number
163
            start = p.geometry.start
164
            end = p.geometry.end
165
            ptype = p.type
166
            fs = p.fileSystem.type if p.fileSystem is not None else ''
167
            partitions.append(Partition(num, start, end, ptype, fs))
168

    
169
        return partitions
170

    
171
    def _shrink_partitions(self, image):
172

    
173
        new_end = self.disk.device.getLength()
174

    
175
        image_dev = parted.Device(image)
176
        image_disk = parted.Disk(image_dev)
177

    
178
        is_extended = lambda p: p.type == parted.PARTITION_EXTENDED
179
        is_logical = lambda p: p.type == parted.PARTITION_LOGICAL
180

    
181
        partitions = self._get_partitions(self.disk)
182

    
183
        last = partitions[-1]
184
        if last.fs == 'linux-swap(v1)':
185
            MB = 2 ** 20
186
            size = (last.end - last.start + 1) * self.disk.device.sectorSize
187
            self.meta['SWAP'] = "%d:%s" % (last.num, (size + MB - 1) // MB)
188

    
189
            image_disk.deletePartition(
190
                image_disk.getPartitionBySector(last.start))
191
            image_disk.commit()
192

    
193
            if is_logical(last) and last.num == 5:
194
                extended = image_disk.getExtendedPartition()
195
                image_disk.deletePartition(extended)
196
                image_disk.commit()
197
                partitions.remove(filter(is_extended, partitions)[0])
198

    
199
            partitions.remove(last)
200
            last = partitions[-1]
201

    
202
            # Leave 2048 blocks at the end
203
            new_end = last.end + 2048
204

    
205
        mount_options = self._get_mount_options(
206
                self.disk.getPartitionBySector(last.start).path)
207
        if mount_options is not None:
208
            stat = os.statvfs(mount_options.mpoint)
209
            # Shrink the last partition. The new size should be the size of the
210
            # occupied blocks
211
            blcks = stat.f_blocks - stat.f_bavail
212
            new_size = (blcks * stat.f_frsize) // self.disk.device.sectorSize
213

    
214
            # Add 10% just to be on the safe side
215
            part_end = last.start + (new_size * 11) // 10
216
            # Alighn to 2048
217
            part_end = ((part_end + 2047) // 2048) * 2048
218

    
219
            image_disk.setPartitionGeometry(
220
                image_disk.getPartitionBySector(last.start),
221
                parted.Constraint(device=image_disk.device),
222
                start=last.start, end=part_end)
223
            image_disk.commit()
224

    
225
            # Parted may have changed this for better alignment
226
            part_end = image_disk.getPartitionBySector(last.start).geometry.end
227
            last = last._replace(end=part_end)
228
            partitions[-1] = last
229

    
230
            # Leave 2048 blocks at the end.
231
            new_end = part_end + 2048
232

    
233
            if last.type == parted.PARTITION_LOGICAL:
234
                # Fix the extended partition
235
                extended = disk.getExtendedPartition()
236

    
237
                image_disk.setPartitionGeometry(extended,
238
                    parted.Constraint(device=img_dev),
239
                    ext.geometry.start, end=last.end)
240
                image_disk.commit()
241

    
242
        image_dev.destroy()
243
        return new_end
244

    
245
    def _map_partition(self, dev, num, start, end):
246
        name = os.path.basename(dev)
247
        tablefd, table = tempfile.mkstemp()
248
        try:
249
            size = end - start + 1
250
            os.write(tablefd, "0 %d linear %s %d" % (size, dev, start))
251
            dmsetup('create', "%sp%d" % (name, num), table)
252
        finally:
253
            os.unlink(table)
254

    
255
        return "/dev/mapper/%sp%d" % (name, num)
256

    
257
    def _unmap_partition(self, dev):
258
        if not os.path.exists(dev):
259
            return
260

    
261
        dmsetup('remove', dev.split('/dev/mapper/')[1])
262
        time.sleep(0.1)
263

    
264
    def _mount(self, target, devs):
265

    
266
        devs.sort(key=lambda d: d[1])
267
        for dev, mpoint in devs:
268
            absmpoint = os.path.abspath(target + mpoint)
269
            if not os.path.exists(absmpoint):
270
                os.makedirs(absmpoint)
271
            mount(dev, absmpoint)
272

    
273
    def _umount_all(self, target):
274
        mpoints = []
275
        for entry in self._read_fstable('/proc/mounts'):
276
            if entry.mpoint.startswith(os.path.abspath(target)):
277
                    mpoints.append(entry.mpoint)
278

    
279
        mpoints.sort()
280
        for mpoint in reversed(mpoints):
281
            umount(mpoint)
282

    
283
    def _to_exclude(self):
284
        excluded = ['/tmp', '/var/tmp']
285
        local_filesystems = MKFS_OPTS.keys() + ['rootfs']
286
        for entry in self._read_fstable('/proc/mounts'):
287
            if entry.fs in local_filesystems:
288
                continue
289

    
290
            mpoint = entry.mpoint
291
            if mpoint in excluded:
292
                continue
293

    
294
            descendants = filter(lambda p: p.startswith(mpoint + '/'),
295
                    excluded)
296
            if len(descendants):
297
                for d in descendants:
298
                    excluded.remove(d)
299
                excluded.append(mpoint)
300
                continue
301

    
302
            dirname = mpoint
303
            basename = ''
304
            found_ancestor = False
305
            while dirname != '/':
306
                (dirname, basename) = os.path.split(dirname)
307
                if dirname in excluded:
308
                    found_ancestor = True
309
                    break
310

    
311
            if not found_ancestor:
312
                excluded.append(mpoint)
313

    
314
        return map(lambda d: d + "/*", excluded)
315

    
316
    def _replace_uuids(self, target, new_uuid):
317

    
318
        files = ['/etc/fstab',
319
                 '/boot/grub/grub.cfg',
320
                 '/boot/grub/menu.lst',
321
                 '/boot/grub/grub.conf']
322

    
323
        orig = dict(map(lambda p: (p.number, blkid( '-s', 'UUID', '-o',
324
            'value', p.path).stdout.strip()), self.disk.partitions))
325

    
326
        for f in map(lambda f: target + f, files):
327

    
328
            if not os.path.exists(f):
329
                continue
330

    
331
            with open(f, 'r') as src:
332
                lines = src.readlines()
333
            with open(f, 'w') as dest:
334
                for line in lines:
335
                    for i, uuid in new_uuid.items():
336
                        line = re.sub(orig[i], uuid, line)
337
                    dest.write(line)
338

    
339
    def _create_filesystems(self, image):
340

    
341
        filesystem = {}
342
        for p in self.disk.partitions:
343
            filesystem[p.number] = self._get_mount_options(p.path)
344

    
345
        partitions = self._get_partitions(parted.Disk(parted.Device(image)))
346
        unmounted = filter(lambda p: filesystem[p.num] is None, partitions)
347
        mounted = filter(lambda p: filesystem[p.num] is not None, partitions)
348

    
349
        # For partitions that are not mounted right now, we can simply dd them
350
        # into the image.
351
        for p in unmounted:
352
            dd('if=%s' % self.disk.device.path, 'of=%s' % image,
353
               'count=%d' % (p.end - p.start + 1), 'conv=notrunc',
354
               'seek=%d' % p.start, 'skip=%d' % p.start)
355

    
356
        loop = str(losetup('-f', '--show', image)).strip()
357
        mapped = {}
358
        try:
359
            for p in mounted:
360
                i = p.num
361
                mapped[i] = self._map_partition(loop, i, p.start, p.end)
362

    
363
            new_uuid = {}
364
            # Create the file systems
365
            for i, dev in mapped.iteritems():
366
                fs = filesystem[i].fs
367
                self.out.output('Creating %s filesystem on partition %d ... ' %
368
                    (fs, i), False)
369
                get_command('mkfs.%s' % fs)(*(MKFS_OPTS[fs] + [dev]))
370
                self.out.success('done')
371
                new_uuid[i] = blkid('-s', 'UUID', '-o', 'value', dev
372
                    ).stdout.strip()
373

    
374
            target = tempfile.mkdtemp()
375
            try:
376
                absmpoints = self._mount(target,
377
                    [(mapped[i], filesystem[i].mpoint) for i in mapped.keys()]
378
                )
379
                exclude = self._to_exclude() + [image]
380
                rsync = Rsync('/', target,
381
                              map(lambda p: os.path.relpath(p, '/'), exclude))
382
                msg = "Copying host files into the image"
383
                rsync.archive().run(self.out, msg)
384

    
385
                self._replace_uuids(target, new_uuid)
386

    
387
            finally:
388
                self._umount_all(target)
389
                os.rmdir(target)
390
        finally:
391
            for dev in mapped.values():
392
                self._unmap_partition(dev)
393
            losetup('-d', loop)
394

    
395
    def create_image(self):
396

    
397
        image = '/mnt/%s.diskdump' % uuid.uuid4().hex
398

    
399
        disk_size = self.disk.device.getLength() * self.disk.device.sectorSize
400

    
401
        # Create sparse file to host the image
402
        truncate("-s", "%d" % disk_size, image)
403

    
404
        self._create_partition_table(image)
405

    
406
        end_sector = self._shrink_partitions(image)
407

    
408
        # Check if the available space is enough to host the image
409
        dirname = os.path.dirname(image)
410
        size = (end_sector + 1) * self.disk.device.sectorSize
411
        self.out.output("Examining available space in %s ..." % dirname, False)
412
        stat = os.statvfs(dirname)
413
        available = stat.f_bavail * stat.f_frsize
414
        if available <= size:
415
            raise FatalError('Not enough space in %s to host the image' %
416
                             dirname)
417
        self.out.success("sufficient")
418

    
419
        self._create_filesystems(image)
420

    
421
        # Truncate image to the new size. I counldn't find a better way to do
422
        # this. It seems that python's high level functions work in a different
423
        # way.
424
        fd = os.open(image, os.O_RDWR)
425
        try:
426
            os.ftruncate(fd, size)
427
        finally:
428
            os.close(fd)
429

    
430
        return image
431

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