Statistics
| Branch: | Tag: | Revision:

root / image_creator / disk.py @ d144e954

History | View | Annotate | Download (10.3 kB)

1 ae48a082 Nikos Skalkotos
# Copyright 2012 GRNET S.A. All rights reserved.
2 ae48a082 Nikos Skalkotos
#
3 ae48a082 Nikos Skalkotos
# Redistribution and use in source and binary forms, with or
4 ae48a082 Nikos Skalkotos
# without modification, are permitted provided that the following
5 ae48a082 Nikos Skalkotos
# conditions are met:
6 ae48a082 Nikos Skalkotos
#
7 ae48a082 Nikos Skalkotos
#   1. Redistributions of source code must retain the above
8 ae48a082 Nikos Skalkotos
#      copyright notice, this list of conditions and the following
9 ae48a082 Nikos Skalkotos
#      disclaimer.
10 ae48a082 Nikos Skalkotos
#
11 ae48a082 Nikos Skalkotos
#   2. Redistributions in binary form must reproduce the above
12 ae48a082 Nikos Skalkotos
#      copyright notice, this list of conditions and the following
13 ae48a082 Nikos Skalkotos
#      disclaimer in the documentation and/or other materials
14 ae48a082 Nikos Skalkotos
#      provided with the distribution.
15 ae48a082 Nikos Skalkotos
#
16 ae48a082 Nikos Skalkotos
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 ae48a082 Nikos Skalkotos
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 ae48a082 Nikos Skalkotos
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 ae48a082 Nikos Skalkotos
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 ae48a082 Nikos Skalkotos
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 ae48a082 Nikos Skalkotos
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 ae48a082 Nikos Skalkotos
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 ae48a082 Nikos Skalkotos
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 ae48a082 Nikos Skalkotos
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 ae48a082 Nikos Skalkotos
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 ae48a082 Nikos Skalkotos
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 ae48a082 Nikos Skalkotos
# POSSIBILITY OF SUCH DAMAGE.
28 ae48a082 Nikos Skalkotos
#
29 ae48a082 Nikos Skalkotos
# The views and conclusions contained in the software and
30 ae48a082 Nikos Skalkotos
# documentation are those of the authors and should not be
31 ae48a082 Nikos Skalkotos
# interpreted as representing official policies, either expressed
32 ae48a082 Nikos Skalkotos
# or implied, of GRNET S.A.
33 d57775d4 Nikos Skalkotos
34 22a6d232 Nikos Skalkotos
from image_creator.util import get_command, warn, progress_generator
35 0ae01e26 Nikos Skalkotos
from image_creator import FatalError
36 22a6d232 Nikos Skalkotos
from clint.textui import indent, puts, colored
37 3ccb2618 Nikos Skalkotos
38 d57775d4 Nikos Skalkotos
import stat
39 d57775d4 Nikos Skalkotos
import os
40 d57775d4 Nikos Skalkotos
import tempfile
41 d57775d4 Nikos Skalkotos
import uuid
42 d57775d4 Nikos Skalkotos
import re
43 1377b8a7 Nikos Skalkotos
import sys
44 1377b8a7 Nikos Skalkotos
import guestfs
45 0db22eac Nikos Skalkotos
import time
46 1377b8a7 Nikos Skalkotos
47 8c574358 Nikos Skalkotos
48 8c574358 Nikos Skalkotos
class DiskError(Exception):
49 8c574358 Nikos Skalkotos
    pass
50 8c574358 Nikos Skalkotos
51 3ccb2618 Nikos Skalkotos
dd = get_command('dd')
52 3ccb2618 Nikos Skalkotos
dmsetup = get_command('dmsetup')
53 3ccb2618 Nikos Skalkotos
losetup = get_command('losetup')
54 3ccb2618 Nikos Skalkotos
blockdev = get_command('blockdev')
55 01a7cff3 Nikos Skalkotos
56 01a7cff3 Nikos Skalkotos
57 d57775d4 Nikos Skalkotos
class Disk(object):
58 3b2f6619 Nikos Skalkotos
    """This class represents a hard disk hosting an Operating System
59 3b2f6619 Nikos Skalkotos

60 3b2f6619 Nikos Skalkotos
    A Disk instance never alters the source media it is created from.
61 3b2f6619 Nikos Skalkotos
    Any change is done on a snapshot created by the device-mapper of
62 3b2f6619 Nikos Skalkotos
    the Linux kernel.
63 3b2f6619 Nikos Skalkotos
    """
64 d57775d4 Nikos Skalkotos
65 d57775d4 Nikos Skalkotos
    def __init__(self, source):
66 3b2f6619 Nikos Skalkotos
        """Create a new Disk instance out of a source media. The source
67 3b2f6619 Nikos Skalkotos
        media can be an image file, a block device or a directory."""
68 d57775d4 Nikos Skalkotos
        self._cleanup_jobs = []
69 d57775d4 Nikos Skalkotos
        self._devices = []
70 d57775d4 Nikos Skalkotos
        self.source = source
71 d57775d4 Nikos Skalkotos
72 d57775d4 Nikos Skalkotos
    def _add_cleanup(self, job, *args):
73 d57775d4 Nikos Skalkotos
        self._cleanup_jobs.append((job, args))
74 d57775d4 Nikos Skalkotos
75 d57775d4 Nikos Skalkotos
    def _losetup(self, fname):
76 3ccb2618 Nikos Skalkotos
        loop = losetup('-f', '--show', fname)
77 ae48a082 Nikos Skalkotos
        loop = loop.strip()  # remove the new-line char
78 3ccb2618 Nikos Skalkotos
        self._add_cleanup(losetup, '-d', loop)
79 3ccb2618 Nikos Skalkotos
        return loop
80 d57775d4 Nikos Skalkotos
81 d57775d4 Nikos Skalkotos
    def _dir_to_disk(self):
82 d57775d4 Nikos Skalkotos
        raise NotImplementedError
83 d57775d4 Nikos Skalkotos
84 d57775d4 Nikos Skalkotos
    def cleanup(self):
85 3b2f6619 Nikos Skalkotos
        """Cleanup internal data. This needs to be called before the
86 3b2f6619 Nikos Skalkotos
        program ends.
87 3b2f6619 Nikos Skalkotos
        """
88 1377b8a7 Nikos Skalkotos
        while len(self._devices):
89 1377b8a7 Nikos Skalkotos
            device = self._devices.pop()
90 1377b8a7 Nikos Skalkotos
            device.destroy()
91 8c574358 Nikos Skalkotos
92 d57775d4 Nikos Skalkotos
        while len(self._cleanup_jobs):
93 d57775d4 Nikos Skalkotos
            job, args = self._cleanup_jobs.pop()
94 d57775d4 Nikos Skalkotos
            job(*args)
95 d57775d4 Nikos Skalkotos
96 d57775d4 Nikos Skalkotos
    def get_device(self):
97 3b2f6619 Nikos Skalkotos
        """Returns a newly created DiskDevice instance.
98 c408053f Nikos Skalkotos

99 3b2f6619 Nikos Skalkotos
        This instance is a snapshot of the original source media of
100 3b2f6619 Nikos Skalkotos
        the Disk instance.
101 3b2f6619 Nikos Skalkotos
        """
102 22a6d232 Nikos Skalkotos
103 3f70f242 Nikos Skalkotos
        puts("Examining source media `%s'..." % self.source, False)
104 3f70f242 Nikos Skalkotos
        sourcedev = self.source
105 3f70f242 Nikos Skalkotos
        mode = os.stat(self.source).st_mode
106 3f70f242 Nikos Skalkotos
        if stat.S_ISDIR(mode):
107 3f70f242 Nikos Skalkotos
            puts(colored.green('looks like a directory'))
108 3f70f242 Nikos Skalkotos
            return self._losetup(self._dir_to_disk())
109 3f70f242 Nikos Skalkotos
        elif stat.S_ISREG(mode):
110 3f70f242 Nikos Skalkotos
            puts(colored.green('looks like an image file'))
111 3f70f242 Nikos Skalkotos
            sourcedev = self._losetup(self.source)
112 3f70f242 Nikos Skalkotos
        elif not stat.S_ISBLK(mode):
113 3f70f242 Nikos Skalkotos
            raise ValueError("Invalid media source. Only block devices, "
114 3f70f242 Nikos Skalkotos
                            "regular files and directories are supported.")
115 3f70f242 Nikos Skalkotos
        else:
116 3f70f242 Nikos Skalkotos
            puts(colored.green('looks like a block device'))
117 d57775d4 Nikos Skalkotos
118 d57775d4 Nikos Skalkotos
        # Take a snapshot and return it to the user
119 3f70f242 Nikos Skalkotos
        puts("Snapshotting media source...", False)
120 3f70f242 Nikos Skalkotos
        size = blockdev('--getsize', sourcedev)
121 3f70f242 Nikos Skalkotos
        cowfd, cow = tempfile.mkstemp()
122 3f70f242 Nikos Skalkotos
        os.close(cowfd)
123 3f70f242 Nikos Skalkotos
        self._add_cleanup(os.unlink, cow)
124 3f70f242 Nikos Skalkotos
        # Create 1G cow sparse file
125 3f70f242 Nikos Skalkotos
        dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', \
126 3f70f242 Nikos Skalkotos
                                        'seek=%d' % (1024 * 1024))
127 3f70f242 Nikos Skalkotos
        cowdev = self._losetup(cow)
128 3f70f242 Nikos Skalkotos
129 3f70f242 Nikos Skalkotos
        snapshot = uuid.uuid4().hex
130 3f70f242 Nikos Skalkotos
        tablefd, table = tempfile.mkstemp()
131 3f70f242 Nikos Skalkotos
        try:
132 3f70f242 Nikos Skalkotos
            os.write(tablefd, "0 %d snapshot %s %s n 8" % \
133 3f70f242 Nikos Skalkotos
                                        (int(size), sourcedev, cowdev))
134 3f70f242 Nikos Skalkotos
            dmsetup('create', snapshot, table)
135 3f70f242 Nikos Skalkotos
            self._add_cleanup(dmsetup, 'remove', snapshot)
136 3f70f242 Nikos Skalkotos
            # Sometimes dmsetup remove fails with Device or resource busy,
137 3f70f242 Nikos Skalkotos
            # although everything is cleaned up and the snapshot is not
138 3f70f242 Nikos Skalkotos
            # used by anyone. Add a 2 seconds delay to be on the safe side.
139 3f70f242 Nikos Skalkotos
            self._add_cleanup(time.sleep, 2)
140 3f70f242 Nikos Skalkotos
141 3f70f242 Nikos Skalkotos
        finally:
142 3f70f242 Nikos Skalkotos
            os.unlink(table)
143 3f70f242 Nikos Skalkotos
        puts(colored.green('done'))
144 1377b8a7 Nikos Skalkotos
        new_device = DiskDevice("/dev/mapper/%s" % snapshot)
145 d57775d4 Nikos Skalkotos
        self._devices.append(new_device)
146 0db22eac Nikos Skalkotos
        new_device.enable()
147 d57775d4 Nikos Skalkotos
        return new_device
148 d57775d4 Nikos Skalkotos
149 1377b8a7 Nikos Skalkotos
    def destroy_device(self, device):
150 3b2f6619 Nikos Skalkotos
        """Destroys a DiskDevice instance previously created by
151 3b2f6619 Nikos Skalkotos
        get_device method.
152 3b2f6619 Nikos Skalkotos
        """
153 1377b8a7 Nikos Skalkotos
        self._devices.remove(device)
154 1377b8a7 Nikos Skalkotos
        device.destroy()
155 1377b8a7 Nikos Skalkotos
156 8c574358 Nikos Skalkotos
157 d57775d4 Nikos Skalkotos
class DiskDevice(object):
158 3b2f6619 Nikos Skalkotos
    """This class represents a block device hosting an Operating System
159 3b2f6619 Nikos Skalkotos
    as created by the device-mapper.
160 3b2f6619 Nikos Skalkotos
    """
161 d57775d4 Nikos Skalkotos
162 8c574358 Nikos Skalkotos
    def __init__(self, device, bootable=True):
163 3b2f6619 Nikos Skalkotos
        """Create a new DiskDevice."""
164 0db22eac Nikos Skalkotos
165 333ff548 Nikos Skalkotos
        self.device = device
166 1377b8a7 Nikos Skalkotos
        self.bootable = bootable
167 586da0a0 Nikos Skalkotos
        self.progress_bar = None
168 1377b8a7 Nikos Skalkotos
169 1377b8a7 Nikos Skalkotos
        self.g = guestfs.GuestFS()
170 0db22eac Nikos Skalkotos
        self.g.add_drive_opts(self.device, readonly=0)
171 0d5a999d Nikos Skalkotos
172 586da0a0 Nikos Skalkotos
        #self.g.set_trace(1)
173 586da0a0 Nikos Skalkotos
        #self.g.set_verbose(1)
174 0d5a999d Nikos Skalkotos
175 0db22eac Nikos Skalkotos
        self.guestfs_enabled = False
176 ae48a082 Nikos Skalkotos
177 0db22eac Nikos Skalkotos
    def enable(self):
178 0db22eac Nikos Skalkotos
        """Enable a newly created DiskDevice"""
179 22a6d232 Nikos Skalkotos
        self.progressbar = progress_generator("Launching helper VM: ")
180 3f70f242 Nikos Skalkotos
        self.progressbar.next()
181 3f70f242 Nikos Skalkotos
        eh = self.g.set_event_callback(self.progress_callback,
182 3f70f242 Nikos Skalkotos
                                                    guestfs.EVENT_PROGRESS)
183 3f70f242 Nikos Skalkotos
        self.g.launch()
184 3f70f242 Nikos Skalkotos
        self.guestfs_enabled = True
185 3f70f242 Nikos Skalkotos
        self.g.delete_event_callback(eh)
186 3f70f242 Nikos Skalkotos
        if self.progressbar is not None:
187 3f70f242 Nikos Skalkotos
            self.progressbar.send(100)
188 3f70f242 Nikos Skalkotos
            self.progressbar = None
189 3f70f242 Nikos Skalkotos
190 3f70f242 Nikos Skalkotos
        puts('Inspecting Operating System...', False)
191 3f70f242 Nikos Skalkotos
        roots = self.g.inspect_os()
192 3f70f242 Nikos Skalkotos
        if len(roots) == 0:
193 3f70f242 Nikos Skalkotos
            raise FatalError("No operating system found")
194 3f70f242 Nikos Skalkotos
        if len(roots) > 1:
195 3f70f242 Nikos Skalkotos
            raise FatalError("Multiple operating systems found."
196 3f70f242 Nikos Skalkotos
                            "We only support images with one filesystem.")
197 3f70f242 Nikos Skalkotos
        self.root = roots[0]
198 3f70f242 Nikos Skalkotos
        self.ostype = self.g.inspect_get_type(self.root)
199 3f70f242 Nikos Skalkotos
        self.distro = self.g.inspect_get_distro(self.root)
200 3f70f242 Nikos Skalkotos
        puts(colored.green('found a %s system' % self.distro))
201 8c574358 Nikos Skalkotos
202 1377b8a7 Nikos Skalkotos
    def destroy(self):
203 3b2f6619 Nikos Skalkotos
        """Destroy this DiskDevice instance."""
204 0db22eac Nikos Skalkotos
205 0db22eac Nikos Skalkotos
        if self.guestfs_enabled:
206 0db22eac Nikos Skalkotos
            self.g.umount_all()
207 0db22eac Nikos Skalkotos
            self.g.sync()
208 0db22eac Nikos Skalkotos
209 0db22eac Nikos Skalkotos
        # Close the guestfs handler if open
210 aa2062ba Nikos Skalkotos
        self.g.close()
211 8c574358 Nikos Skalkotos
212 586da0a0 Nikos Skalkotos
    def progress_callback(self, ev, eh, buf, array):
213 586da0a0 Nikos Skalkotos
        position = array[2]
214 586da0a0 Nikos Skalkotos
        total = array[3]
215 586da0a0 Nikos Skalkotos
216 ae48a082 Nikos Skalkotos
        self.progressbar.send((position * 100) // total)
217 586da0a0 Nikos Skalkotos
218 586da0a0 Nikos Skalkotos
        if position == total:
219 c54fc0e8 Nikos Skalkotos
            self.progressbar = None
220 586da0a0 Nikos Skalkotos
221 1377b8a7 Nikos Skalkotos
    def mount(self):
222 3b2f6619 Nikos Skalkotos
        """Mount all disk partitions in a correct order."""
223 0d5a999d Nikos Skalkotos
        mps = self.g.inspect_get_mountpoints(self.root)
224 8c574358 Nikos Skalkotos
225 1377b8a7 Nikos Skalkotos
        # Sort the keys to mount the fs in a correct order.
226 1377b8a7 Nikos Skalkotos
        # / should be mounted befor /boot, etc
227 8c574358 Nikos Skalkotos
        def compare(a, b):
228 8c574358 Nikos Skalkotos
            if len(a[0]) > len(b[0]):
229 8c574358 Nikos Skalkotos
                return 1
230 8c574358 Nikos Skalkotos
            elif len(a[0]) == len(b[0]):
231 8c574358 Nikos Skalkotos
                return 0
232 8c574358 Nikos Skalkotos
            else:
233 8c574358 Nikos Skalkotos
                return -1
234 1377b8a7 Nikos Skalkotos
        mps.sort(compare)
235 1377b8a7 Nikos Skalkotos
        for mp, dev in mps:
236 1377b8a7 Nikos Skalkotos
            try:
237 1377b8a7 Nikos Skalkotos
                self.g.mount(dev, mp)
238 1377b8a7 Nikos Skalkotos
            except RuntimeError as msg:
239 1377b8a7 Nikos Skalkotos
                print "%s (ignored)" % msg
240 d57775d4 Nikos Skalkotos
241 8c574358 Nikos Skalkotos
    def umount(self):
242 3b2f6619 Nikos Skalkotos
        """Umount all mounted filesystems."""
243 8c574358 Nikos Skalkotos
        self.g.umount_all()
244 8c574358 Nikos Skalkotos
245 8c574358 Nikos Skalkotos
    def shrink(self):
246 3b2f6619 Nikos Skalkotos
        """Shrink the disk.
247 3b2f6619 Nikos Skalkotos

248 3b2f6619 Nikos Skalkotos
        This is accomplished by shrinking the last filesystem in the
249 3b2f6619 Nikos Skalkotos
        disk and then updating the partition table. The new disk size
250 3b2f6619 Nikos Skalkotos
        (in bytes) is returned.
251 3b2f6619 Nikos Skalkotos
        """
252 3f70f242 Nikos Skalkotos
        puts("Shrinking image (this may take a while)...", False)
253 3f70f242 Nikos Skalkotos
254 8c574358 Nikos Skalkotos
        dev = self.g.part_to_dev(self.root)
255 8c574358 Nikos Skalkotos
        parttype = self.g.part_get_parttype(dev)
256 8c574358 Nikos Skalkotos
        if parttype != 'msdos':
257 0ae01e26 Nikos Skalkotos
            raise FatalError("You have a %s partition table. "
258 8c574358 Nikos Skalkotos
                "Only msdos partitions are supported" % parttype)
259 8c574358 Nikos Skalkotos
260 8c574358 Nikos Skalkotos
        last_partition = self.g.part_list(dev)[-1]
261 8c574358 Nikos Skalkotos
262 8c574358 Nikos Skalkotos
        if last_partition['part_num'] > 4:
263 0ae01e26 Nikos Skalkotos
            raise FatalError("This disk contains logical partitions. "
264 8c574358 Nikos Skalkotos
                "Only primary partitions are supported.")
265 8c574358 Nikos Skalkotos
266 8c574358 Nikos Skalkotos
        part_dev = "%s%d" % (dev, last_partition['part_num'])
267 8c574358 Nikos Skalkotos
        fs_type = self.g.vfs_type(part_dev)
268 8c574358 Nikos Skalkotos
        if not re.match("ext[234]", fs_type):
269 22a6d232 Nikos Skalkotos
            warn("Don't know how to resize %s partitions." % vfs_type)
270 8c574358 Nikos Skalkotos
            return
271 8c574358 Nikos Skalkotos
272 3f70f242 Nikos Skalkotos
        self.g.e2fsck_f(part_dev)
273 3f70f242 Nikos Skalkotos
        self.g.resize2fs_M(part_dev)
274 3f70f242 Nikos Skalkotos
275 3f70f242 Nikos Skalkotos
        output = self.g.tune2fs_l(part_dev)
276 3f70f242 Nikos Skalkotos
        block_size = int(
277 3f70f242 Nikos Skalkotos
            filter(lambda x: x[0] == 'Block size', output)[0][1])
278 3f70f242 Nikos Skalkotos
        block_cnt = int(
279 3f70f242 Nikos Skalkotos
            filter(lambda x: x[0] == 'Block count', output)[0][1])
280 22a6d232 Nikos Skalkotos
281 3f70f242 Nikos Skalkotos
        sector_size = self.g.blockdev_getss(dev)
282 8c574358 Nikos Skalkotos
283 3f70f242 Nikos Skalkotos
        start = last_partition['part_start'] / sector_size
284 3f70f242 Nikos Skalkotos
        end = start + (block_size * block_cnt) / sector_size - 1
285 8c574358 Nikos Skalkotos
286 3f70f242 Nikos Skalkotos
        self.g.part_del(dev, last_partition['part_num'])
287 3f70f242 Nikos Skalkotos
        self.g.part_add(dev, 'p', start, end)
288 8c574358 Nikos Skalkotos
289 3f70f242 Nikos Skalkotos
        new_size = (end + 1) * sector_size
290 3f70f242 Nikos Skalkotos
        puts(colored.green("new image size is %dMB\n" % (new_size // 2 ** 20)))
291 c408053f Nikos Skalkotos
292 22a6d232 Nikos Skalkotos
        return new_size
293 e3aac3f9 Nikos Skalkotos
294 c408053f Nikos Skalkotos
    def size(self):
295 c408053f Nikos Skalkotos
        """Returns the "payload" size of the device.
296 c408053f Nikos Skalkotos

297 c408053f Nikos Skalkotos
        The size returned by this method is the size of the space occupied by
298 c408053f Nikos Skalkotos
        the partitions (including the space before the first partition).
299 c408053f Nikos Skalkotos
        """
300 c408053f Nikos Skalkotos
        dev = self.g.part_to_dev(self.root)
301 c408053f Nikos Skalkotos
        last = self.g.part_list(dev)[-1]
302 c408053f Nikos Skalkotos
303 13675429 Nikos Skalkotos
        return last['part_end'] + 1
304 8c574358 Nikos Skalkotos
305 d57775d4 Nikos Skalkotos
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :