Statistics
| Branch: | Tag: | Revision:

root / image_creator / image.py @ 71b0ab28

History | View | Annotate | Download (12.7 kB)

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

    
34
from image_creator.util import FatalError
35
from image_creator.gpt import GPTPartitionTable
36
from image_creator.os_type import os_cls
37

    
38
import re
39
import guestfs
40
from sendfile import sendfile
41

    
42

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

    
46
    def __init__(self, device, output, bootable=True, meta={}):
47
        """Create a new Image instance"""
48

    
49
        self.device = device
50
        self.out = output
51
        self.bootable = bootable
52
        self.meta = meta
53
        self.progress_bar = None
54
        self.guestfs_device = None
55
        self.size = 0
56

    
57
        self.g = guestfs.GuestFS()
58
        self.g.add_drive_opts(self.device, readonly=0, format="raw")
59

    
60
        # Before version 1.17.14 the recovery process, which is a fork of the
61
        # original process that called libguestfs, did not close its inherited
62
        # file descriptors. This can cause problems especially if the parent
63
        # process has opened pipes. Since the recovery process is an optional
64
        # feature of libguestfs, it's better to disable it.
65
        self.g.set_recovery_proc(0)
66
        version = self.g.version()
67
        if version['major'] > 1 or \
68
            (version['major'] == 1 and (version['minor'] >= 18 or
69
                                        (version['minor'] == 17 and
70
                                         version['release'] >= 14))):
71
            self.g.set_recovery_proc(1)
72
            self.out.output("Enabling recovery proc")
73

    
74
        #self.g.set_trace(1)
75
        #self.g.set_verbose(1)
76

    
77
        self.guestfs_enabled = False
78

    
79
    def enable(self):
80
        """Enable a newly created Image instance"""
81

    
82
        self.out.output('Launching helper VM (may take a while) ...', False)
83
        # self.progressbar = self.out.Progress(100, "Launching helper VM",
84
        #                                     "percent")
85
        # eh = self.g.set_event_callback(self.progress_callback,
86
        #                               guestfs.EVENT_PROGRESS)
87
        self.g.launch()
88
        self.guestfs_enabled = True
89
        # self.g.delete_event_callback(eh)
90
        # self.progressbar.success('done')
91
        # self.progressbar = None
92
        self.out.success('done')
93

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

    
107
        self.ostype = self.g.inspect_get_type(self.root)
108
        self.distro = self.g.inspect_get_distro(self.root)
109
        self.out.success(
110
            'found a(n) %s system' %
111
            self.ostype if self.distro == "unknown" else self.distro)
112

    
113
    def _get_os(self):
114
        """Return an OS class instance for this image"""
115
        if hasattr(self, "_os"):
116
            return self._os
117

    
118
        if not self.guestfs_enabled:
119
            self.enable()
120

    
121
        cls = os_cls(self.distro, self.ostype)
122
        self._os = cls(self.root, self.g, self.out)
123

    
124
        self._os.collect_metadata()
125

    
126
        return self._os
127

    
128
    os = property(_get_os)
129

    
130
    def destroy(self):
131
        """Destroy this Image instance."""
132

    
133
        # In new guestfs versions, there is a handy shutdown method for this
134
        try:
135
            if self.guestfs_enabled:
136
                self.g.umount_all()
137
                self.g.sync()
138
        finally:
139
            # Close the guestfs handler if open
140
            self.g.close()
141

    
142
#    def progress_callback(self, ev, eh, buf, array):
143
#        position = array[2]
144
#        total = array[3]
145
#
146
#        self.progressbar.goto((position * 100) // total)
147

    
148
    def _last_partition(self):
149
        """Return the last partition of the image disk"""
150
        if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
151
            msg = "Unsupported partition table: %s. Only msdos and gpt " \
152
                "partition tables are supported" % self.meta['PARTITION_TABLE']
153
            raise FatalError(msg)
154

    
155
        is_extended = lambda p: \
156
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
157
            in (0x5, 0xf)
158
        is_logical = lambda p: \
159
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
160

    
161
        partitions = self.g.part_list(self.guestfs_device)
162
        last_partition = partitions[-1]
163

    
164
        if is_logical(last_partition):
165
            # The disk contains extended and logical partitions....
166
            extended = filter(is_extended, partitions)[0]
167
            last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
168

    
169
            # check if extended is the last primary partition
170
            if last_primary['part_num'] > extended['part_num']:
171
                last_partition = last_primary
172

    
173
        return last_partition
174

    
175
    def shrink(self):
176
        """Shrink the image.
177

178
        This is accomplished by shrinking the last file system of the
179
        image and then updating the partition table. The new disk size
180
        (in bytes) is returned.
181

182
        ATTENTION: make sure unmount is called before shrink
183
        """
184
        get_fstype = lambda p: \
185
            self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
186
        is_logical = lambda p: \
187
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
188
        is_extended = lambda p: \
189
            self.meta['PARTITION_TABLE'] == 'msdos' and \
190
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
191
            in (0x5, 0xf)
192

    
193
        part_add = lambda ptype, start, stop: \
194
            self.g.part_add(self.guestfs_device, ptype, start, stop)
195
        part_del = lambda p: self.g.part_del(self.guestfs_device, p)
196
        part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
197
        part_set_id = lambda p, id: \
198
            self.g.part_set_mbr_id(self.guestfs_device, p, id)
199
        part_get_bootable = lambda p: \
200
            self.g.part_get_bootable(self.guestfs_device, p)
201
        part_set_bootable = lambda p, bootable: \
202
            self.g.part_set_bootable(self.guestfs_device, p, bootable)
203

    
204
        MB = 2 ** 20
205

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

    
208
        sector_size = self.g.blockdev_getss(self.guestfs_device)
209

    
210
        last_part = None
211
        fstype = None
212
        while True:
213
            last_part = self._last_partition()
214
            fstype = get_fstype(last_part)
215

    
216
            if fstype == 'swap':
217
                self.meta['SWAP'] = "%d:%s" % \
218
                    (last_part['part_num'],
219
                     (last_part['part_size'] + MB - 1) // MB)
220
                part_del(last_part['part_num'])
221
                continue
222
            elif is_extended(last_part):
223
                part_del(last_part['part_num'])
224
                continue
225

    
226
            # Most disk manipulation programs leave 2048 sectors after the last
227
            # partition
228
            new_size = last_part['part_end'] + 1 + 2048 * sector_size
229
            self.size = min(self.size, new_size)
230
            break
231

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

    
236
        part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
237
        self.g.e2fsck_f(part_dev)
238
        self.g.resize2fs_M(part_dev)
239

    
240
        out = self.g.tune2fs_l(part_dev)
241
        block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
242
        block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
243

    
244
        start = last_part['part_start'] / sector_size
245
        end = start + (block_size * block_cnt) / sector_size - 1
246

    
247
        if is_logical(last_part):
248
            partitions = self.g.part_list(self.guestfs_device)
249

    
250
            logical = []  # logical partitions
251
            for partition in partitions:
252
                if partition['part_num'] < 4:
253
                    continue
254
                logical.append({
255
                    'num': partition['part_num'],
256
                    'start': partition['part_start'] / sector_size,
257
                    'end': partition['part_end'] / sector_size,
258
                    'id': part_get_id(partition['part_num']),
259
                    'bootable': part_get_bootable(partition['part_num'])
260
                })
261

    
262
            logical[-1]['end'] = end  # new end after resize
263

    
264
            # Recreate the extended partition
265
            extended = filter(is_extended, partitions)[0]
266
            part_del(extended['part_num'])
267
            part_add('e', extended['part_start'] / sector_size, end)
268

    
269
            # Create all the logical partitions back
270
            for l in logical:
271
                part_add('l', l['start'], l['end'])
272
                part_set_id(l['num'], l['id'])
273
                part_set_bootable(l['num'], l['bootable'])
274
        else:
275
            # Recreate the last partition
276
            if self.meta['PARTITION_TABLE'] == 'msdos':
277
                last_part['id'] = part_get_id(last_part['part_num'])
278

    
279
            last_part['bootable'] = part_get_bootable(last_part['part_num'])
280
            part_del(last_part['part_num'])
281
            part_add('p', start, end)
282
            part_set_bootable(last_part['part_num'], last_part['bootable'])
283

    
284
            if self.meta['PARTITION_TABLE'] == 'msdos':
285
                part_set_id(last_part['part_num'], last_part['id'])
286

    
287
        new_size = (end + 1) * sector_size
288

    
289
        assert (new_size <= self.size)
290

    
291
        if self.meta['PARTITION_TABLE'] == 'gpt':
292
            ptable = GPTPartitionTable(self.device)
293
            self.size = ptable.shrink(new_size, self.size)
294
        else:
295
            self.size = min(new_size + 2048 * sector_size, self.size)
296

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

    
299
        return self.size
300

    
301
    def dump(self, outfile):
302
        """Dumps the content of the image into a file.
303

304
        This method will only dump the actual payload, found by reading the
305
        partition table. Empty space in the end of the device will be ignored.
306
        """
307
        MB = 2 ** 20
308
        blocksize = 4 * MB  # 4MB
309
        size = self.size
310
        progr_size = (size + MB - 1) // MB  # in MB
311
        progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
312

    
313
        with open(self.device, 'r') as src:
314
            with open(outfile, "w") as dst:
315
                left = size
316
                offset = 0
317
                progressbar.next()
318
                while left > 0:
319
                    length = min(left, blocksize)
320
                    sent = sendfile(dst.fileno(), src.fileno(), offset, length)
321

    
322
                    # Workaround for python-sendfile API change. In
323
                    # python-sendfile 1.2.x (py-sendfile) the returning value
324
                    # of sendfile is a tuple, where in version 2.x (pysendfile)
325
                    # it is just a sigle integer.
326
                    if isinstance(sent, tuple):
327
                        sent = sent[1]
328

    
329
                    offset += sent
330
                    left -= sent
331
                    progressbar.goto((size - left) // MB)
332
        progressbar.success('image file %s was successfully created' % outfile)
333

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