Statistics
| Branch: | Tag: | Revision:

root / image_creator / disk.py @ e7c01d1e

History | View | Annotate | Download (7.8 kB)

1 d57775d4 Nikos Skalkotos
#!/usr/bin/env python
2 d57775d4 Nikos Skalkotos
3 d57775d4 Nikos Skalkotos
import losetup
4 d57775d4 Nikos Skalkotos
import stat
5 d57775d4 Nikos Skalkotos
import os
6 d57775d4 Nikos Skalkotos
import tempfile
7 d57775d4 Nikos Skalkotos
import uuid
8 d57775d4 Nikos Skalkotos
import re
9 1377b8a7 Nikos Skalkotos
import sys
10 1377b8a7 Nikos Skalkotos
import guestfs
11 1377b8a7 Nikos Skalkotos
12 01a7cff3 Nikos Skalkotos
import pbs
13 d57775d4 Nikos Skalkotos
from pbs import dd
14 586da0a0 Nikos Skalkotos
from clint.textui import progress
15 1377b8a7 Nikos Skalkotos
16 8c574358 Nikos Skalkotos
17 8c574358 Nikos Skalkotos
class DiskError(Exception):
18 8c574358 Nikos Skalkotos
    pass
19 8c574358 Nikos Skalkotos
20 d57775d4 Nikos Skalkotos
21 01a7cff3 Nikos Skalkotos
def find_sbin_command(command, exception):
22 01a7cff3 Nikos Skalkotos
    search_paths = ['/usr/local/sbin', '/usr/sbin', '/sbin']
23 01a7cff3 Nikos Skalkotos
    for fullpath in map(lambda x: "%s/%s" % (x, command), search_paths):
24 01a7cff3 Nikos Skalkotos
        if os.path.exists(fullpath) and os.access(fullpath, os.X_OK):
25 01a7cff3 Nikos Skalkotos
            return pbs.Command(fullpath)
26 01a7cff3 Nikos Skalkotos
        continue
27 01a7cff3 Nikos Skalkotos
    raise exception
28 01a7cff3 Nikos Skalkotos
29 01a7cff3 Nikos Skalkotos
30 01a7cff3 Nikos Skalkotos
try:
31 01a7cff3 Nikos Skalkotos
    from pbs import dmsetup
32 01a7cff3 Nikos Skalkotos
except pbs.CommandNotFound as e:
33 01a7cff3 Nikos Skalkotos
    dmsetup = find_sbin_command('dmsetup', e)
34 01a7cff3 Nikos Skalkotos
35 01a7cff3 Nikos Skalkotos
try:
36 01a7cff3 Nikos Skalkotos
    from pbs import blockdev
37 01a7cff3 Nikos Skalkotos
except pbs.CommandNotFound as e:
38 01a7cff3 Nikos Skalkotos
    blockdev = find_sbin_command('blockdev', e)
39 01a7cff3 Nikos Skalkotos
40 01a7cff3 Nikos Skalkotos
41 d57775d4 Nikos Skalkotos
class Disk(object):
42 3b2f6619 Nikos Skalkotos
    """This class represents a hard disk hosting an Operating System
43 3b2f6619 Nikos Skalkotos

44 3b2f6619 Nikos Skalkotos
    A Disk instance never alters the source media it is created from.
45 3b2f6619 Nikos Skalkotos
    Any change is done on a snapshot created by the device-mapper of
46 3b2f6619 Nikos Skalkotos
    the Linux kernel.
47 3b2f6619 Nikos Skalkotos
    """
48 d57775d4 Nikos Skalkotos
49 d57775d4 Nikos Skalkotos
    def __init__(self, source):
50 3b2f6619 Nikos Skalkotos
        """Create a new Disk instance out of a source media. The source
51 3b2f6619 Nikos Skalkotos
        media can be an image file, a block device or a directory."""
52 d57775d4 Nikos Skalkotos
        self._cleanup_jobs = []
53 d57775d4 Nikos Skalkotos
        self._devices = []
54 d57775d4 Nikos Skalkotos
        self.source = source
55 d57775d4 Nikos Skalkotos
56 d57775d4 Nikos Skalkotos
    def _add_cleanup(self, job, *args):
57 d57775d4 Nikos Skalkotos
        self._cleanup_jobs.append((job, args))
58 d57775d4 Nikos Skalkotos
59 d57775d4 Nikos Skalkotos
    def _losetup(self, fname):
60 d57775d4 Nikos Skalkotos
        loop = losetup.find_unused_loop_device()
61 d57775d4 Nikos Skalkotos
        loop.mount(fname)
62 d57775d4 Nikos Skalkotos
        self._add_cleanup(loop.unmount)
63 d57775d4 Nikos Skalkotos
        return loop.device
64 d57775d4 Nikos Skalkotos
65 d57775d4 Nikos Skalkotos
    def _dir_to_disk(self):
66 d57775d4 Nikos Skalkotos
        raise NotImplementedError
67 d57775d4 Nikos Skalkotos
68 d57775d4 Nikos Skalkotos
    def cleanup(self):
69 3b2f6619 Nikos Skalkotos
        """Cleanup internal data. This needs to be called before the
70 3b2f6619 Nikos Skalkotos
        program ends.
71 3b2f6619 Nikos Skalkotos
        """
72 1377b8a7 Nikos Skalkotos
        while len(self._devices):
73 1377b8a7 Nikos Skalkotos
            device = self._devices.pop()
74 1377b8a7 Nikos Skalkotos
            device.destroy()
75 8c574358 Nikos Skalkotos
76 d57775d4 Nikos Skalkotos
        while len(self._cleanup_jobs):
77 d57775d4 Nikos Skalkotos
            job, args = self._cleanup_jobs.pop()
78 d57775d4 Nikos Skalkotos
            job(*args)
79 d57775d4 Nikos Skalkotos
80 d57775d4 Nikos Skalkotos
    def get_device(self):
81 3b2f6619 Nikos Skalkotos
        """Returns a newly created DiskDevice instance.
82 c408053f Nikos Skalkotos

83 3b2f6619 Nikos Skalkotos
        This instance is a snapshot of the original source media of
84 3b2f6619 Nikos Skalkotos
        the Disk instance.
85 3b2f6619 Nikos Skalkotos
        """
86 d57775d4 Nikos Skalkotos
        sourcedev = self.source
87 d57775d4 Nikos Skalkotos
        mode = os.stat(self.source).st_mode
88 d57775d4 Nikos Skalkotos
        if stat.S_ISDIR(mode):
89 d57775d4 Nikos Skalkotos
            return self._losetup(self._dir_to_disk())
90 d57775d4 Nikos Skalkotos
        elif stat.S_ISREG(mode):
91 d57775d4 Nikos Skalkotos
            sourcedev = self._losetup(self.source)
92 d57775d4 Nikos Skalkotos
        elif not stat.S_ISBLK(mode):
93 d57775d4 Nikos Skalkotos
            raise ValueError("Value for self.source is invalid")
94 d57775d4 Nikos Skalkotos
95 d57775d4 Nikos Skalkotos
        # Take a snapshot and return it to the user
96 d57775d4 Nikos Skalkotos
        size = blockdev('--getsize', sourcedev)
97 d57775d4 Nikos Skalkotos
        cowfd, cow = tempfile.mkstemp()
98 d57775d4 Nikos Skalkotos
        self._add_cleanup(os.unlink, cow)
99 8c574358 Nikos Skalkotos
        # Create 1G cow sparse file
100 8c574358 Nikos Skalkotos
        dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
101 d57775d4 Nikos Skalkotos
        cowdev = self._losetup(cow)
102 d57775d4 Nikos Skalkotos
103 d57775d4 Nikos Skalkotos
        snapshot = uuid.uuid4().hex
104 d57775d4 Nikos Skalkotos
        tablefd, table = tempfile.mkstemp()
105 d57775d4 Nikos Skalkotos
        try:
106 d57775d4 Nikos Skalkotos
            os.write(tablefd, "0 %d snapshot %s %s n 8" % \
107 d57775d4 Nikos Skalkotos
                                        (int(size), sourcedev, cowdev))
108 d57775d4 Nikos Skalkotos
            dmsetup('create', snapshot, table)
109 d57775d4 Nikos Skalkotos
            self._add_cleanup(dmsetup, 'remove', snapshot)
110 d57775d4 Nikos Skalkotos
        finally:
111 d57775d4 Nikos Skalkotos
            os.unlink(table)
112 d57775d4 Nikos Skalkotos
113 1377b8a7 Nikos Skalkotos
        new_device = DiskDevice("/dev/mapper/%s" % snapshot)
114 d57775d4 Nikos Skalkotos
        self._devices.append(new_device)
115 d57775d4 Nikos Skalkotos
        return new_device
116 d57775d4 Nikos Skalkotos
117 1377b8a7 Nikos Skalkotos
    def destroy_device(self, device):
118 3b2f6619 Nikos Skalkotos
        """Destroys a DiskDevice instance previously created by
119 3b2f6619 Nikos Skalkotos
        get_device method.
120 3b2f6619 Nikos Skalkotos
        """
121 1377b8a7 Nikos Skalkotos
        self._devices.remove(device)
122 1377b8a7 Nikos Skalkotos
        device.destroy()
123 1377b8a7 Nikos Skalkotos
124 8c574358 Nikos Skalkotos
125 586da0a0 Nikos Skalkotos
def progress_generator(total):
126 586da0a0 Nikos Skalkotos
    position = 0;
127 586da0a0 Nikos Skalkotos
    for i in progress.bar(range(total)):
128 586da0a0 Nikos Skalkotos
        if i < position:
129 586da0a0 Nikos Skalkotos
            continue
130 586da0a0 Nikos Skalkotos
        position = yield
131 e7c01d1e Nikos Skalkotos
    yield #suppress the StopIteration exception
132 586da0a0 Nikos Skalkotos
133 586da0a0 Nikos Skalkotos
134 d57775d4 Nikos Skalkotos
class DiskDevice(object):
135 3b2f6619 Nikos Skalkotos
    """This class represents a block device hosting an Operating System
136 3b2f6619 Nikos Skalkotos
    as created by the device-mapper.
137 3b2f6619 Nikos Skalkotos
    """
138 d57775d4 Nikos Skalkotos
139 8c574358 Nikos Skalkotos
    def __init__(self, device, bootable=True):
140 3b2f6619 Nikos Skalkotos
        """Create a new DiskDevice."""
141 333ff548 Nikos Skalkotos
        self.device = device
142 1377b8a7 Nikos Skalkotos
        self.bootable = bootable
143 586da0a0 Nikos Skalkotos
        self.progress_bar = None
144 1377b8a7 Nikos Skalkotos
145 1377b8a7 Nikos Skalkotos
        self.g = guestfs.GuestFS()
146 586da0a0 Nikos Skalkotos
        self.g.add_drive_opts(device, readonly=0)
147 0d5a999d Nikos Skalkotos
148 586da0a0 Nikos Skalkotos
        #self.g.set_trace(1)
149 586da0a0 Nikos Skalkotos
        #self.g.set_verbose(1)
150 0d5a999d Nikos Skalkotos
151 586da0a0 Nikos Skalkotos
        eh = self.g.set_event_callback(self.progress_callback, guestfs.EVENT_PROGRESS)
152 1377b8a7 Nikos Skalkotos
        self.g.launch()
153 586da0a0 Nikos Skalkotos
        self.g.delete_event_callback(eh)
154 586da0a0 Nikos Skalkotos
        
155 1377b8a7 Nikos Skalkotos
        roots = self.g.inspect_os()
156 1377b8a7 Nikos Skalkotos
        if len(roots) == 0:
157 1377b8a7 Nikos Skalkotos
            raise DiskError("No operating system found")
158 1377b8a7 Nikos Skalkotos
        if len(roots) > 1:
159 1377b8a7 Nikos Skalkotos
            raise DiskError("Multiple operating systems found")
160 1377b8a7 Nikos Skalkotos
161 1377b8a7 Nikos Skalkotos
        self.root = roots[0]
162 aa2062ba Nikos Skalkotos
        self.ostype = self.g.inspect_get_type(self.root)
163 aa2062ba Nikos Skalkotos
        self.distro = self.g.inspect_get_distro(self.root)
164 8c574358 Nikos Skalkotos
165 1377b8a7 Nikos Skalkotos
    def destroy(self):
166 3b2f6619 Nikos Skalkotos
        """Destroy this DiskDevice instance."""
167 1377b8a7 Nikos Skalkotos
        self.g.umount_all()
168 1377b8a7 Nikos Skalkotos
        self.g.sync()
169 1377b8a7 Nikos Skalkotos
        # Close the guestfs handler
170 aa2062ba Nikos Skalkotos
        self.g.close()
171 8c574358 Nikos Skalkotos
172 586da0a0 Nikos Skalkotos
    def progress_callback(self, ev, eh, buf, array):
173 586da0a0 Nikos Skalkotos
        position = array[2]
174 586da0a0 Nikos Skalkotos
        total = array[3]
175 586da0a0 Nikos Skalkotos
        
176 586da0a0 Nikos Skalkotos
        if self.progress_bar is None:
177 586da0a0 Nikos Skalkotos
            self.progress_bar = progress_generator(total)
178 586da0a0 Nikos Skalkotos
            self.progress_bar.next()
179 586da0a0 Nikos Skalkotos
180 586da0a0 Nikos Skalkotos
        self.progress_bar.send(position)
181 586da0a0 Nikos Skalkotos
182 586da0a0 Nikos Skalkotos
        if position == total:
183 586da0a0 Nikos Skalkotos
            self.progress_bar = None
184 586da0a0 Nikos Skalkotos
185 1377b8a7 Nikos Skalkotos
    def mount(self):
186 3b2f6619 Nikos Skalkotos
        """Mount all disk partitions in a correct order."""
187 0d5a999d Nikos Skalkotos
        mps = self.g.inspect_get_mountpoints(self.root)
188 8c574358 Nikos Skalkotos
189 1377b8a7 Nikos Skalkotos
        # Sort the keys to mount the fs in a correct order.
190 1377b8a7 Nikos Skalkotos
        # / should be mounted befor /boot, etc
191 8c574358 Nikos Skalkotos
        def compare(a, b):
192 8c574358 Nikos Skalkotos
            if len(a[0]) > len(b[0]):
193 8c574358 Nikos Skalkotos
                return 1
194 8c574358 Nikos Skalkotos
            elif len(a[0]) == len(b[0]):
195 8c574358 Nikos Skalkotos
                return 0
196 8c574358 Nikos Skalkotos
            else:
197 8c574358 Nikos Skalkotos
                return -1
198 1377b8a7 Nikos Skalkotos
        mps.sort(compare)
199 1377b8a7 Nikos Skalkotos
        for mp, dev in mps:
200 1377b8a7 Nikos Skalkotos
            try:
201 1377b8a7 Nikos Skalkotos
                self.g.mount(dev, mp)
202 1377b8a7 Nikos Skalkotos
            except RuntimeError as msg:
203 1377b8a7 Nikos Skalkotos
                print "%s (ignored)" % msg
204 d57775d4 Nikos Skalkotos
205 8c574358 Nikos Skalkotos
    def umount(self):
206 3b2f6619 Nikos Skalkotos
        """Umount all mounted filesystems."""
207 8c574358 Nikos Skalkotos
        self.g.umount_all()
208 8c574358 Nikos Skalkotos
209 8c574358 Nikos Skalkotos
    def shrink(self):
210 3b2f6619 Nikos Skalkotos
        """Shrink the disk.
211 3b2f6619 Nikos Skalkotos

212 3b2f6619 Nikos Skalkotos
        This is accomplished by shrinking the last filesystem in the
213 3b2f6619 Nikos Skalkotos
        disk and then updating the partition table. The new disk size
214 3b2f6619 Nikos Skalkotos
        (in bytes) is returned.
215 3b2f6619 Nikos Skalkotos
        """
216 8c574358 Nikos Skalkotos
        dev = self.g.part_to_dev(self.root)
217 8c574358 Nikos Skalkotos
        parttype = self.g.part_get_parttype(dev)
218 8c574358 Nikos Skalkotos
        if parttype != 'msdos':
219 8c574358 Nikos Skalkotos
            raise DiskError("You have a %s partition table. "
220 8c574358 Nikos Skalkotos
                "Only msdos partitions are supported" % parttype)
221 8c574358 Nikos Skalkotos
222 8c574358 Nikos Skalkotos
        last_partition = self.g.part_list(dev)[-1]
223 8c574358 Nikos Skalkotos
224 8c574358 Nikos Skalkotos
        if last_partition['part_num'] > 4:
225 8c574358 Nikos Skalkotos
            raise DiskError("This disk contains logical partitions. "
226 8c574358 Nikos Skalkotos
                "Only primary partitions are supported.")
227 8c574358 Nikos Skalkotos
228 8c574358 Nikos Skalkotos
        part_dev = "%s%d" % (dev, last_partition['part_num'])
229 8c574358 Nikos Skalkotos
        fs_type = self.g.vfs_type(part_dev)
230 8c574358 Nikos Skalkotos
        if not re.match("ext[234]", fs_type):
231 a1a5ca4b Nikos Skalkotos
            print "Warning: Don't know how to resize %s partitions." % vfs_type
232 8c574358 Nikos Skalkotos
            return
233 8c574358 Nikos Skalkotos
234 8c574358 Nikos Skalkotos
        self.g.e2fsck_f(part_dev)
235 8c574358 Nikos Skalkotos
        self.g.resize2fs_M(part_dev)
236 8c574358 Nikos Skalkotos
        output = self.g.tune2fs_l(part_dev)
237 8c574358 Nikos Skalkotos
        block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
238 8c574358 Nikos Skalkotos
        block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
239 8c574358 Nikos Skalkotos
240 8c574358 Nikos Skalkotos
        sector_size = self.g.blockdev_getss(dev)
241 8c574358 Nikos Skalkotos
242 8c574358 Nikos Skalkotos
        start = last_partition['part_start'] / sector_size
243 8c574358 Nikos Skalkotos
        end = start + (block_size * block_cnt) / sector_size - 1
244 8c574358 Nikos Skalkotos
245 c408053f Nikos Skalkotos
        self.g.part_del(dev, last_partition['part_num'])
246 c408053f Nikos Skalkotos
        self.g.part_add(dev, 'p', start, end)
247 c408053f Nikos Skalkotos
248 e3aac3f9 Nikos Skalkotos
        return (end + 1) * sector_size
249 e3aac3f9 Nikos Skalkotos
250 c408053f Nikos Skalkotos
    def size(self):
251 c408053f Nikos Skalkotos
        """Returns the "payload" size of the device.
252 c408053f Nikos Skalkotos

253 c408053f Nikos Skalkotos
        The size returned by this method is the size of the space occupied by
254 c408053f Nikos Skalkotos
        the partitions (including the space before the first partition).
255 c408053f Nikos Skalkotos
        """
256 c408053f Nikos Skalkotos
        dev = self.g.part_to_dev(self.root)
257 c408053f Nikos Skalkotos
        last = self.g.part_list(dev)[-1]
258 c408053f Nikos Skalkotos
259 c408053f Nikos Skalkotos
        return last['part_end']
260 8c574358 Nikos Skalkotos
261 d57775d4 Nikos Skalkotos
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :