Statistics
| Branch: | Tag: | Revision:

root / image_creator / bundle_volume.py @ f953c647

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