Statistics
| Branch: | Tag: | Revision:

root / image_creator / disk.py @ 8ec4da46

History | View | Annotate | Download (18.2 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 979096dd Nikos Skalkotos
from image_creator.util import get_command
35 e77e66a9 Nikos Skalkotos
from image_creator.util import FatalError
36 f3845095 Nikos Skalkotos
from image_creator.util import try_fail_repeat
37 c16850ae Nikos Skalkotos
from image_creator.util import free_space
38 331aa0ec Nikos Skalkotos
from image_creator.gpt import GPTPartitionTable
39 a939e3b8 Nikos Skalkotos
from image_creator.bundle_volume import BundleVolume
40 8eea5572 Nikos Skalkotos
41 d57775d4 Nikos Skalkotos
import stat
42 d57775d4 Nikos Skalkotos
import os
43 d57775d4 Nikos Skalkotos
import tempfile
44 d57775d4 Nikos Skalkotos
import uuid
45 d57775d4 Nikos Skalkotos
import re
46 1377b8a7 Nikos Skalkotos
import sys
47 1377b8a7 Nikos Skalkotos
import guestfs
48 f6ea8d02 Nikos Skalkotos
import shutil
49 d603d80d Nikos Skalkotos
from sendfile import sendfile
50 1377b8a7 Nikos Skalkotos
51 8c574358 Nikos Skalkotos
52 3ccb2618 Nikos Skalkotos
dd = get_command('dd')
53 3ccb2618 Nikos Skalkotos
dmsetup = get_command('dmsetup')
54 3ccb2618 Nikos Skalkotos
losetup = get_command('losetup')
55 3ccb2618 Nikos Skalkotos
blockdev = get_command('blockdev')
56 01a7cff3 Nikos Skalkotos
57 01a7cff3 Nikos Skalkotos
58 c16850ae Nikos Skalkotos
TMP_CANDIDATES = ['/var/tmp', os.path.expanduser('~'), '/mnt']
59 c16850ae Nikos Skalkotos
60 c16850ae Nikos Skalkotos
61 d57775d4 Nikos Skalkotos
class Disk(object):
62 3b2f6619 Nikos Skalkotos
    """This class represents a hard disk hosting an Operating System
63 3b2f6619 Nikos Skalkotos

64 3b2f6619 Nikos Skalkotos
    A Disk instance never alters the source media it is created from.
65 3b2f6619 Nikos Skalkotos
    Any change is done on a snapshot created by the device-mapper of
66 3b2f6619 Nikos Skalkotos
    the Linux kernel.
67 3b2f6619 Nikos Skalkotos
    """
68 d57775d4 Nikos Skalkotos
69 c16850ae Nikos Skalkotos
    def __init__(self, source, output, tmp=None):
70 3b2f6619 Nikos Skalkotos
        """Create a new Disk instance out of a source media. The source
71 f3845095 Nikos Skalkotos
        media can be an image file, a block device or a directory.
72 f3845095 Nikos Skalkotos
        """
73 d57775d4 Nikos Skalkotos
        self._cleanup_jobs = []
74 d57775d4 Nikos Skalkotos
        self._devices = []
75 d57775d4 Nikos Skalkotos
        self.source = source
76 e77e66a9 Nikos Skalkotos
        self.out = output
77 9517bf29 Nikos Skalkotos
        self.meta = {}
78 c16850ae Nikos Skalkotos
        self.tmp = tempfile.mkdtemp(prefix='.snf_image_creator.',
79 c16850ae Nikos Skalkotos
                                    dir=self._get_tmp_dir(tmp))
80 c16850ae Nikos Skalkotos
81 f6ea8d02 Nikos Skalkotos
        self._add_cleanup(shutil.rmtree, self.tmp)
82 c16850ae Nikos Skalkotos
83 c16850ae Nikos Skalkotos
    def _get_tmp_dir(self, default=None):
84 c16850ae Nikos Skalkotos
        if default is not None:
85 c16850ae Nikos Skalkotos
            return default
86 c16850ae Nikos Skalkotos
87 c16850ae Nikos Skalkotos
        space = map(free_space, TMP_CANDIDATES)
88 c16850ae Nikos Skalkotos
89 c16850ae Nikos Skalkotos
        max_idx = 0
90 c16850ae Nikos Skalkotos
        max_val = space[0]
91 c16850ae Nikos Skalkotos
        for i, val in zip(range(len(space)), space):
92 c16850ae Nikos Skalkotos
            if val > max_val:
93 c16850ae Nikos Skalkotos
                max_val = val
94 c16850ae Nikos Skalkotos
                max_idx = i
95 c16850ae Nikos Skalkotos
96 c16850ae Nikos Skalkotos
        # Return the candidate path with more available space
97 c16850ae Nikos Skalkotos
        return TMP_CANDIDATES[max_idx]
98 d57775d4 Nikos Skalkotos
99 d57775d4 Nikos Skalkotos
    def _add_cleanup(self, job, *args):
100 d57775d4 Nikos Skalkotos
        self._cleanup_jobs.append((job, args))
101 d57775d4 Nikos Skalkotos
102 d57775d4 Nikos Skalkotos
    def _losetup(self, fname):
103 3ccb2618 Nikos Skalkotos
        loop = losetup('-f', '--show', fname)
104 ae48a082 Nikos Skalkotos
        loop = loop.strip()  # remove the new-line char
105 f3845095 Nikos Skalkotos
        self._add_cleanup(try_fail_repeat, losetup, '-d', loop)
106 3ccb2618 Nikos Skalkotos
        return loop
107 d57775d4 Nikos Skalkotos
108 d57775d4 Nikos Skalkotos
    def _dir_to_disk(self):
109 9e4b4de2 Nikos Skalkotos
        if self.source == '/':
110 8eea5572 Nikos Skalkotos
            bundle = BundleVolume(self.out, self.meta)
111 c16850ae Nikos Skalkotos
            image = '%s/%s.diskdump' % (self.tmp, uuid.uuid4().hex)
112 567891a6 Nikos Skalkotos
113 567891a6 Nikos Skalkotos
            def check_unlink(path):
114 567891a6 Nikos Skalkotos
                if os.path.exists(path):
115 567891a6 Nikos Skalkotos
                    os.unlink(path)
116 567891a6 Nikos Skalkotos
117 567891a6 Nikos Skalkotos
            self._add_cleanup(check_unlink, image)
118 567891a6 Nikos Skalkotos
            bundle.create_image(image)
119 69a4f894 Nikos Skalkotos
            return self._losetup(image)
120 9e4b4de2 Nikos Skalkotos
        raise FatalError("Using a directory as media source is supported")
121 d57775d4 Nikos Skalkotos
122 d57775d4 Nikos Skalkotos
    def cleanup(self):
123 3b2f6619 Nikos Skalkotos
        """Cleanup internal data. This needs to be called before the
124 3b2f6619 Nikos Skalkotos
        program ends.
125 3b2f6619 Nikos Skalkotos
        """
126 9c354f13 Nikos Skalkotos
        try:
127 9c354f13 Nikos Skalkotos
            while len(self._devices):
128 9c354f13 Nikos Skalkotos
                device = self._devices.pop()
129 9c354f13 Nikos Skalkotos
                device.destroy()
130 9c354f13 Nikos Skalkotos
        finally:
131 9c354f13 Nikos Skalkotos
            # Make sure those are executed even if one of the device.destroy
132 9c354f13 Nikos Skalkotos
            # methods throws exeptions.
133 9c354f13 Nikos Skalkotos
            while len(self._cleanup_jobs):
134 9c354f13 Nikos Skalkotos
                job, args = self._cleanup_jobs.pop()
135 9c354f13 Nikos Skalkotos
                job(*args)
136 d57775d4 Nikos Skalkotos
137 e22aa3a9 Nikos Skalkotos
    def snapshot(self):
138 e22aa3a9 Nikos Skalkotos
        """Creates a snapshot of the original source media of the Disk
139 e22aa3a9 Nikos Skalkotos
        instance.
140 3b2f6619 Nikos Skalkotos
        """
141 22a6d232 Nikos Skalkotos
142 663f5f80 Nikos Skalkotos
        self.out.output("Examining source media `%s' ..." % self.source, False)
143 3f70f242 Nikos Skalkotos
        sourcedev = self.source
144 3f70f242 Nikos Skalkotos
        mode = os.stat(self.source).st_mode
145 3f70f242 Nikos Skalkotos
        if stat.S_ISDIR(mode):
146 279f2c7d Nikos Skalkotos
            self.out.success('looks like a directory')
147 8eea5572 Nikos Skalkotos
            return self._dir_to_disk()
148 3f70f242 Nikos Skalkotos
        elif stat.S_ISREG(mode):
149 279f2c7d Nikos Skalkotos
            self.out.success('looks like an image file')
150 3f70f242 Nikos Skalkotos
            sourcedev = self._losetup(self.source)
151 3f70f242 Nikos Skalkotos
        elif not stat.S_ISBLK(mode):
152 3f70f242 Nikos Skalkotos
            raise ValueError("Invalid media source. Only block devices, "
153 f99fe99d Nikos Skalkotos
                             "regular files and directories are supported.")
154 3f70f242 Nikos Skalkotos
        else:
155 e77e66a9 Nikos Skalkotos
            self.out.success('looks like a block device')
156 d57775d4 Nikos Skalkotos
157 d57775d4 Nikos Skalkotos
        # Take a snapshot and return it to the user
158 e77e66a9 Nikos Skalkotos
        self.out.output("Snapshotting media source...", False)
159 8fed77f7 Nikos Skalkotos
        size = blockdev('--getsz', sourcedev)
160 c16850ae Nikos Skalkotos
        cowfd, cow = tempfile.mkstemp(dir=self.tmp)
161 3f70f242 Nikos Skalkotos
        os.close(cowfd)
162 3f70f242 Nikos Skalkotos
        self._add_cleanup(os.unlink, cow)
163 8fed77f7 Nikos Skalkotos
        # Create cow sparse file
164 8fed77f7 Nikos Skalkotos
        dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
165 3f70f242 Nikos Skalkotos
        cowdev = self._losetup(cow)
166 3f70f242 Nikos Skalkotos
167 3f70f242 Nikos Skalkotos
        snapshot = uuid.uuid4().hex
168 3f70f242 Nikos Skalkotos
        tablefd, table = tempfile.mkstemp()
169 3f70f242 Nikos Skalkotos
        try:
170 f99fe99d Nikos Skalkotos
            os.write(tablefd, "0 %d snapshot %s %s n 8" %
171 f99fe99d Nikos Skalkotos
                              (int(size), sourcedev, cowdev))
172 3f70f242 Nikos Skalkotos
            dmsetup('create', snapshot, table)
173 f3845095 Nikos Skalkotos
            self._add_cleanup(try_fail_repeat, dmsetup, 'remove', snapshot)
174 3f70f242 Nikos Skalkotos
175 3f70f242 Nikos Skalkotos
        finally:
176 3f70f242 Nikos Skalkotos
            os.unlink(table)
177 e77e66a9 Nikos Skalkotos
        self.out.success('done')
178 e22aa3a9 Nikos Skalkotos
        return "/dev/mapper/%s" % snapshot
179 e22aa3a9 Nikos Skalkotos
180 e22aa3a9 Nikos Skalkotos
    def get_device(self, media):
181 e22aa3a9 Nikos Skalkotos
        """Returns a newly created DiskDevice instance."""
182 e22aa3a9 Nikos Skalkotos
183 e77e66a9 Nikos Skalkotos
        new_device = DiskDevice(media, self.out)
184 d57775d4 Nikos Skalkotos
        self._devices.append(new_device)
185 0db22eac Nikos Skalkotos
        new_device.enable()
186 d57775d4 Nikos Skalkotos
        return new_device
187 d57775d4 Nikos Skalkotos
188 1377b8a7 Nikos Skalkotos
    def destroy_device(self, device):
189 3b2f6619 Nikos Skalkotos
        """Destroys a DiskDevice instance previously created by
190 3b2f6619 Nikos Skalkotos
        get_device method.
191 3b2f6619 Nikos Skalkotos
        """
192 1377b8a7 Nikos Skalkotos
        self._devices.remove(device)
193 1377b8a7 Nikos Skalkotos
        device.destroy()
194 1377b8a7 Nikos Skalkotos
195 8c574358 Nikos Skalkotos
196 d57775d4 Nikos Skalkotos
class DiskDevice(object):
197 3b2f6619 Nikos Skalkotos
    """This class represents a block device hosting an Operating System
198 3b2f6619 Nikos Skalkotos
    as created by the device-mapper.
199 3b2f6619 Nikos Skalkotos
    """
200 d57775d4 Nikos Skalkotos
201 9517bf29 Nikos Skalkotos
    def __init__(self, device, output, bootable=True, meta={}):
202 3b2f6619 Nikos Skalkotos
        """Create a new DiskDevice."""
203 0db22eac Nikos Skalkotos
204 e108efd2 Nikos Skalkotos
        self.real_device = device
205 e77e66a9 Nikos Skalkotos
        self.out = output
206 1377b8a7 Nikos Skalkotos
        self.bootable = bootable
207 9517bf29 Nikos Skalkotos
        self.meta = meta
208 586da0a0 Nikos Skalkotos
        self.progress_bar = None
209 e108efd2 Nikos Skalkotos
        self.guestfs_device = None
210 dbf466eb Nikos Skalkotos
        self.size = 0
211 1377b8a7 Nikos Skalkotos
212 1377b8a7 Nikos Skalkotos
        self.g = guestfs.GuestFS()
213 8ec4da46 Nikos Skalkotos
        self.g.add_drive_opts(self.real_device, readonly=0, format="raw")
214 0d5a999d Nikos Skalkotos
215 5f27b178 Nikos Skalkotos
        # Before version 1.17.14 the recovery process, which is a fork of the
216 5f27b178 Nikos Skalkotos
        # original process that called libguestfs, did not close its inherited
217 5f27b178 Nikos Skalkotos
        # file descriptors. This can cause problems especially if the parent
218 5f27b178 Nikos Skalkotos
        # process has opened pipes. Since the recovery process is an optional
219 5f27b178 Nikos Skalkotos
        # feature of libguestfs, it's better to disable it.
220 5f27b178 Nikos Skalkotos
        self.g.set_recovery_proc(0)
221 5f27b178 Nikos Skalkotos
        version = self.g.version()
222 f99fe99d Nikos Skalkotos
        if version['major'] > 1 or \
223 f99fe99d Nikos Skalkotos
            (version['major'] == 1 and (version['minor'] >= 18 or
224 f99fe99d Nikos Skalkotos
                                        (version['minor'] == 17 and
225 f99fe99d Nikos Skalkotos
                                         version['release'] >= 14))):
226 5f27b178 Nikos Skalkotos
            self.g.set_recovery_proc(1)
227 5f27b178 Nikos Skalkotos
            self.out.output("Enabling recovery proc")
228 5f27b178 Nikos Skalkotos
229 586da0a0 Nikos Skalkotos
        #self.g.set_trace(1)
230 586da0a0 Nikos Skalkotos
        #self.g.set_verbose(1)
231 0d5a999d Nikos Skalkotos
232 0db22eac Nikos Skalkotos
        self.guestfs_enabled = False
233 ae48a082 Nikos Skalkotos
234 0db22eac Nikos Skalkotos
    def enable(self):
235 0db22eac Nikos Skalkotos
        """Enable a newly created DiskDevice"""
236 923d52df Nikos Skalkotos
237 923d52df Nikos Skalkotos
        self.out.output('Launching helper VM (may take a while) ...', False)
238 923d52df Nikos Skalkotos
        # self.progressbar = self.out.Progress(100, "Launching helper VM",
239 923d52df Nikos Skalkotos
        #                                     "percent")
240 923d52df Nikos Skalkotos
        # eh = self.g.set_event_callback(self.progress_callback,
241 923d52df Nikos Skalkotos
        #                               guestfs.EVENT_PROGRESS)
242 3f70f242 Nikos Skalkotos
        self.g.launch()
243 3f70f242 Nikos Skalkotos
        self.guestfs_enabled = True
244 923d52df Nikos Skalkotos
        # self.g.delete_event_callback(eh)
245 923d52df Nikos Skalkotos
        # self.progressbar.success('done')
246 923d52df Nikos Skalkotos
        # self.progressbar = None
247 923d52df Nikos Skalkotos
        self.out.success('done')
248 3f70f242 Nikos Skalkotos
249 25b4d858 Nikos Skalkotos
        self.out.output('Inspecting Operating System ...', False)
250 3f70f242 Nikos Skalkotos
        roots = self.g.inspect_os()
251 3f70f242 Nikos Skalkotos
        if len(roots) == 0:
252 3f70f242 Nikos Skalkotos
            raise FatalError("No operating system found")
253 3f70f242 Nikos Skalkotos
        if len(roots) > 1:
254 3f70f242 Nikos Skalkotos
            raise FatalError("Multiple operating systems found."
255 835171dc Nikos Skalkotos
                             "We only support images with one OS.")
256 3f70f242 Nikos Skalkotos
        self.root = roots[0]
257 e108efd2 Nikos Skalkotos
        self.guestfs_device = self.g.part_to_dev(self.root)
258 dbf466eb Nikos Skalkotos
        self.size = self.g.blockdev_getsize64(self.guestfs_device)
259 e8b1b48b Nikos Skalkotos
        self.meta['PARTITION_TABLE'] = \
260 f99fe99d Nikos Skalkotos
            self.g.part_get_parttype(self.guestfs_device)
261 331aa0ec Nikos Skalkotos
262 3f70f242 Nikos Skalkotos
        self.ostype = self.g.inspect_get_type(self.root)
263 3f70f242 Nikos Skalkotos
        self.distro = self.g.inspect_get_distro(self.root)
264 e77e66a9 Nikos Skalkotos
        self.out.success('found a(n) %s system' % self.distro)
265 8c574358 Nikos Skalkotos
266 1377b8a7 Nikos Skalkotos
    def destroy(self):
267 3b2f6619 Nikos Skalkotos
        """Destroy this DiskDevice instance."""
268 0db22eac Nikos Skalkotos
269 9c354f13 Nikos Skalkotos
        # In new guestfs versions, there is a handy shutdown method for this
270 9c354f13 Nikos Skalkotos
        try:
271 9c354f13 Nikos Skalkotos
            if self.guestfs_enabled:
272 9c354f13 Nikos Skalkotos
                self.g.umount_all()
273 9c354f13 Nikos Skalkotos
                self.g.sync()
274 9c354f13 Nikos Skalkotos
        finally:
275 9c354f13 Nikos Skalkotos
            # Close the guestfs handler if open
276 9c354f13 Nikos Skalkotos
            self.g.close()
277 8c574358 Nikos Skalkotos
278 923d52df Nikos Skalkotos
#    def progress_callback(self, ev, eh, buf, array):
279 923d52df Nikos Skalkotos
#        position = array[2]
280 923d52df Nikos Skalkotos
#        total = array[3]
281 923d52df Nikos Skalkotos
#
282 923d52df Nikos Skalkotos
#        self.progressbar.goto((position * 100) // total)
283 586da0a0 Nikos Skalkotos
284 550d4a49 Nikos Skalkotos
    def mount(self, readonly=False):
285 3b2f6619 Nikos Skalkotos
        """Mount all disk partitions in a correct order."""
286 979096dd Nikos Skalkotos
287 550d4a49 Nikos Skalkotos
        mount = self.g.mount_ro if readonly else self.g.mount
288 a803b449 Nikos Skalkotos
        msg = " read-only" if readonly else ""
289 663f5f80 Nikos Skalkotos
        self.out.output("Mounting the media%s ..." % msg, False)
290 0d5a999d Nikos Skalkotos
        mps = self.g.inspect_get_mountpoints(self.root)
291 8c574358 Nikos Skalkotos
292 1377b8a7 Nikos Skalkotos
        # Sort the keys to mount the fs in a correct order.
293 1377b8a7 Nikos Skalkotos
        # / should be mounted befor /boot, etc
294 8c574358 Nikos Skalkotos
        def compare(a, b):
295 8c574358 Nikos Skalkotos
            if len(a[0]) > len(b[0]):
296 8c574358 Nikos Skalkotos
                return 1
297 8c574358 Nikos Skalkotos
            elif len(a[0]) == len(b[0]):
298 8c574358 Nikos Skalkotos
                return 0
299 8c574358 Nikos Skalkotos
            else:
300 8c574358 Nikos Skalkotos
                return -1
301 1377b8a7 Nikos Skalkotos
        mps.sort(compare)
302 1377b8a7 Nikos Skalkotos
        for mp, dev in mps:
303 1377b8a7 Nikos Skalkotos
            try:
304 550d4a49 Nikos Skalkotos
                mount(dev, mp)
305 1377b8a7 Nikos Skalkotos
            except RuntimeError as msg:
306 e77e66a9 Nikos Skalkotos
                self.out.warn("%s (ignored)" % msg)
307 e77e66a9 Nikos Skalkotos
        self.out.success("done")
308 d57775d4 Nikos Skalkotos
309 8c574358 Nikos Skalkotos
    def umount(self):
310 3b2f6619 Nikos Skalkotos
        """Umount all mounted filesystems."""
311 8c574358 Nikos Skalkotos
        self.g.umount_all()
312 8c574358 Nikos Skalkotos
313 e8b1b48b Nikos Skalkotos
    def _last_partition(self):
314 e8b1b48b Nikos Skalkotos
        if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
315 e8b1b48b Nikos Skalkotos
            msg = "Unsupported partition table: %s. Only msdos and gpt " \
316 f99fe99d Nikos Skalkotos
                "partition tables are supported" % self.meta['PARTITION_TABLE']
317 e8b1b48b Nikos Skalkotos
            raise FatalError(msg)
318 e8b1b48b Nikos Skalkotos
319 f99fe99d Nikos Skalkotos
        is_extended = lambda p: \
320 7f9ecc51 Nikos Skalkotos
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
321 7f9ecc51 Nikos Skalkotos
            in (0x5, 0xf)
322 f99fe99d Nikos Skalkotos
        is_logical = lambda p: \
323 7f9ecc51 Nikos Skalkotos
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
324 e8b1b48b Nikos Skalkotos
325 e8b1b48b Nikos Skalkotos
        partitions = self.g.part_list(self.guestfs_device)
326 e8b1b48b Nikos Skalkotos
        last_partition = partitions[-1]
327 e8b1b48b Nikos Skalkotos
328 e8b1b48b Nikos Skalkotos
        if is_logical(last_partition):
329 e8b1b48b Nikos Skalkotos
            # The disk contains extended and logical partitions....
330 7f9ecc51 Nikos Skalkotos
            extended = filter(is_extended, partitions)[0]
331 e8b1b48b Nikos Skalkotos
            last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
332 e8b1b48b Nikos Skalkotos
333 e8b1b48b Nikos Skalkotos
            # check if extended is the last primary partition
334 e8b1b48b Nikos Skalkotos
            if last_primary['part_num'] > extended['part_num']:
335 e8b1b48b Nikos Skalkotos
                last_partition = last_primary
336 e8b1b48b Nikos Skalkotos
337 e8b1b48b Nikos Skalkotos
        return last_partition
338 e8b1b48b Nikos Skalkotos
339 8c574358 Nikos Skalkotos
    def shrink(self):
340 3b2f6619 Nikos Skalkotos
        """Shrink the disk.
341 3b2f6619 Nikos Skalkotos

342 3b2f6619 Nikos Skalkotos
        This is accomplished by shrinking the last filesystem in the
343 3b2f6619 Nikos Skalkotos
        disk and then updating the partition table. The new disk size
344 3b2f6619 Nikos Skalkotos
        (in bytes) is returned.
345 e108efd2 Nikos Skalkotos

346 e108efd2 Nikos Skalkotos
        ATTENTION: make sure unmount is called before shrink
347 3b2f6619 Nikos Skalkotos
        """
348 f99fe99d Nikos Skalkotos
        get_fstype = lambda p: \
349 f99fe99d Nikos Skalkotos
            self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
350 f99fe99d Nikos Skalkotos
        is_logical = lambda p: \
351 f99fe99d Nikos Skalkotos
            self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
352 f99fe99d Nikos Skalkotos
        is_extended = lambda p: \
353 f99fe99d Nikos Skalkotos
            self.meta['PARTITION_TABLE'] == 'msdos' and \
354 7f9ecc51 Nikos Skalkotos
            self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) \
355 7f9ecc51 Nikos Skalkotos
            in (0x5, 0xf)
356 e8b1b48b Nikos Skalkotos
357 e8b1b48b Nikos Skalkotos
        part_add = lambda ptype, start, stop: \
358 f99fe99d Nikos Skalkotos
            self.g.part_add(self.guestfs_device, ptype, start, stop)
359 e8b1b48b Nikos Skalkotos
        part_del = lambda p: self.g.part_del(self.guestfs_device, p)
360 e8b1b48b Nikos Skalkotos
        part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
361 f99fe99d Nikos Skalkotos
        part_set_id = lambda p, id: \
362 f99fe99d Nikos Skalkotos
            self.g.part_set_mbr_id(self.guestfs_device, p, id)
363 f99fe99d Nikos Skalkotos
        part_get_bootable = lambda p: \
364 f99fe99d Nikos Skalkotos
            self.g.part_get_bootable(self.guestfs_device, p)
365 f99fe99d Nikos Skalkotos
        part_set_bootable = lambda p, bootable: \
366 f99fe99d Nikos Skalkotos
            self.g.part_set_bootable(self.guestfs_device, p, bootable)
367 e8b1b48b Nikos Skalkotos
368 e8b1b48b Nikos Skalkotos
        MB = 2 ** 20
369 8c574358 Nikos Skalkotos
370 663f5f80 Nikos Skalkotos
        self.out.output("Shrinking image (this may take a while) ...", False)
371 8c574358 Nikos Skalkotos
372 9666a511 Nikos Skalkotos
        sector_size = self.g.blockdev_getss(self.guestfs_device)
373 9666a511 Nikos Skalkotos
374 e8b1b48b Nikos Skalkotos
        last_part = None
375 e8b1b48b Nikos Skalkotos
        fstype = None
376 e8b1b48b Nikos Skalkotos
        while True:
377 e8b1b48b Nikos Skalkotos
            last_part = self._last_partition()
378 e8b1b48b Nikos Skalkotos
            fstype = get_fstype(last_part)
379 e8b1b48b Nikos Skalkotos
380 e8b1b48b Nikos Skalkotos
            if fstype == 'swap':
381 e8b1b48b Nikos Skalkotos
                self.meta['SWAP'] = "%d:%s" % \
382 f99fe99d Nikos Skalkotos
                    (last_part['part_num'],
383 f99fe99d Nikos Skalkotos
                     (last_part['part_size'] + MB - 1) // MB)
384 e8b1b48b Nikos Skalkotos
                part_del(last_part['part_num'])
385 e8b1b48b Nikos Skalkotos
                continue
386 e8b1b48b Nikos Skalkotos
            elif is_extended(last_part):
387 e8b1b48b Nikos Skalkotos
                part_del(last_part['part_num'])
388 e8b1b48b Nikos Skalkotos
                continue
389 e8b1b48b Nikos Skalkotos
390 9666a511 Nikos Skalkotos
            # Most disk manipulation programs leave 2048 sectors after the last
391 9666a511 Nikos Skalkotos
            # partition
392 9666a511 Nikos Skalkotos
            new_size = last_part['part_end'] + 1 + 2048 * sector_size
393 dbf466eb Nikos Skalkotos
            self.size = min(self.size, new_size)
394 e8b1b48b Nikos Skalkotos
            break
395 e8b1b48b Nikos Skalkotos
396 e8b1b48b Nikos Skalkotos
        if not re.match("ext[234]", fstype):
397 e77e66a9 Nikos Skalkotos
            self.out.warn("Don't know how to resize %s partitions." % fstype)
398 dbf466eb Nikos Skalkotos
            return self.size
399 e8b1b48b Nikos Skalkotos
400 e8b1b48b Nikos Skalkotos
        part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
401 3f70f242 Nikos Skalkotos
        self.g.e2fsck_f(part_dev)
402 3f70f242 Nikos Skalkotos
        self.g.resize2fs_M(part_dev)
403 3f70f242 Nikos Skalkotos
404 6f319b6a Nikos Skalkotos
        out = self.g.tune2fs_l(part_dev)
405 7f9ecc51 Nikos Skalkotos
        block_size = int(filter(lambda x: x[0] == 'Block size', out)[0][1])
406 7f9ecc51 Nikos Skalkotos
        block_cnt = int(filter(lambda x: x[0] == 'Block count', out)[0][1])
407 22a6d232 Nikos Skalkotos
408 e8b1b48b Nikos Skalkotos
        start = last_part['part_start'] / sector_size
409 3f70f242 Nikos Skalkotos
        end = start + (block_size * block_cnt) / sector_size - 1
410 8c574358 Nikos Skalkotos
411 e8b1b48b Nikos Skalkotos
        if is_logical(last_part):
412 e8b1b48b Nikos Skalkotos
            partitions = self.g.part_list(self.guestfs_device)
413 e8b1b48b Nikos Skalkotos
414 e8b1b48b Nikos Skalkotos
            logical = []  # logical partitions
415 e8b1b48b Nikos Skalkotos
            for partition in partitions:
416 e8b1b48b Nikos Skalkotos
                if partition['part_num'] < 4:
417 e8b1b48b Nikos Skalkotos
                    continue
418 e8b1b48b Nikos Skalkotos
                logical.append({
419 e8b1b48b Nikos Skalkotos
                    'num': partition['part_num'],
420 e8b1b48b Nikos Skalkotos
                    'start': partition['part_start'] / sector_size,
421 e8b1b48b Nikos Skalkotos
                    'end': partition['part_end'] / sector_size,
422 6d0788a9 Nikos Skalkotos
                    'id': part_get_id(partition['part_num']),
423 e8b1b48b Nikos Skalkotos
                    'bootable': part_get_bootable(partition['part_num'])
424 e8b1b48b Nikos Skalkotos
                })
425 e8b1b48b Nikos Skalkotos
426 e8b1b48b Nikos Skalkotos
            logical[-1]['end'] = end  # new end after resize
427 e8b1b48b Nikos Skalkotos
428 e8b1b48b Nikos Skalkotos
            # Recreate the extended partition
429 7f9ecc51 Nikos Skalkotos
            extended = filter(is_extended, partitions)[0]
430 e8b1b48b Nikos Skalkotos
            part_del(extended['part_num'])
431 6d0788a9 Nikos Skalkotos
            part_add('e', extended['part_start'] / sector_size, end)
432 e8b1b48b Nikos Skalkotos
433 e8b1b48b Nikos Skalkotos
            # Create all the logical partitions back
434 e8b1b48b Nikos Skalkotos
            for l in logical:
435 e8b1b48b Nikos Skalkotos
                part_add('l', l['start'], l['end'])
436 e8b1b48b Nikos Skalkotos
                part_set_id(l['num'], l['id'])
437 e8b1b48b Nikos Skalkotos
                part_set_bootable(l['num'], l['bootable'])
438 e8b1b48b Nikos Skalkotos
        else:
439 e8b1b48b Nikos Skalkotos
            # Recreate the last partition
440 e8b1b48b Nikos Skalkotos
            if self.meta['PARTITION_TABLE'] == 'msdos':
441 e8b1b48b Nikos Skalkotos
                last_part['id'] = part_get_id(last_part['part_num'])
442 e8b1b48b Nikos Skalkotos
443 e8b1b48b Nikos Skalkotos
            last_part['bootable'] = part_get_bootable(last_part['part_num'])
444 e8b1b48b Nikos Skalkotos
            part_del(last_part['part_num'])
445 e8b1b48b Nikos Skalkotos
            part_add('p', start, end)
446 e8b1b48b Nikos Skalkotos
            part_set_bootable(last_part['part_num'], last_part['bootable'])
447 8c574358 Nikos Skalkotos
448 e8b1b48b Nikos Skalkotos
            if self.meta['PARTITION_TABLE'] == 'msdos':
449 e8b1b48b Nikos Skalkotos
                part_set_id(last_part['part_num'], last_part['id'])
450 331aa0ec Nikos Skalkotos
451 e8b1b48b Nikos Skalkotos
        new_size = (end + 1) * sector_size
452 9666a511 Nikos Skalkotos
453 dbf466eb Nikos Skalkotos
        assert (new_size <= self.size)
454 e8b1b48b Nikos Skalkotos
455 e8b1b48b Nikos Skalkotos
        if self.meta['PARTITION_TABLE'] == 'gpt':
456 e108efd2 Nikos Skalkotos
            ptable = GPTPartitionTable(self.real_device)
457 dbf466eb Nikos Skalkotos
            self.size = ptable.shrink(new_size, self.size)
458 e8b1b48b Nikos Skalkotos
        else:
459 dbf466eb Nikos Skalkotos
            self.size = min(new_size + 2048 * sector_size, self.size)
460 9666a511 Nikos Skalkotos
461 dbf466eb Nikos Skalkotos
        self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
462 e3aac3f9 Nikos Skalkotos
463 dbf466eb Nikos Skalkotos
        return self.size
464 8c574358 Nikos Skalkotos
465 d603d80d Nikos Skalkotos
    def dump(self, outfile):
466 d603d80d Nikos Skalkotos
        """Dumps the content of device into a file.
467 d603d80d Nikos Skalkotos

468 d603d80d Nikos Skalkotos
        This method will only dump the actual payload, found by reading the
469 d603d80d Nikos Skalkotos
        partition table. Empty space in the end of the device will be ignored.
470 d603d80d Nikos Skalkotos
        """
471 e8b1b48b Nikos Skalkotos
        MB = 2 ** 20
472 e8b1b48b Nikos Skalkotos
        blocksize = 4 * MB  # 4MB
473 dbf466eb Nikos Skalkotos
        size = self.size
474 96171db1 Nikos Skalkotos
        progr_size = (size + MB - 1) // MB  # in MB
475 96171db1 Nikos Skalkotos
        progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
476 5b801534 Nikos Skalkotos
477 5b801534 Nikos Skalkotos
        with open(self.real_device, 'r') as src:
478 5b801534 Nikos Skalkotos
            with open(outfile, "w") as dst:
479 e8b1b48b Nikos Skalkotos
                left = size
480 d603d80d Nikos Skalkotos
                offset = 0
481 d603d80d Nikos Skalkotos
                progressbar.next()
482 d603d80d Nikos Skalkotos
                while left > 0:
483 d603d80d Nikos Skalkotos
                    length = min(left, blocksize)
484 717b0434 Nikos Skalkotos
                    sent = sendfile(dst.fileno(), src.fileno(), offset, length)
485 717b0434 Nikos Skalkotos
486 717b0434 Nikos Skalkotos
                    # Workaround for python-sendfile API change. In
487 717b0434 Nikos Skalkotos
                    # python-sendfile 1.2.x (py-sendfile) the returning value
488 717b0434 Nikos Skalkotos
                    # of sendfile is a tuple, where in version 2.x (pysendfile)
489 717b0434 Nikos Skalkotos
                    # it is just a sigle integer.
490 717b0434 Nikos Skalkotos
                    if isinstance(sent, tuple):
491 717b0434 Nikos Skalkotos
                        sent = sent[1]
492 717b0434 Nikos Skalkotos
493 d603d80d Nikos Skalkotos
                    offset += sent
494 d603d80d Nikos Skalkotos
                    left -= sent
495 e8b1b48b Nikos Skalkotos
                    progressbar.goto((size - left) // MB)
496 e77e66a9 Nikos Skalkotos
        progressbar.success('image file %s was successfully created' % outfile)
497 d603d80d Nikos Skalkotos
498 d57775d4 Nikos Skalkotos
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :