Statistics
| Branch: | Tag: | Revision:

root / image_creator / bundle_volume.py @ 63af9c37

History | View | Annotate | Download (19.9 kB)

1 121f3bc0 Nikos Skalkotos
# -*- coding: utf-8 -*-
2 121f3bc0 Nikos Skalkotos
#
3 9e4b4de2 Nikos Skalkotos
# Copyright 2012 GRNET S.A. All rights reserved.
4 9e4b4de2 Nikos Skalkotos
#
5 9e4b4de2 Nikos Skalkotos
# Redistribution and use in source and binary forms, with or
6 9e4b4de2 Nikos Skalkotos
# without modification, are permitted provided that the following
7 9e4b4de2 Nikos Skalkotos
# conditions are met:
8 9e4b4de2 Nikos Skalkotos
#
9 9e4b4de2 Nikos Skalkotos
#   1. Redistributions of source code must retain the above
10 9e4b4de2 Nikos Skalkotos
#      copyright notice, this list of conditions and the following
11 9e4b4de2 Nikos Skalkotos
#      disclaimer.
12 9e4b4de2 Nikos Skalkotos
#
13 9e4b4de2 Nikos Skalkotos
#   2. Redistributions in binary form must reproduce the above
14 9e4b4de2 Nikos Skalkotos
#      copyright notice, this list of conditions and the following
15 9e4b4de2 Nikos Skalkotos
#      disclaimer in the documentation and/or other materials
16 9e4b4de2 Nikos Skalkotos
#      provided with the distribution.
17 9e4b4de2 Nikos Skalkotos
#
18 9e4b4de2 Nikos Skalkotos
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 9e4b4de2 Nikos Skalkotos
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 9e4b4de2 Nikos Skalkotos
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 9e4b4de2 Nikos Skalkotos
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 9e4b4de2 Nikos Skalkotos
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 9e4b4de2 Nikos Skalkotos
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 9e4b4de2 Nikos Skalkotos
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 9e4b4de2 Nikos Skalkotos
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 9e4b4de2 Nikos Skalkotos
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 9e4b4de2 Nikos Skalkotos
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 9e4b4de2 Nikos Skalkotos
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 9e4b4de2 Nikos Skalkotos
# POSSIBILITY OF SUCH DAMAGE.
30 9e4b4de2 Nikos Skalkotos
#
31 9e4b4de2 Nikos Skalkotos
# The views and conclusions contained in the software and
32 9e4b4de2 Nikos Skalkotos
# documentation are those of the authors and should not be
33 9e4b4de2 Nikos Skalkotos
# interpreted as representing official policies, either expressed
34 9e4b4de2 Nikos Skalkotos
# or implied, of GRNET S.A.
35 9e4b4de2 Nikos Skalkotos
36 121f3bc0 Nikos Skalkotos
"""This module hosts the code that performes the host bundling operation. By
37 121f3bc0 Nikos Skalkotos
using the create_image method of the BundleVolume class the user can create an
38 121f3bc0 Nikos Skalkotos
image out of the running system.
39 121f3bc0 Nikos Skalkotos
"""
40 121f3bc0 Nikos Skalkotos
41 27a4229d Nikos Skalkotos
import os
42 27a4229d Nikos Skalkotos
import re
43 8eea5572 Nikos Skalkotos
import tempfile
44 b0376b3f Nikos Skalkotos
import uuid
45 9517bf29 Nikos Skalkotos
from collections import namedtuple
46 27a4229d Nikos Skalkotos
47 8eea5572 Nikos Skalkotos
import parted
48 8eea5572 Nikos Skalkotos
49 74149d07 Nikos Skalkotos
from image_creator.rsync import Rsync
50 27a4229d Nikos Skalkotos
from image_creator.util import get_command
51 9e4b4de2 Nikos Skalkotos
from image_creator.util import FatalError
52 f3845095 Nikos Skalkotos
from image_creator.util import try_fail_repeat
53 c16850ae Nikos Skalkotos
from image_creator.util import free_space
54 b0376b3f Nikos Skalkotos
from image_creator.gpt import GPTPartitionTable
55 9e4b4de2 Nikos Skalkotos
56 27a4229d Nikos Skalkotos
findfs = get_command('findfs')
57 27a4229d Nikos Skalkotos
dd = get_command('dd')
58 8eea5572 Nikos Skalkotos
dmsetup = get_command('dmsetup')
59 25b4d858 Nikos Skalkotos
losetup = get_command('losetup')
60 25b4d858 Nikos Skalkotos
mount = get_command('mount')
61 25b4d858 Nikos Skalkotos
umount = get_command('umount')
62 bf15a033 Nikos Skalkotos
blkid = get_command('blkid')
63 81a63274 Nikos Skalkotos
tune2fs = get_command('tune2fs')
64 25b4d858 Nikos Skalkotos
65 6228d45e Nikos Skalkotos
MKFS_OPTS = {'ext2': ['-F'],
66 6228d45e Nikos Skalkotos
             'ext3': ['-F'],
67 6228d45e Nikos Skalkotos
             'ext4': ['-F'],
68 6228d45e Nikos Skalkotos
             'reiserfs': ['-ff'],
69 6228d45e Nikos Skalkotos
             'btrfs': [],
70 6228d45e Nikos Skalkotos
             'minix': [],
71 6228d45e Nikos Skalkotos
             'xfs': ['-f'],
72 6228d45e Nikos Skalkotos
             'jfs': ['-f'],
73 6228d45e Nikos Skalkotos
             'ntfs': ['-F'],
74 6228d45e Nikos Skalkotos
             'msdos': [],
75 6228d45e Nikos Skalkotos
             'vfat': []}
76 9517bf29 Nikos Skalkotos
77 9517bf29 Nikos Skalkotos
78 f3845095 Nikos Skalkotos
class BundleVolume(object):
79 f3845095 Nikos Skalkotos
    """This class can be used to create an image out of the running system"""
80 27a4229d Nikos Skalkotos
81 c16850ae Nikos Skalkotos
    def __init__(self, out, meta, tmp=None):
82 f3845095 Nikos Skalkotos
        """Create an instance of the BundleVolume class."""
83 8eea5572 Nikos Skalkotos
        self.out = out
84 8eea5572 Nikos Skalkotos
        self.meta = meta
85 c16850ae Nikos Skalkotos
        self.tmp = tmp
86 27a4229d Nikos Skalkotos
87 a939e3b8 Nikos Skalkotos
        self.out.output('Searching for root device ...', False)
88 8eea5572 Nikos Skalkotos
        root = self._get_root_partition()
89 27a4229d Nikos Skalkotos
90 8eea5572 Nikos Skalkotos
        if root.startswith("UUID=") or root.startswith("LABEL="):
91 25b4d858 Nikos Skalkotos
            root = findfs(root).stdout.strip()
92 27a4229d Nikos Skalkotos
93 25b4d858 Nikos Skalkotos
        if not re.match('/dev/[hsv]d[a-z][1-9]*$', root):
94 25b4d858 Nikos Skalkotos
            raise FatalError("Don't know how to handle root device: %s" % root)
95 9517bf29 Nikos Skalkotos
96 25b4d858 Nikos Skalkotos
        out.success(root)
97 9517bf29 Nikos Skalkotos
98 25b4d858 Nikos Skalkotos
        disk_file = re.split('[0-9]', root)[0]
99 25b4d858 Nikos Skalkotos
        device = parted.Device(disk_file)
100 25b4d858 Nikos Skalkotos
        self.disk = parted.Disk(device)
101 9517bf29 Nikos Skalkotos
102 a939e3b8 Nikos Skalkotos
    def _read_fstable(self, f):
103 88f83027 Nikos Skalkotos
        """Use this generator to iterate over the lines of and fstab file"""
104 25b4d858 Nikos Skalkotos
105 8eea5572 Nikos Skalkotos
        if not os.path.isfile(f):
106 8eea5572 Nikos Skalkotos
            raise FatalError("Unable to open: `%s'. File is missing." % f)
107 27a4229d Nikos Skalkotos
108 74149d07 Nikos Skalkotos
        FileSystemTableEntry = namedtuple('FileSystemTableEntry',
109 6228d45e Nikos Skalkotos
                                          'dev mpoint fs opts freq passno')
110 8eea5572 Nikos Skalkotos
        with open(f) as table:
111 8eea5572 Nikos Skalkotos
            for line in iter(table):
112 8eea5572 Nikos Skalkotos
                entry = line.split('#')[0].strip().split()
113 8eea5572 Nikos Skalkotos
                if len(entry) != 6:
114 8eea5572 Nikos Skalkotos
                    continue
115 74149d07 Nikos Skalkotos
                yield FileSystemTableEntry(*entry)
116 27a4229d Nikos Skalkotos
117 a939e3b8 Nikos Skalkotos
    def _get_root_partition(self):
118 88f83027 Nikos Skalkotos
        """Return the fstab entry accosiated with the root filesystem"""
119 8eea5572 Nikos Skalkotos
        for entry in self._read_fstable('/etc/fstab'):
120 8eea5572 Nikos Skalkotos
            if entry.mpoint == '/':
121 8eea5572 Nikos Skalkotos
                return entry.dev
122 27a4229d Nikos Skalkotos
123 8eea5572 Nikos Skalkotos
        raise FatalError("Unable to find root device in /etc/fstab")
124 27a4229d Nikos Skalkotos
125 a939e3b8 Nikos Skalkotos
    def _is_mpoint(self, path):
126 88f83027 Nikos Skalkotos
        """Check if a directory is currently a mount point"""
127 a939e3b8 Nikos Skalkotos
        for entry in self._read_fstable('/proc/mounts'):
128 8eea5572 Nikos Skalkotos
            if entry.mpoint == path:
129 8eea5572 Nikos Skalkotos
                return True
130 8eea5572 Nikos Skalkotos
        return False
131 27a4229d Nikos Skalkotos
132 25b4d858 Nikos Skalkotos
    def _get_mount_options(self, device):
133 88f83027 Nikos Skalkotos
        """Return the mount entry associated with a mounted device"""
134 a939e3b8 Nikos Skalkotos
        for entry in self._read_fstable('/proc/mounts'):
135 8eea5572 Nikos Skalkotos
            if not entry.dev.startswith('/'):
136 8eea5572 Nikos Skalkotos
                continue
137 9517bf29 Nikos Skalkotos
138 8eea5572 Nikos Skalkotos
            if os.path.realpath(entry.dev) == os.path.realpath(device):
139 8eea5572 Nikos Skalkotos
                return entry
140 9517bf29 Nikos Skalkotos
141 25b4d858 Nikos Skalkotos
        return None
142 9517bf29 Nikos Skalkotos
143 25b4d858 Nikos Skalkotos
    def _create_partition_table(self, image):
144 88f83027 Nikos Skalkotos
        """Copy the partition table of the host system into the image"""
145 8eea5572 Nikos Skalkotos
146 8eea5572 Nikos Skalkotos
        # Copy the MBR and the space between the MBR and the first partition.
147 bc9d122f Nikos Skalkotos
        # In msdos partition tables Grub Stage 1.5 is located there.
148 b0376b3f Nikos Skalkotos
        # In gpt partition tables the Primary GPT Header is there.
149 25b4d858 Nikos Skalkotos
        first_sector = self.disk.getPrimaryPartitions()[0].geometry.start
150 8eea5572 Nikos Skalkotos
151 25b4d858 Nikos Skalkotos
        dd('if=%s' % self.disk.device.path, 'of=%s' % image,
152 25b4d858 Nikos Skalkotos
           'bs=%d' % self.disk.device.sectorSize,
153 8eea5572 Nikos Skalkotos
           'count=%d' % first_sector, 'conv=notrunc')
154 8eea5572 Nikos Skalkotos
155 b0376b3f Nikos Skalkotos
        if self.disk.type == 'gpt':
156 b0376b3f Nikos Skalkotos
            # Copy the Secondary GPT Header
157 b0376b3f Nikos Skalkotos
            table = GPTPartitionTable(self.disk.device.path)
158 b0376b3f Nikos Skalkotos
            dd('if=%s' % self.disk.device.path, 'of=%s' % image,
159 37d581b8 Nikos Skalkotos
               'bs=%d' % self.disk.device.sectorSize, 'conv=notrunc',
160 37d581b8 Nikos Skalkotos
               'seek=%d' % table.primary.last_usable_lba,
161 37d581b8 Nikos Skalkotos
               'skip=%d' % table.primary.last_usable_lba)
162 b0376b3f Nikos Skalkotos
163 8eea5572 Nikos Skalkotos
        # Create the Extended boot records (EBRs) in the image
164 25b4d858 Nikos Skalkotos
        extended = self.disk.getExtendedPartition()
165 8eea5572 Nikos Skalkotos
        if not extended:
166 8eea5572 Nikos Skalkotos
            return
167 8eea5572 Nikos Skalkotos
168 8eea5572 Nikos Skalkotos
        # Extended boot records precede the logical partitions they describe
169 25b4d858 Nikos Skalkotos
        logical = self.disk.getLogicalPartitions()
170 8eea5572 Nikos Skalkotos
        start = extended.geometry.start
171 8eea5572 Nikos Skalkotos
        for i in range(len(logical)):
172 8eea5572 Nikos Skalkotos
            end = logical[i].geometry.start - 1
173 25b4d858 Nikos Skalkotos
            dd('if=%s' % self.disk.device.path, 'of=%s' % image,
174 8eea5572 Nikos Skalkotos
               'count=%d' % (end - start + 1), 'conv=notrunc',
175 8eea5572 Nikos Skalkotos
               'seek=%d' % start, 'skip=%d' % start)
176 8eea5572 Nikos Skalkotos
            start = logical[i].geometry.end + 1
177 8eea5572 Nikos Skalkotos
178 25b4d858 Nikos Skalkotos
    def _get_partitions(self, disk):
179 88f83027 Nikos Skalkotos
        """Returns a list with the partitions of the provided disk"""
180 25b4d858 Nikos Skalkotos
        Partition = namedtuple('Partition', 'num start end type fs')
181 8eea5572 Nikos Skalkotos
182 8eea5572 Nikos Skalkotos
        partitions = []
183 25b4d858 Nikos Skalkotos
        for p in disk.partitions:
184 25b4d858 Nikos Skalkotos
            num = p.number
185 25b4d858 Nikos Skalkotos
            start = p.geometry.start
186 25b4d858 Nikos Skalkotos
            end = p.geometry.end
187 25b4d858 Nikos Skalkotos
            ptype = p.type
188 25b4d858 Nikos Skalkotos
            fs = p.fileSystem.type if p.fileSystem is not None else ''
189 25b4d858 Nikos Skalkotos
            partitions.append(Partition(num, start, end, ptype, fs))
190 25b4d858 Nikos Skalkotos
191 25b4d858 Nikos Skalkotos
        return partitions
192 25b4d858 Nikos Skalkotos
193 25b4d858 Nikos Skalkotos
    def _shrink_partitions(self, image):
194 88f83027 Nikos Skalkotos
        """Remove the last partition of the image if it is a swap partition and
195 88f83027 Nikos Skalkotos
        shrink the partition before that. Make sure it can still host all the
196 88f83027 Nikos Skalkotos
        files the corresponding host file system hosts
197 88f83027 Nikos Skalkotos
        """
198 e62eb365 Nikos Skalkotos
        new_end = self.disk.device.length
199 25b4d858 Nikos Skalkotos
200 b0376b3f Nikos Skalkotos
        image_disk = parted.Disk(parted.Device(image))
201 a939e3b8 Nikos Skalkotos
202 a939e3b8 Nikos Skalkotos
        is_extended = lambda p: p.type == parted.PARTITION_EXTENDED
203 a939e3b8 Nikos Skalkotos
        is_logical = lambda p: p.type == parted.PARTITION_LOGICAL
204 a939e3b8 Nikos Skalkotos
205 25b4d858 Nikos Skalkotos
        partitions = self._get_partitions(self.disk)
206 a939e3b8 Nikos Skalkotos
207 a939e3b8 Nikos Skalkotos
        last = partitions[-1]
208 a939e3b8 Nikos Skalkotos
        if last.fs == 'linux-swap(v1)':
209 a939e3b8 Nikos Skalkotos
            MB = 2 ** 20
210 25b4d858 Nikos Skalkotos
            size = (last.end - last.start + 1) * self.disk.device.sectorSize
211 a939e3b8 Nikos Skalkotos
            self.meta['SWAP'] = "%d:%s" % (last.num, (size + MB - 1) // MB)
212 a939e3b8 Nikos Skalkotos
213 a939e3b8 Nikos Skalkotos
            image_disk.deletePartition(
214 a939e3b8 Nikos Skalkotos
                image_disk.getPartitionBySector(last.start))
215 b0376b3f Nikos Skalkotos
            image_disk.commitToDevice()
216 a939e3b8 Nikos Skalkotos
217 a939e3b8 Nikos Skalkotos
            if is_logical(last) and last.num == 5:
218 a939e3b8 Nikos Skalkotos
                extended = image_disk.getExtendedPartition()
219 a939e3b8 Nikos Skalkotos
                image_disk.deletePartition(extended)
220 b0376b3f Nikos Skalkotos
                image_disk.commitToDevice()
221 a939e3b8 Nikos Skalkotos
                partitions.remove(filter(is_extended, partitions)[0])
222 a939e3b8 Nikos Skalkotos
223 a939e3b8 Nikos Skalkotos
            partitions.remove(last)
224 a939e3b8 Nikos Skalkotos
            last = partitions[-1]
225 a939e3b8 Nikos Skalkotos
226 b0376b3f Nikos Skalkotos
            new_end = last.end
227 a939e3b8 Nikos Skalkotos
228 25b4d858 Nikos Skalkotos
        mount_options = self._get_mount_options(
229 6228d45e Nikos Skalkotos
            self.disk.getPartitionBySector(last.start).path)
230 74149d07 Nikos Skalkotos
        if mount_options is not None:
231 25b4d858 Nikos Skalkotos
            stat = os.statvfs(mount_options.mpoint)
232 25b4d858 Nikos Skalkotos
            # Shrink the last partition. The new size should be the size of the
233 25b4d858 Nikos Skalkotos
            # occupied blocks
234 a939e3b8 Nikos Skalkotos
            blcks = stat.f_blocks - stat.f_bavail
235 25b4d858 Nikos Skalkotos
            new_size = (blcks * stat.f_frsize) // self.disk.device.sectorSize
236 a939e3b8 Nikos Skalkotos
237 a939e3b8 Nikos Skalkotos
            # Add 10% just to be on the safe side
238 a939e3b8 Nikos Skalkotos
            part_end = last.start + (new_size * 11) // 10
239 4a35faa5 Nikos Skalkotos
            # Align to 2048
240 a939e3b8 Nikos Skalkotos
            part_end = ((part_end + 2047) // 2048) * 2048
241 a939e3b8 Nikos Skalkotos
242 c631f3e4 Nikos Skalkotos
            # Make sure the partition starts where the old partition started.
243 c631f3e4 Nikos Skalkotos
            constraint = parted.Constraint(device=image_disk.device)
244 c631f3e4 Nikos Skalkotos
            constraint.startRange = parted.Geometry(device=image_disk.device,
245 c631f3e4 Nikos Skalkotos
                                                    start=last.start, length=1)
246 c631f3e4 Nikos Skalkotos
247 a939e3b8 Nikos Skalkotos
            image_disk.setPartitionGeometry(
248 c631f3e4 Nikos Skalkotos
                image_disk.getPartitionBySector(last.start), constraint,
249 83fe59dd Nikos Skalkotos
                start=last.start, end=part_end)
250 b0376b3f Nikos Skalkotos
            image_disk.commitToDevice()
251 a939e3b8 Nikos Skalkotos
252 25b4d858 Nikos Skalkotos
            # Parted may have changed this for better alignment
253 25b4d858 Nikos Skalkotos
            part_end = image_disk.getPartitionBySector(last.start).geometry.end
254 25b4d858 Nikos Skalkotos
            last = last._replace(end=part_end)
255 25b4d858 Nikos Skalkotos
            partitions[-1] = last
256 25b4d858 Nikos Skalkotos
257 b0376b3f Nikos Skalkotos
            new_end = part_end
258 25b4d858 Nikos Skalkotos
259 a939e3b8 Nikos Skalkotos
            if last.type == parted.PARTITION_LOGICAL:
260 a939e3b8 Nikos Skalkotos
                # Fix the extended partition
261 b0376b3f Nikos Skalkotos
                image_disk.minimizeExtendedPartition()
262 8eea5572 Nikos Skalkotos
263 b0376b3f Nikos Skalkotos
        return (new_end, self._get_partitions(image_disk))
264 8eea5572 Nikos Skalkotos
265 25b4d858 Nikos Skalkotos
    def _map_partition(self, dev, num, start, end):
266 88f83027 Nikos Skalkotos
        """Map a partition into a block device using the device mapper"""
267 b0376b3f Nikos Skalkotos
        name = os.path.basename(dev) + "_" + uuid.uuid4().hex
268 25b4d858 Nikos Skalkotos
        tablefd, table = tempfile.mkstemp()
269 25b4d858 Nikos Skalkotos
        try:
270 b6765b7e Nikos Skalkotos
            try:
271 b6765b7e Nikos Skalkotos
                size = end - start + 1
272 b6765b7e Nikos Skalkotos
                os.write(tablefd, "0 %d linear %s %d" % (size, dev, start))
273 b6765b7e Nikos Skalkotos
            finally:
274 b6765b7e Nikos Skalkotos
                os.close(tablefd)
275 25b4d858 Nikos Skalkotos
            dmsetup('create', "%sp%d" % (name, num), table)
276 25b4d858 Nikos Skalkotos
        finally:
277 25b4d858 Nikos Skalkotos
            os.unlink(table)
278 8eea5572 Nikos Skalkotos
279 25b4d858 Nikos Skalkotos
        return "/dev/mapper/%sp%d" % (name, num)
280 8eea5572 Nikos Skalkotos
281 25b4d858 Nikos Skalkotos
    def _unmap_partition(self, dev):
282 88f83027 Nikos Skalkotos
        """Unmap a previously mapped partition"""
283 25b4d858 Nikos Skalkotos
        if not os.path.exists(dev):
284 25b4d858 Nikos Skalkotos
            return
285 8eea5572 Nikos Skalkotos
286 f3845095 Nikos Skalkotos
        try_fail_repeat(dmsetup, 'remove', dev.split('/dev/mapper/')[1])
287 8eea5572 Nikos Skalkotos
288 25b4d858 Nikos Skalkotos
    def _mount(self, target, devs):
289 88f83027 Nikos Skalkotos
        """Mount a list of filesystems in mountpoints relative to target"""
290 25b4d858 Nikos Skalkotos
        devs.sort(key=lambda d: d[1])
291 81a63274 Nikos Skalkotos
        for dev, mpoint, options in devs:
292 25b4d858 Nikos Skalkotos
            absmpoint = os.path.abspath(target + mpoint)
293 25b4d858 Nikos Skalkotos
            if not os.path.exists(absmpoint):
294 25b4d858 Nikos Skalkotos
                os.makedirs(absmpoint)
295 81a63274 Nikos Skalkotos
296 81a63274 Nikos Skalkotos
            if len(options) > 0:
297 81a63274 Nikos Skalkotos
                mount(dev, absmpoint, '-o', ",".join(options))
298 81a63274 Nikos Skalkotos
            else:
299 81a63274 Nikos Skalkotos
                mount(dev, absmpoint)
300 25b4d858 Nikos Skalkotos
301 25b4d858 Nikos Skalkotos
    def _umount_all(self, target):
302 88f83027 Nikos Skalkotos
        """Unmount all filesystems that are mounted under the directory target
303 88f83027 Nikos Skalkotos
        """
304 25b4d858 Nikos Skalkotos
        mpoints = []
305 25b4d858 Nikos Skalkotos
        for entry in self._read_fstable('/proc/mounts'):
306 25b4d858 Nikos Skalkotos
            if entry.mpoint.startswith(os.path.abspath(target)):
307 25b4d858 Nikos Skalkotos
                    mpoints.append(entry.mpoint)
308 74149d07 Nikos Skalkotos
309 25b4d858 Nikos Skalkotos
        mpoints.sort()
310 25b4d858 Nikos Skalkotos
        for mpoint in reversed(mpoints):
311 f3845095 Nikos Skalkotos
            try_fail_repeat(umount, mpoint)
312 25b4d858 Nikos Skalkotos
313 74149d07 Nikos Skalkotos
    def _to_exclude(self):
314 88f83027 Nikos Skalkotos
        """Find which directories to exclude during the image copy. This is
315 88f83027 Nikos Skalkotos
        accompliced by checking which directories serve as mount points for
316 88f83027 Nikos Skalkotos
        virtual file systems
317 88f83027 Nikos Skalkotos
        """
318 bf15a033 Nikos Skalkotos
        excluded = ['/tmp', '/var/tmp']
319 c16850ae Nikos Skalkotos
        if self.tmp is not None:
320 c16850ae Nikos Skalkotos
            excluded.append(self.tmp)
321 74149d07 Nikos Skalkotos
        local_filesystems = MKFS_OPTS.keys() + ['rootfs']
322 74149d07 Nikos Skalkotos
        for entry in self._read_fstable('/proc/mounts'):
323 74149d07 Nikos Skalkotos
            if entry.fs in local_filesystems:
324 74149d07 Nikos Skalkotos
                continue
325 74149d07 Nikos Skalkotos
326 74149d07 Nikos Skalkotos
            mpoint = entry.mpoint
327 74149d07 Nikos Skalkotos
            if mpoint in excluded:
328 74149d07 Nikos Skalkotos
                continue
329 74149d07 Nikos Skalkotos
330 6228d45e Nikos Skalkotos
            descendants = filter(
331 6228d45e Nikos Skalkotos
                lambda p: p.startswith(mpoint + '/'), excluded)
332 74149d07 Nikos Skalkotos
            if len(descendants):
333 74149d07 Nikos Skalkotos
                for d in descendants:
334 74149d07 Nikos Skalkotos
                    excluded.remove(d)
335 74149d07 Nikos Skalkotos
                excluded.append(mpoint)
336 74149d07 Nikos Skalkotos
                continue
337 74149d07 Nikos Skalkotos
338 74149d07 Nikos Skalkotos
            dirname = mpoint
339 74149d07 Nikos Skalkotos
            basename = ''
340 74149d07 Nikos Skalkotos
            found_ancestor = False
341 74149d07 Nikos Skalkotos
            while dirname != '/':
342 74149d07 Nikos Skalkotos
                (dirname, basename) = os.path.split(dirname)
343 74149d07 Nikos Skalkotos
                if dirname in excluded:
344 74149d07 Nikos Skalkotos
                    found_ancestor = True
345 74149d07 Nikos Skalkotos
                    break
346 74149d07 Nikos Skalkotos
347 74149d07 Nikos Skalkotos
            if not found_ancestor:
348 74149d07 Nikos Skalkotos
                excluded.append(mpoint)
349 74149d07 Nikos Skalkotos
350 252b2b5d Nikos Skalkotos
        return excluded
351 74149d07 Nikos Skalkotos
352 bf15a033 Nikos Skalkotos
    def _replace_uuids(self, target, new_uuid):
353 88f83027 Nikos Skalkotos
        """Replace UUID references in various files. This is needed after
354 88f83027 Nikos Skalkotos
        copying system files of the host into a new filesystem
355 88f83027 Nikos Skalkotos
        """
356 bf15a033 Nikos Skalkotos
357 bf15a033 Nikos Skalkotos
        files = ['/etc/fstab',
358 bf15a033 Nikos Skalkotos
                 '/boot/grub/grub.cfg',
359 bf15a033 Nikos Skalkotos
                 '/boot/grub/menu.lst',
360 bf15a033 Nikos Skalkotos
                 '/boot/grub/grub.conf']
361 bf15a033 Nikos Skalkotos
362 b0376b3f Nikos Skalkotos
        orig = {}
363 b0376b3f Nikos Skalkotos
        for p in self.disk.partitions:
364 b0376b3f Nikos Skalkotos
            if p.number in new_uuid.keys():
365 b0376b3f Nikos Skalkotos
                orig[p.number] = \
366 b0376b3f Nikos Skalkotos
                    blkid('-s', 'UUID', '-o', 'value', p.path).stdout.strip()
367 bf15a033 Nikos Skalkotos
368 bf15a033 Nikos Skalkotos
        for f in map(lambda f: target + f, files):
369 bf15a033 Nikos Skalkotos
            if not os.path.exists(f):
370 bf15a033 Nikos Skalkotos
                continue
371 bf15a033 Nikos Skalkotos
372 bf15a033 Nikos Skalkotos
            with open(f, 'r') as src:
373 bf15a033 Nikos Skalkotos
                lines = src.readlines()
374 bf15a033 Nikos Skalkotos
            with open(f, 'w') as dest:
375 bf15a033 Nikos Skalkotos
                for line in lines:
376 bf15a033 Nikos Skalkotos
                    for i, uuid in new_uuid.items():
377 bf15a033 Nikos Skalkotos
                        line = re.sub(orig[i], uuid, line)
378 bf15a033 Nikos Skalkotos
                    dest.write(line)
379 bf15a033 Nikos Skalkotos
380 b0376b3f Nikos Skalkotos
    def _create_filesystems(self, image, partitions):
381 88f83027 Nikos Skalkotos
        """Fill the image with data. Host file systems that are not currently
382 88f83027 Nikos Skalkotos
        mounted are binary copied into the image. For mounted file systems, a
383 88f83027 Nikos Skalkotos
        file system level copy is performed.
384 88f83027 Nikos Skalkotos
        """
385 74149d07 Nikos Skalkotos
386 bf15a033 Nikos Skalkotos
        filesystem = {}
387 81a63274 Nikos Skalkotos
        orig_dev = {}
388 25b4d858 Nikos Skalkotos
        for p in self.disk.partitions:
389 bf15a033 Nikos Skalkotos
            filesystem[p.number] = self._get_mount_options(p.path)
390 81a63274 Nikos Skalkotos
            orig_dev[p.number] = p.path
391 25b4d858 Nikos Skalkotos
392 bf15a033 Nikos Skalkotos
        unmounted = filter(lambda p: filesystem[p.num] is None, partitions)
393 bf15a033 Nikos Skalkotos
        mounted = filter(lambda p: filesystem[p.num] is not None, partitions)
394 25b4d858 Nikos Skalkotos
395 25b4d858 Nikos Skalkotos
        # For partitions that are not mounted right now, we can simply dd them
396 25b4d858 Nikos Skalkotos
        # into the image.
397 25b4d858 Nikos Skalkotos
        for p in unmounted:
398 10a5c2bf Nikos Skalkotos
            self.out.output('Cloning partition %d ... ' % p.num, False)
399 25b4d858 Nikos Skalkotos
            dd('if=%s' % self.disk.device.path, 'of=%s' % image,
400 25b4d858 Nikos Skalkotos
               'count=%d' % (p.end - p.start + 1), 'conv=notrunc',
401 25b4d858 Nikos Skalkotos
               'seek=%d' % p.start, 'skip=%d' % p.start)
402 10a5c2bf Nikos Skalkotos
            self.out.success("done")
403 25b4d858 Nikos Skalkotos
404 25b4d858 Nikos Skalkotos
        loop = str(losetup('-f', '--show', image)).strip()
405 81a63274 Nikos Skalkotos
406 81a63274 Nikos Skalkotos
        # Recreate mounted file systems
407 25b4d858 Nikos Skalkotos
        mapped = {}
408 25b4d858 Nikos Skalkotos
        try:
409 25b4d858 Nikos Skalkotos
            for p in mounted:
410 74149d07 Nikos Skalkotos
                i = p.num
411 25b4d858 Nikos Skalkotos
                mapped[i] = self._map_partition(loop, i, p.start, p.end)
412 25b4d858 Nikos Skalkotos
413 bf15a033 Nikos Skalkotos
            new_uuid = {}
414 25b4d858 Nikos Skalkotos
            # Create the file systems
415 25b4d858 Nikos Skalkotos
            for i, dev in mapped.iteritems():
416 bf15a033 Nikos Skalkotos
                fs = filesystem[i].fs
417 25b4d858 Nikos Skalkotos
                self.out.output('Creating %s filesystem on partition %d ... ' %
418 6228d45e Nikos Skalkotos
                                (fs, i), False)
419 25b4d858 Nikos Skalkotos
                get_command('mkfs.%s' % fs)(*(MKFS_OPTS[fs] + [dev]))
420 81a63274 Nikos Skalkotos
421 81a63274 Nikos Skalkotos
                # For ext[234] enable the default mount options
422 81a63274 Nikos Skalkotos
                if re.match('^ext[234]$', fs):
423 81a63274 Nikos Skalkotos
                    mopts = filter(
424 81a63274 Nikos Skalkotos
                        lambda p: p.startswith('Default mount options:'),
425 81a63274 Nikos Skalkotos
                        tune2fs('-l', orig_dev[i]).splitlines()
426 35b13de5 Nikos Skalkotos
                    )[0].split(':')[1].strip().split()
427 81a63274 Nikos Skalkotos
428 81a63274 Nikos Skalkotos
                    if not (len(mopts) == 1 and mopts[0] == '(none)'):
429 81a63274 Nikos Skalkotos
                        for opt in mopts:
430 81a63274 Nikos Skalkotos
                            tune2fs('-o', opt, dev)
431 81a63274 Nikos Skalkotos
432 25b4d858 Nikos Skalkotos
                self.out.success('done')
433 6228d45e Nikos Skalkotos
                new_uuid[i] = blkid(
434 6228d45e Nikos Skalkotos
                    '-s', 'UUID', '-o', 'value', dev).stdout.strip()
435 25b4d858 Nikos Skalkotos
436 25b4d858 Nikos Skalkotos
            target = tempfile.mkdtemp()
437 81a63274 Nikos Skalkotos
            devs = []
438 81a63274 Nikos Skalkotos
            for i in mapped.keys():
439 81a63274 Nikos Skalkotos
                fs = filesystem[i].fs
440 81a63274 Nikos Skalkotos
                mpoint = filesystem[i].mpoint
441 81a63274 Nikos Skalkotos
                opts = []
442 81a63274 Nikos Skalkotos
                for opt in filesystem[i].opts.split(','):
443 81a63274 Nikos Skalkotos
                    if opt in ('acl', 'user_xattr'):
444 81a63274 Nikos Skalkotos
                        opts.append(opt)
445 81a63274 Nikos Skalkotos
                devs.append((mapped[i], mpoint, opts))
446 25b4d858 Nikos Skalkotos
            try:
447 81a63274 Nikos Skalkotos
                self._mount(target, devs)
448 61d14323 Nikos Skalkotos
449 252b2b5d Nikos Skalkotos
                excluded = self._to_exclude()
450 1fa75c4c Nikos Skalkotos
451 1fa75c4c Nikos Skalkotos
                rsync = Rsync(self.out)
452 1fa75c4c Nikos Skalkotos
453 dd22ce7b Nikos Skalkotos
                for excl in excluded + [image]:
454 1fa75c4c Nikos Skalkotos
                    rsync.exclude(excl)
455 1fa75c4c Nikos Skalkotos
456 1fa75c4c Nikos Skalkotos
                rsync.archive().hard_links().xattrs().sparse().acls()
457 f2ddf4db Nikos Skalkotos
                rsync.run('/', target, 'host', 'temporary image')
458 25b4d858 Nikos Skalkotos
459 252b2b5d Nikos Skalkotos
                # Create missing mountpoints. Since they are mountpoints, we
460 252b2b5d Nikos Skalkotos
                # cannot determine the ownership and the mode of the real
461 252b2b5d Nikos Skalkotos
                # directory. Make them inherit those properties from their
462 252b2b5d Nikos Skalkotos
                # parent dir
463 252b2b5d Nikos Skalkotos
                for excl in excluded:
464 10a5c2bf Nikos Skalkotos
                    dirname = os.path.dirname(excl)
465 10a5c2bf Nikos Skalkotos
                    stat = os.stat(dirname)
466 41bf4a8f Nikos Skalkotos
                    os.mkdir(target + excl)
467 41bf4a8f Nikos Skalkotos
                    os.chmod(target + excl, stat.st_mode)
468 10a5c2bf Nikos Skalkotos
                    os.chown(target + excl, stat.st_uid, stat.st_gid)
469 252b2b5d Nikos Skalkotos
470 41bf4a8f Nikos Skalkotos
                # /tmp and /var/tmp are special cases. We exclude then even if
471 41bf4a8f Nikos Skalkotos
                # they aren't mountpoints. Restore their permissions.
472 41bf4a8f Nikos Skalkotos
                for excl in ('/tmp', '/var/tmp'):
473 41bf4a8f Nikos Skalkotos
                    if self._is_mpoint(excl):
474 41bf4a8f Nikos Skalkotos
                        os.chmod(target + excl, 041777)
475 41bf4a8f Nikos Skalkotos
                        os.chown(target + excl, 0, 0)
476 41bf4a8f Nikos Skalkotos
                    else:
477 41bf4a8f Nikos Skalkotos
                        stat = os.stat(excl)
478 41bf4a8f Nikos Skalkotos
                        os.chmod(target + excl, stat.st_mode)
479 41bf4a8f Nikos Skalkotos
                        os.chown(target + excl, stat.st_uid, stat.st_gid)
480 41bf4a8f Nikos Skalkotos
481 6228d45e Nikos Skalkotos
                # We need to replace the old UUID referencies with the new
482 6228d45e Nikos Skalkotos
                # ones in grub configuration files and /etc/fstab for file
483 6228d45e Nikos Skalkotos
                # systems that have been recreated.
484 bf15a033 Nikos Skalkotos
                self._replace_uuids(target, new_uuid)
485 bf15a033 Nikos Skalkotos
486 25b4d858 Nikos Skalkotos
            finally:
487 25b4d858 Nikos Skalkotos
                self._umount_all(target)
488 25b4d858 Nikos Skalkotos
                os.rmdir(target)
489 25b4d858 Nikos Skalkotos
        finally:
490 25b4d858 Nikos Skalkotos
            for dev in mapped.values():
491 25b4d858 Nikos Skalkotos
                self._unmap_partition(dev)
492 25b4d858 Nikos Skalkotos
            losetup('-d', loop)
493 25b4d858 Nikos Skalkotos
494 567891a6 Nikos Skalkotos
    def create_image(self, image):
495 f3845095 Nikos Skalkotos
        """Given an image filename, this method will create an image out of the
496 f3845095 Nikos Skalkotos
        running system.
497 f3845095 Nikos Skalkotos
        """
498 a939e3b8 Nikos Skalkotos
499 e62eb365 Nikos Skalkotos
        size = self.disk.device.length * self.disk.device.sectorSize
500 a939e3b8 Nikos Skalkotos
501 a939e3b8 Nikos Skalkotos
        # Create sparse file to host the image
502 567891a6 Nikos Skalkotos
        fd = os.open(image, os.O_WRONLY | os.O_CREAT)
503 567891a6 Nikos Skalkotos
        try:
504 567891a6 Nikos Skalkotos
            os.ftruncate(fd, size)
505 567891a6 Nikos Skalkotos
        finally:
506 567891a6 Nikos Skalkotos
            os.close(fd)
507 25b4d858 Nikos Skalkotos
508 25b4d858 Nikos Skalkotos
        self._create_partition_table(image)
509 b0376b3f Nikos Skalkotos
        end_sector, partitions = self._shrink_partitions(image)
510 b0376b3f Nikos Skalkotos
511 b0376b3f Nikos Skalkotos
        if self.disk.type == 'gpt':
512 b0376b3f Nikos Skalkotos
            old_size = size
513 b0376b3f Nikos Skalkotos
            size = (end_sector + 1) * self.disk.device.sectorSize
514 b0376b3f Nikos Skalkotos
            ptable = GPTPartitionTable(image)
515 b0376b3f Nikos Skalkotos
            size = ptable.shrink(size, old_size)
516 b0376b3f Nikos Skalkotos
        else:
517 b0376b3f Nikos Skalkotos
            # Alighn to 2048
518 b0376b3f Nikos Skalkotos
            end_sector = ((end_sector + 2047) // 2048) * 2048
519 b0376b3f Nikos Skalkotos
            size = (end_sector + 1) * self.disk.device.sectorSize
520 fa39c7e3 Nikos Skalkotos
521 fa39c7e3 Nikos Skalkotos
        # Truncate image to the new size.
522 fa39c7e3 Nikos Skalkotos
        fd = os.open(image, os.O_RDWR)
523 fa39c7e3 Nikos Skalkotos
        try:
524 fa39c7e3 Nikos Skalkotos
            os.ftruncate(fd, size)
525 fa39c7e3 Nikos Skalkotos
        finally:
526 fa39c7e3 Nikos Skalkotos
            os.close(fd)
527 fa39c7e3 Nikos Skalkotos
528 25b4d858 Nikos Skalkotos
        # Check if the available space is enough to host the image
529 25b4d858 Nikos Skalkotos
        dirname = os.path.dirname(image)
530 1294501a Nikos Skalkotos
        self.out.output("Examining available space ...", False)
531 c16850ae Nikos Skalkotos
        if free_space(dirname) <= size:
532 dd22ce7b Nikos Skalkotos
            raise FatalError("Not enough space under %s to host the temporary "
533 dd22ce7b Nikos Skalkotos
                             "image" % dirname)
534 25b4d858 Nikos Skalkotos
        self.out.success("sufficient")
535 8eea5572 Nikos Skalkotos
536 b0376b3f Nikos Skalkotos
        self._create_filesystems(image, partitions)
537 8eea5572 Nikos Skalkotos
538 25b4d858 Nikos Skalkotos
        return image
539 9e4b4de2 Nikos Skalkotos
540 9e4b4de2 Nikos Skalkotos
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :