Statistics
| Branch: | Tag: | Revision:

root / image_creator / image.py @ f5634b9a

History | View | Annotate | Download (14.7 kB)

1
# -*- coding: utf-8 -*-
2
#
3
# Copyright 2013 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35

    
36
from image_creator.util import FatalError
37
from image_creator.gpt import GPTPartitionTable
38
from image_creator.os_type import os_cls
39

    
40
import re
41
import guestfs
42
from sendfile import sendfile
43

    
44

    
45
class Image(object):
46
    """The instances of this class can create images out of block devices."""
47

    
48
    def __init__(self, device, output, **kargs):
49
        """Create a new Image instance"""
50

    
51
        self.device = device
52
        self.out = output
53

    
54
        self.meta = kargs['meta'] if 'meta' in kargs else {}
55
        self.sysprep_params = \
56
            kargs['sysprep_params'] if 'sysprep_params' in kargs else {}
57

    
58
        self.progress_bar = None
59
        self.guestfs_device = None
60
        self.size = 0
61

    
62
        self.g = guestfs.GuestFS()
63
        self.guestfs_enabled = False
64
        self.guestfs_version = self.g.version()
65

    
66
    def check_guestfs_version(self, major, minor, release):
67
        """Checks if the version of the used libguestfs is smaller, equal or
68
        greater than the one specified by the major, minor and release triplet
69

70
        Returns:
71
            < 0 if the installed version is smaller than the specified one
72
            = 0 if they are equal
73
            > 0 if the installed one is greater than the specified one
74
        """
75

    
76
        for (a, b) in (self.guestfs_version['major'], major), \
77
                (self.guestfs_version['minor'], minor), \
78
                (self.guestfs_version['release'], release):
79
            if a != b:
80
                return a - b
81

    
82
        return 0
83

    
84
    def enable(self):
85
        """Enable a newly created Image instance"""
86

    
87
        self.enable_guestfs()
88

    
89
        self.out.output('Inspecting Operating System ...', False)
90
        roots = self.g.inspect_os()
91
        if len(roots) == 0:
92
            raise FatalError("No operating system found")
93
        if len(roots) > 1:
94
            raise FatalError("Multiple operating systems found."
95
                             "We only support images with one OS.")
96
        self.root = roots[0]
97
        self.guestfs_device = self.g.part_to_dev(self.root)
98
        self.size = self.g.blockdev_getsize64(self.guestfs_device)
99
        self.meta['PARTITION_TABLE'] = \
100
            self.g.part_get_parttype(self.guestfs_device)
101

    
102
        self.ostype = self.g.inspect_get_type(self.root)
103
        self.distro = self.g.inspect_get_distro(self.root)
104
        self.out.success(
105
            'found a(n) %s system' %
106
            self.ostype if self.distro == "unknown" else self.distro)
107

    
108
    def enable_guestfs(self):
109
        """Enable the guestfs handler"""
110

    
111
        if self.guestfs_enabled:
112
            self.out.warn("Guestfs is already enabled")
113
            return
114

    
115
        # Before version 1.18.4 the behaviour of kill_subprocess was different
116
        # and you need to reset the guestfs handler to relaunch a previously
117
        # shut down qemu backend
118
        if self.check_guestfs_version(1, 18, 4) < 0:
119
            self.g = guestfs.GuestFS()
120

    
121
        self.g.add_drive_opts(self.device, readonly=0, format="raw")
122

    
123
        # Before version 1.17.14 the recovery process, which is a fork of the
124
        # original process that called libguestfs, did not close its inherited
125
        # file descriptors. This can cause problems especially if the parent
126
        # process has opened pipes. Since the recovery process is an optional
127
        # feature of libguestfs, it's better to disable it.
128
        if self.check_guestfs_version(1, 17, 14) >= 0:
129
            self.out.output("Enabling recovery proc")
130
            self.g.set_recovery_proc(1)
131
        else:
132
            self.g.set_recovery_proc(0)
133

    
134
        #self.g.set_trace(1)
135
        #self.g.set_verbose(1)
136

    
137
        self.out.output('Launching helper VM (may take a while) ...', False)
138
        # self.progressbar = self.out.Progress(100, "Launching helper VM",
139
        #                                     "percent")
140
        # eh = self.g.set_event_callback(self.progress_callback,
141
        #                               guestfs.EVENT_PROGRESS)
142
        self.g.launch()
143
        self.guestfs_enabled = True
144
        # self.g.delete_event_callback(eh)
145
        # self.progressbar.success('done')
146
        # self.progressbar = None
147

    
148
        if self.check_guestfs_version(1, 18, 4) < 0:
149
            self.g.inspect_os()  # some calls need this
150

    
151
        self.out.success('done')
152

    
153
    def disable_guestfs(self):
154
        """Disable the guestfs handler"""
155

    
156
        if not self.guestfs_enabled:
157
            self.out.warn("Guestfs is already disabled")
158
            return
159

    
160
        self.out.output("Shutting down helper VM ...", False)
161
        self.g.sync()
162
        # guestfs_shutdown which is the prefered way to shutdown the backend
163
        # process was introduced in version 1.19.16
164
        if self.check_guestfs_version(1, 19, 16) >= 0:
165
            self.g.shutdown()
166
        else:
167
            self.g.kill_subprocess()
168

    
169
        self.guestfs_enabled = False
170
        self.out.success('done')
171

    
172
    def _get_os(self):
173
        """Return an OS class instance for this image"""
174
        if hasattr(self, "_os"):
175
            return self._os
176

    
177
        if not self.guestfs_enabled:
178
            self.enable()
179

    
180
        cls = os_cls(self.distro, self.ostype)
181
        self._os = cls(self, sysprep_params=self.sysprep_params)
182

    
183
        self._os.collect_metadata()
184

    
185
        return self._os
186

    
187
    os = property(_get_os)
188

    
189
    def destroy(self):
190
        """Destroy this Image instance."""
191

    
192
        # In new guestfs versions, there is a handy shutdown method for this
193
        try:
194
            if self.guestfs_enabled:
195
                self.g.umount_all()
196
                self.g.sync()
197
        finally:
198
            # Close the guestfs handler if open
199
            self.g.close()
200

    
201
#    def progress_callback(self, ev, eh, buf, array):
202
#        position = array[2]
203
#        total = array[3]
204
#
205
#        self.progressbar.goto((position * 100) // total)
206

    
207
    def _last_partition(self):
208
        """Return the last partition of the image disk"""
209
        if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
210
            msg = "Unsupported partition table: %s. Only msdos and gpt " \
211
                "partition tables are supported" % self.meta['PARTITION_TABLE']
212
            raise FatalError(msg)
213

    
214
        is_extended = lambda p: \
215
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
216
            in (0x5, 0xf)
217
        is_logical = lambda p: \
218
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
219

    
220
        partitions = self.g.part_list(self.guestfs_device)
221
        last_partition = partitions[-1]
222

    
223
        if is_logical(last_partition):
224
            # The disk contains extended and logical partitions....
225
            extended = filter(is_extended, partitions)[0]
226
            last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
227

    
228
            # check if extended is the last primary partition
229
            if last_primary['part_num'] > extended['part_num']:
230
                last_partition = last_primary
231

    
232
        return last_partition
233

    
234
    def shrink(self):
235
        """Shrink the image.
236

237
        This is accomplished by shrinking the last file system of the
238
        image and then updating the partition table. The new disk size
239
        (in bytes) is returned.
240

241
        ATTENTION: make sure unmount is called before shrink
242
        """
243
        get_fstype = lambda p: \
244
            self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
245
        is_logical = lambda p: \
246
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
247
        is_extended = lambda p: \
248
            self.meta['PARTITION_TABLE'] == 'msdos' and \
249
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
250
            in (0x5, 0xf)
251

    
252
        part_add = lambda ptype, start, stop: \
253
            self.g.part_add(self.guestfs_device, ptype, start, stop)
254
        part_del = lambda p: self.g.part_del(self.guestfs_device, p)
255
        part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
256
        part_set_id = lambda p, id: \
257
            self.g.part_set_mbr_id(self.guestfs_device, p, id)
258
        part_get_bootable = lambda p: \
259
            self.g.part_get_bootable(self.guestfs_device, p)
260
        part_set_bootable = lambda p, bootable: \
261
            self.g.part_set_bootable(self.guestfs_device, p, bootable)
262

    
263
        MB = 2 ** 20
264

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

    
267
        sector_size = self.g.blockdev_getss(self.guestfs_device)
268

    
269
        last_part = None
270
        fstype = None
271
        while True:
272
            last_part = self._last_partition()
273
            fstype = get_fstype(last_part)
274

    
275
            if fstype == 'swap':
276
                self.meta['SWAP'] = "%d:%s" % \
277
                    (last_part['part_num'],
278
                     (last_part['part_size'] + MB - 1) // MB)
279
                part_del(last_part['part_num'])
280
                continue
281
            elif is_extended(last_part):
282
                part_del(last_part['part_num'])
283
                continue
284

    
285
            # Most disk manipulation programs leave 2048 sectors after the last
286
            # partition
287
            new_size = last_part['part_end'] + 1 + 2048 * sector_size
288
            self.size = min(self.size, new_size)
289
            break
290

    
291
        if not re.match("ext[234]", fstype):
292
            self.out.warn("Don't know how to shrink %s partitions." % fstype)
293
            return self.size
294

    
295
        part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
296

    
297
        if self.check_guestfs_version(1, 15, 17) >= 0:
298
            self.g.e2fsck(part_dev, forceall=1)
299
        else:
300
            self.g.e2fsck_f(part_dev)
301

    
302
        self.g.resize2fs_M(part_dev)
303

    
304
        out = self.g.tune2fs_l(part_dev)
305
        block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
306
        block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
307

    
308
        start = last_part['part_start'] / sector_size
309
        end = start + (block_size * block_cnt) / sector_size - 1
310

    
311
        if is_logical(last_part):
312
            partitions = self.g.part_list(self.guestfs_device)
313

    
314
            logical = []  # logical partitions
315
            for partition in partitions:
316
                if partition['part_num'] < 4:
317
                    continue
318
                logical.append({
319
                    'num': partition['part_num'],
320
                    'start': partition['part_start'] / sector_size,
321
                    'end': partition['part_end'] / sector_size,
322
                    'id': part_get_id(partition['part_num']),
323
                    'bootable': part_get_bootable(partition['part_num'])
324
                })
325

    
326
            logical[-1]['end'] = end  # new end after resize
327

    
328
            # Recreate the extended partition
329
            extended = filter(is_extended, partitions)[0]
330
            part_del(extended['part_num'])
331
            part_add('e', extended['part_start'] / sector_size, end)
332

    
333
            # Create all the logical partitions back
334
            for l in logical:
335
                part_add('l', l['start'], l['end'])
336
                part_set_id(l['num'], l['id'])
337
                part_set_bootable(l['num'], l['bootable'])
338
        else:
339
            # Recreate the last partition
340
            if self.meta['PARTITION_TABLE'] == 'msdos':
341
                last_part['id'] = part_get_id(last_part['part_num'])
342

    
343
            last_part['bootable'] = part_get_bootable(last_part['part_num'])
344
            part_del(last_part['part_num'])
345
            part_add('p', start, end)
346
            part_set_bootable(last_part['part_num'], last_part['bootable'])
347

    
348
            if self.meta['PARTITION_TABLE'] == 'msdos':
349
                part_set_id(last_part['part_num'], last_part['id'])
350

    
351
        new_size = (end + 1) * sector_size
352

    
353
        assert (new_size <= self.size)
354

    
355
        if self.meta['PARTITION_TABLE'] == 'gpt':
356
            ptable = GPTPartitionTable(self.device)
357
            self.size = ptable.shrink(new_size, self.size)
358
        else:
359
            self.size = min(new_size + 2048 * sector_size, self.size)
360

    
361
        self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
362

    
363
        return self.size
364

    
365
    def dump(self, outfile):
366
        """Dumps the content of the image into a file.
367

368
        This method will only dump the actual payload, found by reading the
369
        partition table. Empty space in the end of the device will be ignored.
370
        """
371
        MB = 2 ** 20
372
        blocksize = 4 * MB  # 4MB
373
        size = self.size
374
        progr_size = (size + MB - 1) // MB  # in MB
375
        progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
376

    
377
        with open(self.device, 'r') as src:
378
            with open(outfile, "w") as dst:
379
                left = size
380
                offset = 0
381
                progressbar.next()
382
                while left > 0:
383
                    length = min(left, blocksize)
384
                    sent = sendfile(dst.fileno(), src.fileno(), offset, length)
385

    
386
                    # Workaround for python-sendfile API change. In
387
                    # python-sendfile 1.2.x (py-sendfile) the returning value
388
                    # of sendfile is a tuple, where in version 2.x (pysendfile)
389
                    # it is just a sigle integer.
390
                    if isinstance(sent, tuple):
391
                        sent = sent[1]
392

    
393
                    offset += sent
394
                    left -= sent
395
                    progressbar.goto((size - left) // MB)
396
        progressbar.success('image file %s was successfully created' % outfile)
397

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