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"""
209 self.out.output('Launching helper VM (may take a while) ...', False)
210 # self.progressbar = self.out.Progress(100, "Launching helper VM",
212 # eh = self.g.set_event_callback(self.progress_callback,
213 # guestfs.EVENT_PROGRESS)
215 self.guestfs_enabled = True
216 # self.g.delete_event_callback(eh)
217 # self.progressbar.success('done')
218 # self.progressbar = None
219 self.out.success('done')
221 self.out.output('Inspecting Operating System ...', False)
222 roots = self.g.inspect_os()
224 raise FatalError("No operating system found")
226 raise FatalError("Multiple operating systems found."
227 "We only support images with one OS.")
229 self.guestfs_device = self.g.part_to_dev(self.root)
230 self.size = self.g.blockdev_getsize64(self.guestfs_device)
231 self.meta['PARTITION_TABLE'] = \
232 self.g.part_get_parttype(self.guestfs_device)
234 self.ostype = self.g.inspect_get_type(self.root)
235 self.distro = self.g.inspect_get_distro(self.root)
236 self.out.success('found a(n) %s system' % self.distro)
239 """Destroy this DiskDevice instance."""
241 # In new guestfs versions, there is a handy shutdown method for this
243 if self.guestfs_enabled:
247 # Close the guestfs handler if open
250 # def progress_callback(self, ev, eh, buf, array):
251 # position = array[2]
254 # self.progressbar.goto((position * 100) // total)
256 def mount(self, readonly=False):
257 """Mount all disk partitions in a correct order."""
259 mount = self.g.mount_ro if readonly else self.g.mount
260 msg = " read-only" if readonly else ""
261 self.out.output("Mounting the media%s..." % msg, False)
262 mps = self.g.inspect_get_mountpoints(self.root)
264 # Sort the keys to mount the fs in a correct order.
265 # / should be mounted befor /boot, etc
267 if len(a[0]) > len(b[0]):
269 elif len(a[0]) == len(b[0]):
277 except RuntimeError as msg:
278 self.out.warn("%s (ignored)" % msg)
279 self.out.success("done")
282 """Umount all mounted filesystems."""
285 def _last_partition(self):
286 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
287 msg = "Unsupported partition table: %s. Only msdos and gpt " \
288 "partition tables are supported" % self.meta['PARTITION_TABLE']
289 raise FatalError(msg)
291 is_extended = lambda p: \
292 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
293 is_logical = lambda p: \
294 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
296 partitions = self.g.part_list(self.guestfs_device)
297 last_partition = partitions[-1]
299 if is_logical(last_partition):
300 # The disk contains extended and logical partitions....
301 extended = [p for p in partitions if is_extended(p)][0]
302 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
304 # check if extended is the last primary partition
305 if last_primary['part_num'] > extended['part_num']:
306 last_partition = last_primary
308 return last_partition
313 This is accomplished by shrinking the last filesystem in the
314 disk and then updating the partition table. The new disk size
315 (in bytes) is returned.
317 ATTENTION: make sure unmount is called before shrink
319 get_fstype = lambda p: \
320 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
321 is_logical = lambda p: \
322 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
323 is_extended = lambda p: \
324 self.meta['PARTITION_TABLE'] == 'msdos' and \
325 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
327 part_add = lambda ptype, start, stop: \
328 self.g.part_add(self.guestfs_device, ptype, start, stop)
329 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
330 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
331 part_set_id = lambda p, id: \
332 self.g.part_set_mbr_id(self.guestfs_device, p, id)
333 part_get_bootable = lambda p: \
334 self.g.part_get_bootable(self.guestfs_device, p)
335 part_set_bootable = lambda p, bootable: \
336 self.g.part_set_bootable(self.guestfs_device, p, bootable)
340 self.out.output("Shrinking image (this may take a while)...", False)
342 sector_size = self.g.blockdev_getss(self.guestfs_device)
347 last_part = self._last_partition()
348 fstype = get_fstype(last_part)
351 self.meta['SWAP'] = "%d:%s" % \
352 (last_part['part_num'],
353 (last_part['part_size'] + MB - 1) // MB)
354 part_del(last_part['part_num'])
356 elif is_extended(last_part):
357 part_del(last_part['part_num'])
360 # Most disk manipulation programs leave 2048 sectors after the last
362 new_size = last_part['part_end'] + 1 + 2048 * sector_size
363 self.size = min(self.size, new_size)
366 if not re.match("ext[234]", fstype):
367 self.out.warn("Don't know how to resize %s partitions." % fstype)
370 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
371 self.g.e2fsck_f(part_dev)
372 self.g.resize2fs_M(part_dev)
374 out = self.g.tune2fs_l(part_dev)
376 filter(lambda x: x[0] == 'Block size', out)[0][1])
378 filter(lambda x: x[0] == 'Block count', out)[0][1])
380 start = last_part['part_start'] / sector_size
381 end = start + (block_size * block_cnt) / sector_size - 1
383 if is_logical(last_part):
384 partitions = self.g.part_list(self.guestfs_device)
386 logical = [] # logical partitions
387 for partition in partitions:
388 if partition['part_num'] < 4:
391 'num': partition['part_num'],
392 'start': partition['part_start'] / sector_size,
393 'end': partition['part_end'] / sector_size,
394 'id': part_get_(partition['part_num']),
395 'bootable': part_get_bootable(partition['part_num'])
398 logical[-1]['end'] = end # new end after resize
400 # Recreate the extended partition
401 extended = [p for p in partitions if self._is_extended(p)][0]
402 part_del(extended['part_num'])
403 part_add('e', extended['part_start'], end)
405 # Create all the logical partitions back
407 part_add('l', l['start'], l['end'])
408 part_set_id(l['num'], l['id'])
409 part_set_bootable(l['num'], l['bootable'])
411 # Recreate the last partition
412 if self.meta['PARTITION_TABLE'] == 'msdos':
413 last_part['id'] = part_get_id(last_part['part_num'])
415 last_part['bootable'] = part_get_bootable(last_part['part_num'])
416 part_del(last_part['part_num'])
417 part_add('p', start, end)
418 part_set_bootable(last_part['part_num'], last_part['bootable'])
420 if self.meta['PARTITION_TABLE'] == 'msdos':
421 part_set_id(last_part['part_num'], last_part['id'])
423 new_size = (end + 1) * sector_size
425 assert (new_size <= self.size)
427 if self.meta['PARTITION_TABLE'] == 'gpt':
428 ptable = GPTPartitionTable(self.real_device)
429 self.size = ptable.shrink(new_size, self.size)
431 self.size = min(new_size + 2048 * sector_size, self.size)
433 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
437 def dump(self, outfile):
438 """Dumps the content of device into a file.
440 This method will only dump the actual payload, found by reading the
441 partition table. Empty space in the end of the device will be ignored.
444 blocksize = 4 * MB # 4MB
446 progr_size = (size + MB - 1) // MB # in MB
447 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
449 with open(self.real_device, 'r') as src:
450 with open(outfile, "w") as dst:
455 length = min(left, blocksize)
456 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
460 progressbar.goto((size - left) // MB)
461 progressbar.success('image file %s was successfully created' % outfile)
463 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :