Statistics
| Branch: | Tag: | Revision:

root / image_creator / bundle_volume.py @ c71f38be

History | View | Annotate | Download (19.8 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 25b4d858 Nikos Skalkotos
            size = end - start + 1
271 25b4d858 Nikos Skalkotos
            os.write(tablefd, "0 %d linear %s %d" % (size, dev, start))
272 25b4d858 Nikos Skalkotos
            dmsetup('create', "%sp%d" % (name, num), table)
273 25b4d858 Nikos Skalkotos
        finally:
274 25b4d858 Nikos Skalkotos
            os.unlink(table)
275 8eea5572 Nikos Skalkotos
276 25b4d858 Nikos Skalkotos
        return "/dev/mapper/%sp%d" % (name, num)
277 8eea5572 Nikos Skalkotos
278 25b4d858 Nikos Skalkotos
    def _unmap_partition(self, dev):
279 88f83027 Nikos Skalkotos
        """Unmap a previously mapped partition"""
280 25b4d858 Nikos Skalkotos
        if not os.path.exists(dev):
281 25b4d858 Nikos Skalkotos
            return
282 8eea5572 Nikos Skalkotos
283 f3845095 Nikos Skalkotos
        try_fail_repeat(dmsetup, 'remove', dev.split('/dev/mapper/')[1])
284 8eea5572 Nikos Skalkotos
285 25b4d858 Nikos Skalkotos
    def _mount(self, target, devs):
286 88f83027 Nikos Skalkotos
        """Mount a list of filesystems in mountpoints relative to target"""
287 25b4d858 Nikos Skalkotos
        devs.sort(key=lambda d: d[1])
288 81a63274 Nikos Skalkotos
        for dev, mpoint, options in devs:
289 25b4d858 Nikos Skalkotos
            absmpoint = os.path.abspath(target + mpoint)
290 25b4d858 Nikos Skalkotos
            if not os.path.exists(absmpoint):
291 25b4d858 Nikos Skalkotos
                os.makedirs(absmpoint)
292 81a63274 Nikos Skalkotos
293 81a63274 Nikos Skalkotos
            if len(options) > 0:
294 81a63274 Nikos Skalkotos
                mount(dev, absmpoint, '-o', ",".join(options))
295 81a63274 Nikos Skalkotos
            else:
296 81a63274 Nikos Skalkotos
                mount(dev, absmpoint)
297 25b4d858 Nikos Skalkotos
298 25b4d858 Nikos Skalkotos
    def _umount_all(self, target):
299 88f83027 Nikos Skalkotos
        """Unmount all filesystems that are mounted under the directory target
300 88f83027 Nikos Skalkotos
        """
301 25b4d858 Nikos Skalkotos
        mpoints = []
302 25b4d858 Nikos Skalkotos
        for entry in self._read_fstable('/proc/mounts'):
303 25b4d858 Nikos Skalkotos
            if entry.mpoint.startswith(os.path.abspath(target)):
304 25b4d858 Nikos Skalkotos
                    mpoints.append(entry.mpoint)
305 74149d07 Nikos Skalkotos
306 25b4d858 Nikos Skalkotos
        mpoints.sort()
307 25b4d858 Nikos Skalkotos
        for mpoint in reversed(mpoints):
308 f3845095 Nikos Skalkotos
            try_fail_repeat(umount, mpoint)
309 25b4d858 Nikos Skalkotos
310 74149d07 Nikos Skalkotos
    def _to_exclude(self):
311 88f83027 Nikos Skalkotos
        """Find which directories to exclude during the image copy. This is
312 88f83027 Nikos Skalkotos
        accompliced by checking which directories serve as mount points for
313 88f83027 Nikos Skalkotos
        virtual file systems
314 88f83027 Nikos Skalkotos
        """
315 bf15a033 Nikos Skalkotos
        excluded = ['/tmp', '/var/tmp']
316 c16850ae Nikos Skalkotos
        if self.tmp is not None:
317 c16850ae Nikos Skalkotos
            excluded.append(self.tmp)
318 74149d07 Nikos Skalkotos
        local_filesystems = MKFS_OPTS.keys() + ['rootfs']
319 74149d07 Nikos Skalkotos
        for entry in self._read_fstable('/proc/mounts'):
320 74149d07 Nikos Skalkotos
            if entry.fs in local_filesystems:
321 74149d07 Nikos Skalkotos
                continue
322 74149d07 Nikos Skalkotos
323 74149d07 Nikos Skalkotos
            mpoint = entry.mpoint
324 74149d07 Nikos Skalkotos
            if mpoint in excluded:
325 74149d07 Nikos Skalkotos
                continue
326 74149d07 Nikos Skalkotos
327 6228d45e Nikos Skalkotos
            descendants = filter(
328 6228d45e Nikos Skalkotos
                lambda p: p.startswith(mpoint + '/'), excluded)
329 74149d07 Nikos Skalkotos
            if len(descendants):
330 74149d07 Nikos Skalkotos
                for d in descendants:
331 74149d07 Nikos Skalkotos
                    excluded.remove(d)
332 74149d07 Nikos Skalkotos
                excluded.append(mpoint)
333 74149d07 Nikos Skalkotos
                continue
334 74149d07 Nikos Skalkotos
335 74149d07 Nikos Skalkotos
            dirname = mpoint
336 74149d07 Nikos Skalkotos
            basename = ''
337 74149d07 Nikos Skalkotos
            found_ancestor = False
338 74149d07 Nikos Skalkotos
            while dirname != '/':
339 74149d07 Nikos Skalkotos
                (dirname, basename) = os.path.split(dirname)
340 74149d07 Nikos Skalkotos
                if dirname in excluded:
341 74149d07 Nikos Skalkotos
                    found_ancestor = True
342 74149d07 Nikos Skalkotos
                    break
343 74149d07 Nikos Skalkotos
344 74149d07 Nikos Skalkotos
            if not found_ancestor:
345 74149d07 Nikos Skalkotos
                excluded.append(mpoint)
346 74149d07 Nikos Skalkotos
347 252b2b5d Nikos Skalkotos
        return excluded
348 74149d07 Nikos Skalkotos
349 bf15a033 Nikos Skalkotos
    def _replace_uuids(self, target, new_uuid):
350 88f83027 Nikos Skalkotos
        """Replace UUID references in various files. This is needed after
351 88f83027 Nikos Skalkotos
        copying system files of the host into a new filesystem
352 88f83027 Nikos Skalkotos
        """
353 bf15a033 Nikos Skalkotos
354 bf15a033 Nikos Skalkotos
        files = ['/etc/fstab',
355 bf15a033 Nikos Skalkotos
                 '/boot/grub/grub.cfg',
356 bf15a033 Nikos Skalkotos
                 '/boot/grub/menu.lst',
357 bf15a033 Nikos Skalkotos
                 '/boot/grub/grub.conf']
358 bf15a033 Nikos Skalkotos
359 b0376b3f Nikos Skalkotos
        orig = {}
360 b0376b3f Nikos Skalkotos
        for p in self.disk.partitions:
361 b0376b3f Nikos Skalkotos
            if p.number in new_uuid.keys():
362 b0376b3f Nikos Skalkotos
                orig[p.number] = \
363 b0376b3f Nikos Skalkotos
                    blkid('-s', 'UUID', '-o', 'value', p.path).stdout.strip()
364 bf15a033 Nikos Skalkotos
365 bf15a033 Nikos Skalkotos
        for f in map(lambda f: target + f, files):
366 bf15a033 Nikos Skalkotos
            if not os.path.exists(f):
367 bf15a033 Nikos Skalkotos
                continue
368 bf15a033 Nikos Skalkotos
369 bf15a033 Nikos Skalkotos
            with open(f, 'r') as src:
370 bf15a033 Nikos Skalkotos
                lines = src.readlines()
371 bf15a033 Nikos Skalkotos
            with open(f, 'w') as dest:
372 bf15a033 Nikos Skalkotos
                for line in lines:
373 bf15a033 Nikos Skalkotos
                    for i, uuid in new_uuid.items():
374 bf15a033 Nikos Skalkotos
                        line = re.sub(orig[i], uuid, line)
375 bf15a033 Nikos Skalkotos
                    dest.write(line)
376 bf15a033 Nikos Skalkotos
377 b0376b3f Nikos Skalkotos
    def _create_filesystems(self, image, partitions):
378 88f83027 Nikos Skalkotos
        """Fill the image with data. Host file systems that are not currently
379 88f83027 Nikos Skalkotos
        mounted are binary copied into the image. For mounted file systems, a
380 88f83027 Nikos Skalkotos
        file system level copy is performed.
381 88f83027 Nikos Skalkotos
        """
382 74149d07 Nikos Skalkotos
383 bf15a033 Nikos Skalkotos
        filesystem = {}
384 81a63274 Nikos Skalkotos
        orig_dev = {}
385 25b4d858 Nikos Skalkotos
        for p in self.disk.partitions:
386 bf15a033 Nikos Skalkotos
            filesystem[p.number] = self._get_mount_options(p.path)
387 81a63274 Nikos Skalkotos
            orig_dev[p.number] = p.path
388 25b4d858 Nikos Skalkotos
389 bf15a033 Nikos Skalkotos
        unmounted = filter(lambda p: filesystem[p.num] is None, partitions)
390 bf15a033 Nikos Skalkotos
        mounted = filter(lambda p: filesystem[p.num] is not None, partitions)
391 25b4d858 Nikos Skalkotos
392 25b4d858 Nikos Skalkotos
        # For partitions that are not mounted right now, we can simply dd them
393 25b4d858 Nikos Skalkotos
        # into the image.
394 25b4d858 Nikos Skalkotos
        for p in unmounted:
395 10a5c2bf Nikos Skalkotos
            self.out.output('Cloning partition %d ... ' % p.num, False)
396 25b4d858 Nikos Skalkotos
            dd('if=%s' % self.disk.device.path, 'of=%s' % image,
397 25b4d858 Nikos Skalkotos
               'count=%d' % (p.end - p.start + 1), 'conv=notrunc',
398 25b4d858 Nikos Skalkotos
               'seek=%d' % p.start, 'skip=%d' % p.start)
399 10a5c2bf Nikos Skalkotos
            self.out.success("done")
400 25b4d858 Nikos Skalkotos
401 25b4d858 Nikos Skalkotos
        loop = str(losetup('-f', '--show', image)).strip()
402 81a63274 Nikos Skalkotos
403 81a63274 Nikos Skalkotos
        # Recreate mounted file systems
404 25b4d858 Nikos Skalkotos
        mapped = {}
405 25b4d858 Nikos Skalkotos
        try:
406 25b4d858 Nikos Skalkotos
            for p in mounted:
407 74149d07 Nikos Skalkotos
                i = p.num
408 25b4d858 Nikos Skalkotos
                mapped[i] = self._map_partition(loop, i, p.start, p.end)
409 25b4d858 Nikos Skalkotos
410 bf15a033 Nikos Skalkotos
            new_uuid = {}
411 25b4d858 Nikos Skalkotos
            # Create the file systems
412 25b4d858 Nikos Skalkotos
            for i, dev in mapped.iteritems():
413 bf15a033 Nikos Skalkotos
                fs = filesystem[i].fs
414 25b4d858 Nikos Skalkotos
                self.out.output('Creating %s filesystem on partition %d ... ' %
415 6228d45e Nikos Skalkotos
                                (fs, i), False)
416 25b4d858 Nikos Skalkotos
                get_command('mkfs.%s' % fs)(*(MKFS_OPTS[fs] + [dev]))
417 81a63274 Nikos Skalkotos
418 81a63274 Nikos Skalkotos
                # For ext[234] enable the default mount options
419 81a63274 Nikos Skalkotos
                if re.match('^ext[234]$', fs):
420 81a63274 Nikos Skalkotos
                    mopts = filter(
421 81a63274 Nikos Skalkotos
                        lambda p: p.startswith('Default mount options:'),
422 81a63274 Nikos Skalkotos
                        tune2fs('-l', orig_dev[i]).splitlines()
423 35b13de5 Nikos Skalkotos
                    )[0].split(':')[1].strip().split()
424 81a63274 Nikos Skalkotos
425 81a63274 Nikos Skalkotos
                    if not (len(mopts) == 1 and mopts[0] == '(none)'):
426 81a63274 Nikos Skalkotos
                        for opt in mopts:
427 81a63274 Nikos Skalkotos
                            tune2fs('-o', opt, dev)
428 81a63274 Nikos Skalkotos
429 25b4d858 Nikos Skalkotos
                self.out.success('done')
430 6228d45e Nikos Skalkotos
                new_uuid[i] = blkid(
431 6228d45e Nikos Skalkotos
                    '-s', 'UUID', '-o', 'value', dev).stdout.strip()
432 25b4d858 Nikos Skalkotos
433 25b4d858 Nikos Skalkotos
            target = tempfile.mkdtemp()
434 81a63274 Nikos Skalkotos
            devs = []
435 81a63274 Nikos Skalkotos
            for i in mapped.keys():
436 81a63274 Nikos Skalkotos
                fs = filesystem[i].fs
437 81a63274 Nikos Skalkotos
                mpoint = filesystem[i].mpoint
438 81a63274 Nikos Skalkotos
                opts = []
439 81a63274 Nikos Skalkotos
                for opt in filesystem[i].opts.split(','):
440 81a63274 Nikos Skalkotos
                    if opt in ('acl', 'user_xattr'):
441 81a63274 Nikos Skalkotos
                        opts.append(opt)
442 81a63274 Nikos Skalkotos
                devs.append((mapped[i], mpoint, opts))
443 25b4d858 Nikos Skalkotos
            try:
444 81a63274 Nikos Skalkotos
                self._mount(target, devs)
445 61d14323 Nikos Skalkotos
446 252b2b5d Nikos Skalkotos
                excluded = self._to_exclude()
447 1fa75c4c Nikos Skalkotos
448 1fa75c4c Nikos Skalkotos
                rsync = Rsync(self.out)
449 1fa75c4c Nikos Skalkotos
450 dd22ce7b Nikos Skalkotos
                for excl in excluded + [image]:
451 1fa75c4c Nikos Skalkotos
                    rsync.exclude(excl)
452 1fa75c4c Nikos Skalkotos
453 1fa75c4c Nikos Skalkotos
                rsync.archive().hard_links().xattrs().sparse().acls()
454 f2ddf4db Nikos Skalkotos
                rsync.run('/', target, 'host', 'temporary image')
455 25b4d858 Nikos Skalkotos
456 252b2b5d Nikos Skalkotos
                # Create missing mountpoints. Since they are mountpoints, we
457 252b2b5d Nikos Skalkotos
                # cannot determine the ownership and the mode of the real
458 252b2b5d Nikos Skalkotos
                # directory. Make them inherit those properties from their
459 252b2b5d Nikos Skalkotos
                # parent dir
460 252b2b5d Nikos Skalkotos
                for excl in excluded:
461 10a5c2bf Nikos Skalkotos
                    dirname = os.path.dirname(excl)
462 10a5c2bf Nikos Skalkotos
                    stat = os.stat(dirname)
463 41bf4a8f Nikos Skalkotos
                    os.mkdir(target + excl)
464 41bf4a8f Nikos Skalkotos
                    os.chmod(target + excl, stat.st_mode)
465 10a5c2bf Nikos Skalkotos
                    os.chown(target + excl, stat.st_uid, stat.st_gid)
466 252b2b5d Nikos Skalkotos
467 41bf4a8f Nikos Skalkotos
                # /tmp and /var/tmp are special cases. We exclude then even if
468 41bf4a8f Nikos Skalkotos
                # they aren't mountpoints. Restore their permissions.
469 41bf4a8f Nikos Skalkotos
                for excl in ('/tmp', '/var/tmp'):
470 41bf4a8f Nikos Skalkotos
                    if self._is_mpoint(excl):
471 41bf4a8f Nikos Skalkotos
                        os.chmod(target + excl, 041777)
472 41bf4a8f Nikos Skalkotos
                        os.chown(target + excl, 0, 0)
473 41bf4a8f Nikos Skalkotos
                    else:
474 41bf4a8f Nikos Skalkotos
                        stat = os.stat(excl)
475 41bf4a8f Nikos Skalkotos
                        os.chmod(target + excl, stat.st_mode)
476 41bf4a8f Nikos Skalkotos
                        os.chown(target + excl, stat.st_uid, stat.st_gid)
477 41bf4a8f Nikos Skalkotos
478 6228d45e Nikos Skalkotos
                # We need to replace the old UUID referencies with the new
479 6228d45e Nikos Skalkotos
                # ones in grub configuration files and /etc/fstab for file
480 6228d45e Nikos Skalkotos
                # systems that have been recreated.
481 bf15a033 Nikos Skalkotos
                self._replace_uuids(target, new_uuid)
482 bf15a033 Nikos Skalkotos
483 25b4d858 Nikos Skalkotos
            finally:
484 25b4d858 Nikos Skalkotos
                self._umount_all(target)
485 25b4d858 Nikos Skalkotos
                os.rmdir(target)
486 25b4d858 Nikos Skalkotos
        finally:
487 25b4d858 Nikos Skalkotos
            for dev in mapped.values():
488 25b4d858 Nikos Skalkotos
                self._unmap_partition(dev)
489 25b4d858 Nikos Skalkotos
            losetup('-d', loop)
490 25b4d858 Nikos Skalkotos
491 567891a6 Nikos Skalkotos
    def create_image(self, image):
492 f3845095 Nikos Skalkotos
        """Given an image filename, this method will create an image out of the
493 f3845095 Nikos Skalkotos
        running system.
494 f3845095 Nikos Skalkotos
        """
495 a939e3b8 Nikos Skalkotos
496 e62eb365 Nikos Skalkotos
        size = self.disk.device.length * self.disk.device.sectorSize
497 a939e3b8 Nikos Skalkotos
498 a939e3b8 Nikos Skalkotos
        # Create sparse file to host the image
499 567891a6 Nikos Skalkotos
        fd = os.open(image, os.O_WRONLY | os.O_CREAT)
500 567891a6 Nikos Skalkotos
        try:
501 567891a6 Nikos Skalkotos
            os.ftruncate(fd, size)
502 567891a6 Nikos Skalkotos
        finally:
503 567891a6 Nikos Skalkotos
            os.close(fd)
504 25b4d858 Nikos Skalkotos
505 25b4d858 Nikos Skalkotos
        self._create_partition_table(image)
506 b0376b3f Nikos Skalkotos
        end_sector, partitions = self._shrink_partitions(image)
507 b0376b3f Nikos Skalkotos
508 b0376b3f Nikos Skalkotos
        if self.disk.type == 'gpt':
509 b0376b3f Nikos Skalkotos
            old_size = size
510 b0376b3f Nikos Skalkotos
            size = (end_sector + 1) * self.disk.device.sectorSize
511 b0376b3f Nikos Skalkotos
            ptable = GPTPartitionTable(image)
512 b0376b3f Nikos Skalkotos
            size = ptable.shrink(size, old_size)
513 b0376b3f Nikos Skalkotos
        else:
514 b0376b3f Nikos Skalkotos
            # Alighn to 2048
515 b0376b3f Nikos Skalkotos
            end_sector = ((end_sector + 2047) // 2048) * 2048
516 b0376b3f Nikos Skalkotos
            size = (end_sector + 1) * self.disk.device.sectorSize
517 fa39c7e3 Nikos Skalkotos
518 fa39c7e3 Nikos Skalkotos
        # Truncate image to the new size.
519 fa39c7e3 Nikos Skalkotos
        fd = os.open(image, os.O_RDWR)
520 fa39c7e3 Nikos Skalkotos
        try:
521 fa39c7e3 Nikos Skalkotos
            os.ftruncate(fd, size)
522 fa39c7e3 Nikos Skalkotos
        finally:
523 fa39c7e3 Nikos Skalkotos
            os.close(fd)
524 fa39c7e3 Nikos Skalkotos
525 25b4d858 Nikos Skalkotos
        # Check if the available space is enough to host the image
526 25b4d858 Nikos Skalkotos
        dirname = os.path.dirname(image)
527 1294501a Nikos Skalkotos
        self.out.output("Examining available space ...", False)
528 c16850ae Nikos Skalkotos
        if free_space(dirname) <= size:
529 dd22ce7b Nikos Skalkotos
            raise FatalError("Not enough space under %s to host the temporary "
530 dd22ce7b Nikos Skalkotos
                             "image" % dirname)
531 25b4d858 Nikos Skalkotos
        self.out.success("sufficient")
532 8eea5572 Nikos Skalkotos
533 b0376b3f Nikos Skalkotos
        self._create_filesystems(image, partitions)
534 8eea5572 Nikos Skalkotos
535 25b4d858 Nikos Skalkotos
        return image
536 9e4b4de2 Nikos Skalkotos
537 9e4b4de2 Nikos Skalkotos
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :