Statistics
| Branch: | Tag: | Revision:

root / image_creator / disk.py @ e7c01d1e

History | View | Annotate | Download (7.8 kB)

1
#!/usr/bin/env python
2

    
3
import losetup
4
import stat
5
import os
6
import tempfile
7
import uuid
8
import re
9
import sys
10
import guestfs
11

    
12
import pbs
13
from pbs import dd
14
from clint.textui import progress
15

    
16

    
17
class DiskError(Exception):
18
    pass
19

    
20

    
21
def find_sbin_command(command, exception):
22
    search_paths = ['/usr/local/sbin', '/usr/sbin', '/sbin']
23
    for fullpath in map(lambda x: "%s/%s" % (x, command), search_paths):
24
        if os.path.exists(fullpath) and os.access(fullpath, os.X_OK):
25
            return pbs.Command(fullpath)
26
        continue
27
    raise exception
28

    
29

    
30
try:
31
    from pbs import dmsetup
32
except pbs.CommandNotFound as e:
33
    dmsetup = find_sbin_command('dmsetup', e)
34

    
35
try:
36
    from pbs import blockdev
37
except pbs.CommandNotFound as e:
38
    blockdev = find_sbin_command('blockdev', e)
39

    
40

    
41
class Disk(object):
42
    """This class represents a hard disk hosting an Operating System
43

44
    A Disk instance never alters the source media it is created from.
45
    Any change is done on a snapshot created by the device-mapper of
46
    the Linux kernel.
47
    """
48

    
49
    def __init__(self, source):
50
        """Create a new Disk instance out of a source media. The source
51
        media can be an image file, a block device or a directory."""
52
        self._cleanup_jobs = []
53
        self._devices = []
54
        self.source = source
55

    
56
    def _add_cleanup(self, job, *args):
57
        self._cleanup_jobs.append((job, args))
58

    
59
    def _losetup(self, fname):
60
        loop = losetup.find_unused_loop_device()
61
        loop.mount(fname)
62
        self._add_cleanup(loop.unmount)
63
        return loop.device
64

    
65
    def _dir_to_disk(self):
66
        raise NotImplementedError
67

    
68
    def cleanup(self):
69
        """Cleanup internal data. This needs to be called before the
70
        program ends.
71
        """
72
        while len(self._devices):
73
            device = self._devices.pop()
74
            device.destroy()
75

    
76
        while len(self._cleanup_jobs):
77
            job, args = self._cleanup_jobs.pop()
78
            job(*args)
79

    
80
    def get_device(self):
81
        """Returns a newly created DiskDevice instance.
82

83
        This instance is a snapshot of the original source media of
84
        the Disk instance.
85
        """
86
        sourcedev = self.source
87
        mode = os.stat(self.source).st_mode
88
        if stat.S_ISDIR(mode):
89
            return self._losetup(self._dir_to_disk())
90
        elif stat.S_ISREG(mode):
91
            sourcedev = self._losetup(self.source)
92
        elif not stat.S_ISBLK(mode):
93
            raise ValueError("Value for self.source is invalid")
94

    
95
        # Take a snapshot and return it to the user
96
        size = blockdev('--getsize', sourcedev)
97
        cowfd, cow = tempfile.mkstemp()
98
        self._add_cleanup(os.unlink, cow)
99
        # Create 1G cow sparse file
100
        dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
101
        cowdev = self._losetup(cow)
102

    
103
        snapshot = uuid.uuid4().hex
104
        tablefd, table = tempfile.mkstemp()
105
        try:
106
            os.write(tablefd, "0 %d snapshot %s %s n 8" % \
107
                                        (int(size), sourcedev, cowdev))
108
            dmsetup('create', snapshot, table)
109
            self._add_cleanup(dmsetup, 'remove', snapshot)
110
        finally:
111
            os.unlink(table)
112

    
113
        new_device = DiskDevice("/dev/mapper/%s" % snapshot)
114
        self._devices.append(new_device)
115
        return new_device
116

    
117
    def destroy_device(self, device):
118
        """Destroys a DiskDevice instance previously created by
119
        get_device method.
120
        """
121
        self._devices.remove(device)
122
        device.destroy()
123

    
124

    
125
def progress_generator(total):
126
    position = 0;
127
    for i in progress.bar(range(total)):
128
        if i < position:
129
            continue
130
        position = yield
131
    yield #suppress the StopIteration exception
132

    
133

    
134
class DiskDevice(object):
135
    """This class represents a block device hosting an Operating System
136
    as created by the device-mapper.
137
    """
138

    
139
    def __init__(self, device, bootable=True):
140
        """Create a new DiskDevice."""
141
        self.device = device
142
        self.bootable = bootable
143
        self.progress_bar = None
144

    
145
        self.g = guestfs.GuestFS()
146
        self.g.add_drive_opts(device, readonly=0)
147

    
148
        #self.g.set_trace(1)
149
        #self.g.set_verbose(1)
150

    
151
        eh = self.g.set_event_callback(self.progress_callback, guestfs.EVENT_PROGRESS)
152
        self.g.launch()
153
        self.g.delete_event_callback(eh)
154
        
155
        roots = self.g.inspect_os()
156
        if len(roots) == 0:
157
            raise DiskError("No operating system found")
158
        if len(roots) > 1:
159
            raise DiskError("Multiple operating systems found")
160

    
161
        self.root = roots[0]
162
        self.ostype = self.g.inspect_get_type(self.root)
163
        self.distro = self.g.inspect_get_distro(self.root)
164

    
165
    def destroy(self):
166
        """Destroy this DiskDevice instance."""
167
        self.g.umount_all()
168
        self.g.sync()
169
        # Close the guestfs handler
170
        self.g.close()
171

    
172
    def progress_callback(self, ev, eh, buf, array):
173
        position = array[2]
174
        total = array[3]
175
        
176
        if self.progress_bar is None:
177
            self.progress_bar = progress_generator(total)
178
            self.progress_bar.next()
179

    
180
        self.progress_bar.send(position)
181

    
182
        if position == total:
183
            self.progress_bar = None
184

    
185
    def mount(self):
186
        """Mount all disk partitions in a correct order."""
187
        mps = self.g.inspect_get_mountpoints(self.root)
188

    
189
        # Sort the keys to mount the fs in a correct order.
190
        # / should be mounted befor /boot, etc
191
        def compare(a, b):
192
            if len(a[0]) > len(b[0]):
193
                return 1
194
            elif len(a[0]) == len(b[0]):
195
                return 0
196
            else:
197
                return -1
198
        mps.sort(compare)
199
        for mp, dev in mps:
200
            try:
201
                self.g.mount(dev, mp)
202
            except RuntimeError as msg:
203
                print "%s (ignored)" % msg
204

    
205
    def umount(self):
206
        """Umount all mounted filesystems."""
207
        self.g.umount_all()
208

    
209
    def shrink(self):
210
        """Shrink the disk.
211

212
        This is accomplished by shrinking the last filesystem in the
213
        disk and then updating the partition table. The new disk size
214
        (in bytes) is returned.
215
        """
216
        dev = self.g.part_to_dev(self.root)
217
        parttype = self.g.part_get_parttype(dev)
218
        if parttype != 'msdos':
219
            raise DiskError("You have a %s partition table. "
220
                "Only msdos partitions are supported" % parttype)
221

    
222
        last_partition = self.g.part_list(dev)[-1]
223

    
224
        if last_partition['part_num'] > 4:
225
            raise DiskError("This disk contains logical partitions. "
226
                "Only primary partitions are supported.")
227

    
228
        part_dev = "%s%d" % (dev, last_partition['part_num'])
229
        fs_type = self.g.vfs_type(part_dev)
230
        if not re.match("ext[234]", fs_type):
231
            print "Warning: Don't know how to resize %s partitions." % vfs_type
232
            return
233

    
234
        self.g.e2fsck_f(part_dev)
235
        self.g.resize2fs_M(part_dev)
236
        output = self.g.tune2fs_l(part_dev)
237
        block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
238
        block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
239

    
240
        sector_size = self.g.blockdev_getss(dev)
241

    
242
        start = last_partition['part_start'] / sector_size
243
        end = start + (block_size * block_cnt) / sector_size - 1
244

    
245
        self.g.part_del(dev, last_partition['part_num'])
246
        self.g.part_add(dev, 'p', start, end)
247

    
248
        return (end + 1) * sector_size
249

    
250
    def size(self):
251
        """Returns the "payload" size of the device.
252

253
        The size returned by this method is the size of the space occupied by
254
        the partitions (including the space before the first partition).
255
        """
256
        dev = self.g.part_to_dev(self.root)
257
        last = self.g.part_list(dev)[-1]
258

    
259
        return last['part_end']
260

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