Statistics
| Branch: | Tag: | Revision:

root / image_creator / image.py @ bbfcaef1

History | View | Annotate | Download (13.9 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, check_guestfs_version
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

    
65
    def enable(self):
66
        """Enable a newly created Image instance"""
67

    
68
        self.enable_guestfs()
69

    
70
        self.out.output('Inspecting Operating System ...', False)
71
        roots = self.g.inspect_os()
72
        if len(roots) == 0:
73
            raise FatalError("No operating system found")
74
        if len(roots) > 1:
75
            raise FatalError("Multiple operating systems found."
76
                             "We only support images with one OS.")
77
        self.root = roots[0]
78
        self.guestfs_device = self.g.part_to_dev(self.root)
79
        self.size = self.g.blockdev_getsize64(self.guestfs_device)
80
        self.meta['PARTITION_TABLE'] = \
81
            self.g.part_get_parttype(self.guestfs_device)
82

    
83
        self.ostype = self.g.inspect_get_type(self.root)
84
        self.distro = self.g.inspect_get_distro(self.root)
85
        self.out.success(
86
            'found a(n) %s system' %
87
            self.ostype if self.distro == "unknown" else self.distro)
88

    
89
    def enable_guestfs(self):
90
        """Enable the guestfs handler"""
91

    
92
        if self.guestfs_enabled:
93
            self.out.warn("Guestfs is already enabled")
94
            return
95

    
96
        # Before version 1.18.4 the behaviour of kill_subprocess was different
97
        # and you need to reset the guestfs handler to relaunch a previously
98
        # shut down qemu backend
99
        if check_guestfs_version(self.g, 1, 18, 4) < 0:
100
            self.g = guestfs.GuestFS()
101

    
102
        self.g.add_drive_opts(self.device, readonly=0, format="raw")
103

    
104
        # Before version 1.17.14 the recovery process, which is a fork of the
105
        # original process that called libguestfs, did not close its inherited
106
        # file descriptors. This can cause problems especially if the parent
107
        # process has opened pipes. Since the recovery process is an optional
108
        # feature of libguestfs, it's better to disable it.
109
        if check_guestfs_version(self.g, 1, 17, 14) >= 0:
110
            self.out.output("Enabling recovery proc")
111
            self.g.set_recovery_proc(1)
112
        else:
113
            self.g.set_recovery_proc(0)
114

    
115
        #self.g.set_trace(1)
116
        #self.g.set_verbose(1)
117

    
118
        self.out.output('Launching helper VM (may take a while) ...', False)
119
        # self.progressbar = self.out.Progress(100, "Launching helper VM",
120
        #                                     "percent")
121
        # eh = self.g.set_event_callback(self.progress_callback,
122
        #                               guestfs.EVENT_PROGRESS)
123
        self.g.launch()
124
        self.guestfs_enabled = True
125
        # self.g.delete_event_callback(eh)
126
        # self.progressbar.success('done')
127
        # self.progressbar = None
128

    
129
        if check_guestfs_version(self.g, 1, 18, 4) < 0:
130
            self.g.inspect_os()  # some calls need this
131

    
132
        self.out.success('done')
133

    
134
    def disable_guestfs(self):
135
        """Disable the guestfs handler"""
136

    
137
        if not self.guestfs_enabled:
138
            self.out.warn("Guestfs is already disabled")
139
            return
140

    
141
        self.out.output("Shutting down helper VM ...", False)
142
        self.g.sync()
143
        # guestfs_shutdown which is the prefered way to shutdown the backend
144
        # process was introduced in version 1.19.16
145
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
146
            self.g.shutdown()
147
        else:
148
            self.g.kill_subprocess()
149

    
150
        self.guestfs_enabled = False
151
        self.out.success('done')
152

    
153
    def _get_os(self):
154
        """Return an OS class instance for this image"""
155
        if hasattr(self, "_os"):
156
            return self._os
157

    
158
        if not self.guestfs_enabled:
159
            self.enable()
160

    
161
        cls = os_cls(self.distro, self.ostype)
162
        self._os = cls(self, sysprep_params=self.sysprep_params)
163

    
164
        self._os.collect_metadata()
165

    
166
        return self._os
167

    
168
    os = property(_get_os)
169

    
170
    def destroy(self):
171
        """Destroy this Image instance."""
172

    
173
        # In new guestfs versions, there is a handy shutdown method for this
174
        try:
175
            if self.guestfs_enabled:
176
                self.g.umount_all()
177
                self.g.sync()
178
        finally:
179
            # Close the guestfs handler if open
180
            self.g.close()
181

    
182
#    def progress_callback(self, ev, eh, buf, array):
183
#        position = array[2]
184
#        total = array[3]
185
#
186
#        self.progressbar.goto((position * 100) // total)
187

    
188
    def _last_partition(self):
189
        """Return the last partition of the image disk"""
190
        if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
191
            msg = "Unsupported partition table: %s. Only msdos and gpt " \
192
                "partition tables are supported" % self.meta['PARTITION_TABLE']
193
            raise FatalError(msg)
194

    
195
        is_extended = lambda p: \
196
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
197
            in (0x5, 0xf)
198
        is_logical = lambda p: \
199
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
200

    
201
        partitions = self.g.part_list(self.guestfs_device)
202
        last_partition = partitions[-1]
203

    
204
        if is_logical(last_partition):
205
            # The disk contains extended and logical partitions....
206
            extended = filter(is_extended, partitions)[0]
207
            last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
208

    
209
            # check if extended is the last primary partition
210
            if last_primary['part_num'] > extended['part_num']:
211
                last_partition = last_primary
212

    
213
        return last_partition
214

    
215
    def shrink(self):
216
        """Shrink the image.
217

218
        This is accomplished by shrinking the last file system of the
219
        image and then updating the partition table. The new disk size
220
        (in bytes) is returned.
221

222
        ATTENTION: make sure unmount is called before shrink
223
        """
224
        get_fstype = lambda p: \
225
            self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
226
        is_logical = lambda p: \
227
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
228
        is_extended = lambda p: \
229
            self.meta['PARTITION_TABLE'] == 'msdos' and \
230
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
231
            in (0x5, 0xf)
232

    
233
        part_add = lambda ptype, start, stop: \
234
            self.g.part_add(self.guestfs_device, ptype, start, stop)
235
        part_del = lambda p: self.g.part_del(self.guestfs_device, p)
236
        part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
237
        part_set_id = lambda p, id: \
238
            self.g.part_set_mbr_id(self.guestfs_device, p, id)
239
        part_get_bootable = lambda p: \
240
            self.g.part_get_bootable(self.guestfs_device, p)
241
        part_set_bootable = lambda p, bootable: \
242
            self.g.part_set_bootable(self.guestfs_device, p, bootable)
243

    
244
        MB = 2 ** 20
245

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

    
248
        sector_size = self.g.blockdev_getss(self.guestfs_device)
249

    
250
        last_part = None
251
        fstype = None
252
        while True:
253
            last_part = self._last_partition()
254
            fstype = get_fstype(last_part)
255

    
256
            if fstype == 'swap':
257
                self.meta['SWAP'] = "%d:%s" % \
258
                    (last_part['part_num'],
259
                     (last_part['part_size'] + MB - 1) // MB)
260
                part_del(last_part['part_num'])
261
                continue
262
            elif is_extended(last_part):
263
                part_del(last_part['part_num'])
264
                continue
265

    
266
            # Most disk manipulation programs leave 2048 sectors after the last
267
            # partition
268
            new_size = last_part['part_end'] + 1 + 2048 * sector_size
269
            self.size = min(self.size, new_size)
270
            break
271

    
272
        if not re.match("ext[234]", fstype):
273
            self.out.warn("Don't know how to shrink %s partitions." % fstype)
274
            return self.size
275

    
276
        part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
277
        self.g.e2fsck_f(part_dev)
278
        self.g.resize2fs_M(part_dev)
279

    
280
        out = self.g.tune2fs_l(part_dev)
281
        block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
282
        block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
283

    
284
        start = last_part['part_start'] / sector_size
285
        end = start + (block_size * block_cnt) / sector_size - 1
286

    
287
        if is_logical(last_part):
288
            partitions = self.g.part_list(self.guestfs_device)
289

    
290
            logical = []  # logical partitions
291
            for partition in partitions:
292
                if partition['part_num'] < 4:
293
                    continue
294
                logical.append({
295
                    'num': partition['part_num'],
296
                    'start': partition['part_start'] / sector_size,
297
                    'end': partition['part_end'] / sector_size,
298
                    'id': part_get_id(partition['part_num']),
299
                    'bootable': part_get_bootable(partition['part_num'])
300
                })
301

    
302
            logical[-1]['end'] = end  # new end after resize
303

    
304
            # Recreate the extended partition
305
            extended = filter(is_extended, partitions)[0]
306
            part_del(extended['part_num'])
307
            part_add('e', extended['part_start'] / sector_size, end)
308

    
309
            # Create all the logical partitions back
310
            for l in logical:
311
                part_add('l', l['start'], l['end'])
312
                part_set_id(l['num'], l['id'])
313
                part_set_bootable(l['num'], l['bootable'])
314
        else:
315
            # Recreate the last partition
316
            if self.meta['PARTITION_TABLE'] == 'msdos':
317
                last_part['id'] = part_get_id(last_part['part_num'])
318

    
319
            last_part['bootable'] = part_get_bootable(last_part['part_num'])
320
            part_del(last_part['part_num'])
321
            part_add('p', start, end)
322
            part_set_bootable(last_part['part_num'], last_part['bootable'])
323

    
324
            if self.meta['PARTITION_TABLE'] == 'msdos':
325
                part_set_id(last_part['part_num'], last_part['id'])
326

    
327
        new_size = (end + 1) * sector_size
328

    
329
        assert (new_size <= self.size)
330

    
331
        if self.meta['PARTITION_TABLE'] == 'gpt':
332
            ptable = GPTPartitionTable(self.device)
333
            self.size = ptable.shrink(new_size, self.size)
334
        else:
335
            self.size = min(new_size + 2048 * sector_size, self.size)
336

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

    
339
        return self.size
340

    
341
    def dump(self, outfile):
342
        """Dumps the content of the image into a file.
343

344
        This method will only dump the actual payload, found by reading the
345
        partition table. Empty space in the end of the device will be ignored.
346
        """
347
        MB = 2 ** 20
348
        blocksize = 4 * MB  # 4MB
349
        size = self.size
350
        progr_size = (size + MB - 1) // MB  # in MB
351
        progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
352

    
353
        with open(self.device, 'r') as src:
354
            with open(outfile, "w") as dst:
355
                left = size
356
                offset = 0
357
                progressbar.next()
358
                while left > 0:
359
                    length = min(left, blocksize)
360
                    sent = sendfile(dst.fileno(), src.fileno(), offset, length)
361

    
362
                    # Workaround for python-sendfile API change. In
363
                    # python-sendfile 1.2.x (py-sendfile) the returning value
364
                    # of sendfile is a tuple, where in version 2.x (pysendfile)
365
                    # it is just a sigle integer.
366
                    if isinstance(sent, tuple):
367
                        sent = sent[1]
368

    
369
                    offset += sent
370
                    left -= sent
371
                    progressbar.goto((size - left) // MB)
372
        progressbar.success('image file %s was successfully created' % outfile)
373

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