1 # Copyright 2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
34 from image_creator.util import get_command
35 from image_creator.util import FatalError
36 from image_creator.gpt import GPTPartitionTable
37 from image_creator.bundle_volume import BundleVolume
47 from sendfile import sendfile
50 dd = get_command('dd')
51 dmsetup = get_command('dmsetup')
52 losetup = get_command('losetup')
53 blockdev = get_command('blockdev')
57 """This class represents a hard disk hosting an Operating System
59 A Disk instance never alters the source media it is created from.
60 Any change is done on a snapshot created by the device-mapper of
64 def __init__(self, source, output):
65 """Create a new Disk instance out of a source media. The source
66 media can be an image file, a block device or a directory."""
67 self._cleanup_jobs = []
73 def _add_cleanup(self, job, *args):
74 self._cleanup_jobs.append((job, args))
76 def _losetup(self, fname):
77 loop = losetup('-f', '--show', fname)
78 loop = loop.strip() # remove the new-line char
79 self._add_cleanup(losetup, '-d', loop)
82 def _dir_to_disk(self):
83 if self.source == '/':
84 bundle = BundleVolume(self.out, self.meta)
85 image = bundle.create_image()
86 self._add_cleanup(os.unlink, image)
87 return self._losetup(image)
88 raise FatalError("Using a directory as media source is supported")
91 """Cleanup internal data. This needs to be called before the
95 while len(self._devices):
96 device = self._devices.pop()
99 # Make sure those are executed even if one of the device.destroy
100 # methods throws exeptions.
101 while len(self._cleanup_jobs):
102 job, args = self._cleanup_jobs.pop()
106 """Creates a snapshot of the original source media of the Disk
110 self.out.output("Examining source media `%s'..." % self.source, False)
111 sourcedev = self.source
112 mode = os.stat(self.source).st_mode
113 if stat.S_ISDIR(mode):
114 self.out.success('looks like a directory')
115 return self._dir_to_disk()
116 elif stat.S_ISREG(mode):
117 self.out.success('looks like an image file')
118 sourcedev = self._losetup(self.source)
119 elif not stat.S_ISBLK(mode):
120 raise ValueError("Invalid media source. Only block devices, "
121 "regular files and directories are supported.")
123 self.out.success('looks like a block device')
125 # Take a snapshot and return it to the user
126 self.out.output("Snapshotting media source...", False)
127 size = blockdev('--getsz', sourcedev)
128 cowfd, cow = tempfile.mkstemp()
130 self._add_cleanup(os.unlink, cow)
131 # Create cow sparse file
132 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
133 cowdev = self._losetup(cow)
135 snapshot = uuid.uuid4().hex
136 tablefd, table = tempfile.mkstemp()
138 os.write(tablefd, "0 %d snapshot %s %s n 8" %
139 (int(size), sourcedev, cowdev))
140 dmsetup('create', snapshot, table)
141 self._add_cleanup(dmsetup, 'remove', snapshot)
142 # Sometimes dmsetup remove fails with Device or resource busy,
143 # although everything is cleaned up and the snapshot is not
144 # used by anyone. Add a 2 seconds delay to be on the safe side.
145 self._add_cleanup(time.sleep, 2)
149 self.out.success('done')
150 return "/dev/mapper/%s" % snapshot
152 def get_device(self, media):
153 """Returns a newly created DiskDevice instance."""
155 new_device = DiskDevice(media, self.out)
156 self._devices.append(new_device)
160 def destroy_device(self, device):
161 """Destroys a DiskDevice instance previously created by
164 self._devices.remove(device)
168 class DiskDevice(object):
169 """This class represents a block device hosting an Operating System
170 as created by the device-mapper.
173 def __init__(self, device, output, bootable=True, meta={}):
174 """Create a new DiskDevice."""
176 self.real_device = device
178 self.bootable = bootable
180 self.progress_bar = None
181 self.guestfs_device = None
184 self.g = guestfs.GuestFS()
185 self.g.add_drive_opts(self.real_device, readonly=0)
187 # Before version 1.17.14 the recovery process, which is a fork of the
188 # original process that called libguestfs, did not close its inherited
189 # file descriptors. This can cause problems especially if the parent
190 # process has opened pipes. Since the recovery process is an optional
191 # feature of libguestfs, it's better to disable it.
192 self.g.set_recovery_proc(0)
193 version = self.g.version()
194 if version['major'] > 1 or \
195 (version['major'] == 1 and (version['minor'] >= 18 or
196 (version['minor'] == 17 and
197 version['release'] >= 14))):
198 self.g.set_recovery_proc(1)
199 self.out.output("Enabling recovery proc")
202 #self.g.set_verbose(1)
204 self.guestfs_enabled = False
207 """Enable a newly created DiskDevice"""
208 self.progressbar = self.out.Progress(100, "Launching helper VM",
210 eh = self.g.set_event_callback(self.progress_callback,
211 guestfs.EVENT_PROGRESS)
213 self.guestfs_enabled = True
214 self.g.delete_event_callback(eh)
215 self.progressbar.success('done')
216 self.progressbar = None
218 self.out.output('Inspecting Operating System ...', False)
219 roots = self.g.inspect_os()
221 raise FatalError("No operating system found")
223 raise FatalError("Multiple operating systems found."
224 "We only support images with one OS.")
226 self.guestfs_device = self.g.part_to_dev(self.root)
227 self.size = self.g.blockdev_getsize64(self.guestfs_device)
228 self.meta['PARTITION_TABLE'] = \
229 self.g.part_get_parttype(self.guestfs_device)
231 self.ostype = self.g.inspect_get_type(self.root)
232 self.distro = self.g.inspect_get_distro(self.root)
233 self.out.success('found a(n) %s system' % self.distro)
236 """Destroy this DiskDevice instance."""
238 # In new guestfs versions, there is a handy shutdown method for this
240 if self.guestfs_enabled:
244 # Close the guestfs handler if open
247 def progress_callback(self, ev, eh, buf, array):
251 self.progressbar.goto((position * 100) // total)
253 def mount(self, readonly=False):
254 """Mount all disk partitions in a correct order."""
256 mount = self.g.mount_ro if readonly else self.g.mount
257 msg = " read-only" if readonly else ""
258 self.out.output("Mounting the media%s..." % msg, False)
259 mps = self.g.inspect_get_mountpoints(self.root)
261 # Sort the keys to mount the fs in a correct order.
262 # / should be mounted befor /boot, etc
264 if len(a[0]) > len(b[0]):
266 elif len(a[0]) == len(b[0]):
274 except RuntimeError as msg:
275 self.out.warn("%s (ignored)" % msg)
276 self.out.success("done")
279 """Umount all mounted filesystems."""
282 def _last_partition(self):
283 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
284 msg = "Unsupported partition table: %s. Only msdos and gpt " \
285 "partition tables are supported" % self.meta['PARTITION_TABLE']
286 raise FatalError(msg)
288 is_extended = lambda p: \
289 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
290 is_logical = lambda p: \
291 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
293 partitions = self.g.part_list(self.guestfs_device)
294 last_partition = partitions[-1]
296 if is_logical(last_partition):
297 # The disk contains extended and logical partitions....
298 extended = [p for p in partitions if is_extended(p)][0]
299 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
301 # check if extended is the last primary partition
302 if last_primary['part_num'] > extended['part_num']:
303 last_partition = last_primary
305 return last_partition
310 This is accomplished by shrinking the last filesystem in the
311 disk and then updating the partition table. The new disk size
312 (in bytes) is returned.
314 ATTENTION: make sure unmount is called before shrink
316 get_fstype = lambda p: \
317 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
318 is_logical = lambda p: \
319 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
320 is_extended = lambda p: \
321 self.meta['PARTITION_TABLE'] == 'msdos' and \
322 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
324 part_add = lambda ptype, start, stop: \
325 self.g.part_add(self.guestfs_device, ptype, start, stop)
326 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
327 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
328 part_set_id = lambda p, id: \
329 self.g.part_set_mbr_id(self.guestfs_device, p, id)
330 part_get_bootable = lambda p: \
331 self.g.part_get_bootable(self.guestfs_device, p)
332 part_set_bootable = lambda p, bootable: \
333 self.g.part_set_bootable(self.guestfs_device, p, bootable)
337 self.out.output("Shrinking image (this may take a while)...", False)
339 sector_size = self.g.blockdev_getss(self.guestfs_device)
344 last_part = self._last_partition()
345 fstype = get_fstype(last_part)
348 self.meta['SWAP'] = "%d:%s" % \
349 (last_part['part_num'],
350 (last_part['part_size'] + MB - 1) // MB)
351 part_del(last_part['part_num'])
353 elif is_extended(last_part):
354 part_del(last_part['part_num'])
357 # Most disk manipulation programs leave 2048 sectors after the last
359 new_size = last_part['part_end'] + 1 + 2048 * sector_size
360 self.size = min(self.size, new_size)
363 if not re.match("ext[234]", fstype):
364 self.out.warn("Don't know how to resize %s partitions." % fstype)
367 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
368 self.g.e2fsck_f(part_dev)
369 self.g.resize2fs_M(part_dev)
371 out = self.g.tune2fs_l(part_dev)
373 filter(lambda x: x[0] == 'Block size', out)[0][1])
375 filter(lambda x: x[0] == 'Block count', out)[0][1])
377 start = last_part['part_start'] / sector_size
378 end = start + (block_size * block_cnt) / sector_size - 1
380 if is_logical(last_part):
381 partitions = self.g.part_list(self.guestfs_device)
383 logical = [] # logical partitions
384 for partition in partitions:
385 if partition['part_num'] < 4:
388 'num': partition['part_num'],
389 'start': partition['part_start'] / sector_size,
390 'end': partition['part_end'] / sector_size,
391 'id': part_get_(partition['part_num']),
392 'bootable': part_get_bootable(partition['part_num'])
395 logical[-1]['end'] = end # new end after resize
397 # Recreate the extended partition
398 extended = [p for p in partitions if self._is_extended(p)][0]
399 part_del(extended['part_num'])
400 part_add('e', extended['part_start'], end)
402 # Create all the logical partitions back
404 part_add('l', l['start'], l['end'])
405 part_set_id(l['num'], l['id'])
406 part_set_bootable(l['num'], l['bootable'])
408 # Recreate the last partition
409 if self.meta['PARTITION_TABLE'] == 'msdos':
410 last_part['id'] = part_get_id(last_part['part_num'])
412 last_part['bootable'] = part_get_bootable(last_part['part_num'])
413 part_del(last_part['part_num'])
414 part_add('p', start, end)
415 part_set_bootable(last_part['part_num'], last_part['bootable'])
417 if self.meta['PARTITION_TABLE'] == 'msdos':
418 part_set_id(last_part['part_num'], last_part['id'])
420 new_size = (end + 1) * sector_size
422 assert (new_size <= self.size)
424 if self.meta['PARTITION_TABLE'] == 'gpt':
425 ptable = GPTPartitionTable(self.real_device)
426 self.size = ptable.shrink(new_size, self.size)
428 self.size = min(new_size + 2048 * sector_size, self.size)
430 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
434 def dump(self, outfile):
435 """Dumps the content of device into a file.
437 This method will only dump the actual payload, found by reading the
438 partition table. Empty space in the end of the device will be ignored.
441 blocksize = 4 * MB # 4MB
443 progr_size = (size + MB - 1) // MB # in MB
444 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
446 with open(self.real_device, 'r') as src:
447 with open(outfile, "w") as dst:
452 length = min(left, blocksize)
453 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
457 progressbar.goto((size - left) // MB)
458 progressbar.success('image file %s was successfully created' % outfile)
460 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :