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.util import try_fail_repeat
37 from image_creator.gpt import GPTPartitionTable
38 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.
68 self._cleanup_jobs = []
74 def _add_cleanup(self, job, *args):
75 self._cleanup_jobs.append((job, args))
77 def _losetup(self, fname):
78 loop = losetup('-f', '--show', fname)
79 loop = loop.strip() # remove the new-line char
80 self._add_cleanup(try_fail_repeat, losetup, '-d', loop)
83 def _dir_to_disk(self):
84 if self.source == '/':
85 bundle = BundleVolume(self.out, self.meta)
86 image = '/var/tmp/%s.diskdump' % uuid.uuid4().hex
88 def check_unlink(path):
89 if os.path.exists(path):
92 self._add_cleanup(check_unlink, image)
93 bundle.create_image(image)
94 return self._losetup(image)
95 raise FatalError("Using a directory as media source is supported")
98 """Cleanup internal data. This needs to be called before the
102 while len(self._devices):
103 device = self._devices.pop()
106 # Make sure those are executed even if one of the device.destroy
107 # methods throws exeptions.
108 while len(self._cleanup_jobs):
109 job, args = self._cleanup_jobs.pop()
113 """Creates a snapshot of the original source media of the Disk
117 self.out.output("Examining source media `%s'..." % self.source, False)
118 sourcedev = self.source
119 mode = os.stat(self.source).st_mode
120 if stat.S_ISDIR(mode):
121 self.out.success('looks like a directory')
122 return self._dir_to_disk()
123 elif stat.S_ISREG(mode):
124 self.out.success('looks like an image file')
125 sourcedev = self._losetup(self.source)
126 elif not stat.S_ISBLK(mode):
127 raise ValueError("Invalid media source. Only block devices, "
128 "regular files and directories are supported.")
130 self.out.success('looks like a block device')
132 # Take a snapshot and return it to the user
133 self.out.output("Snapshotting media source...", False)
134 size = blockdev('--getsz', sourcedev)
135 cowfd, cow = tempfile.mkstemp()
137 self._add_cleanup(os.unlink, cow)
138 # Create cow sparse file
139 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
140 cowdev = self._losetup(cow)
142 snapshot = uuid.uuid4().hex
143 tablefd, table = tempfile.mkstemp()
145 os.write(tablefd, "0 %d snapshot %s %s n 8" %
146 (int(size), sourcedev, cowdev))
147 dmsetup('create', snapshot, table)
148 self._add_cleanup(try_fail_repeat, dmsetup, 'remove', snapshot)
152 self.out.success('done')
153 return "/dev/mapper/%s" % snapshot
155 def get_device(self, media):
156 """Returns a newly created DiskDevice instance."""
158 new_device = DiskDevice(media, self.out)
159 self._devices.append(new_device)
163 def destroy_device(self, device):
164 """Destroys a DiskDevice instance previously created by
167 self._devices.remove(device)
171 class DiskDevice(object):
172 """This class represents a block device hosting an Operating System
173 as created by the device-mapper.
176 def __init__(self, device, output, bootable=True, meta={}):
177 """Create a new DiskDevice."""
179 self.real_device = device
181 self.bootable = bootable
183 self.progress_bar = None
184 self.guestfs_device = None
187 self.g = guestfs.GuestFS()
188 self.g.add_drive_opts(self.real_device, readonly=0)
190 # Before version 1.17.14 the recovery process, which is a fork of the
191 # original process that called libguestfs, did not close its inherited
192 # file descriptors. This can cause problems especially if the parent
193 # process has opened pipes. Since the recovery process is an optional
194 # feature of libguestfs, it's better to disable it.
195 self.g.set_recovery_proc(0)
196 version = self.g.version()
197 if version['major'] > 1 or \
198 (version['major'] == 1 and (version['minor'] >= 18 or
199 (version['minor'] == 17 and
200 version['release'] >= 14))):
201 self.g.set_recovery_proc(1)
202 self.out.output("Enabling recovery proc")
205 #self.g.set_verbose(1)
207 self.guestfs_enabled = False
210 """Enable a newly created DiskDevice"""
212 self.out.output('Launching helper VM (may take a while) ...', False)
213 # self.progressbar = self.out.Progress(100, "Launching helper VM",
215 # eh = self.g.set_event_callback(self.progress_callback,
216 # guestfs.EVENT_PROGRESS)
218 self.guestfs_enabled = True
219 # self.g.delete_event_callback(eh)
220 # self.progressbar.success('done')
221 # self.progressbar = None
222 self.out.success('done')
224 self.out.output('Inspecting Operating System ...', False)
225 roots = self.g.inspect_os()
227 raise FatalError("No operating system found")
229 raise FatalError("Multiple operating systems found."
230 "We only support images with one OS.")
232 self.guestfs_device = self.g.part_to_dev(self.root)
233 self.size = self.g.blockdev_getsize64(self.guestfs_device)
234 self.meta['PARTITION_TABLE'] = \
235 self.g.part_get_parttype(self.guestfs_device)
237 self.ostype = self.g.inspect_get_type(self.root)
238 self.distro = self.g.inspect_get_distro(self.root)
239 self.out.success('found a(n) %s system' % self.distro)
242 """Destroy this DiskDevice instance."""
244 # In new guestfs versions, there is a handy shutdown method for this
246 if self.guestfs_enabled:
250 # Close the guestfs handler if open
253 # def progress_callback(self, ev, eh, buf, array):
254 # position = array[2]
257 # self.progressbar.goto((position * 100) // total)
259 def mount(self, readonly=False):
260 """Mount all disk partitions in a correct order."""
262 mount = self.g.mount_ro if readonly else self.g.mount
263 msg = " read-only" if readonly else ""
264 self.out.output("Mounting the media%s..." % msg, False)
265 mps = self.g.inspect_get_mountpoints(self.root)
267 # Sort the keys to mount the fs in a correct order.
268 # / should be mounted befor /boot, etc
270 if len(a[0]) > len(b[0]):
272 elif len(a[0]) == len(b[0]):
280 except RuntimeError as msg:
281 self.out.warn("%s (ignored)" % msg)
282 self.out.success("done")
285 """Umount all mounted filesystems."""
288 def _last_partition(self):
289 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
290 msg = "Unsupported partition table: %s. Only msdos and gpt " \
291 "partition tables are supported" % self.meta['PARTITION_TABLE']
292 raise FatalError(msg)
294 is_extended = lambda p: \
295 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
296 is_logical = lambda p: \
297 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
299 partitions = self.g.part_list(self.guestfs_device)
300 last_partition = partitions[-1]
302 if is_logical(last_partition):
303 # The disk contains extended and logical partitions....
304 extended = [p for p in partitions if is_extended(p)][0]
305 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
307 # check if extended is the last primary partition
308 if last_primary['part_num'] > extended['part_num']:
309 last_partition = last_primary
311 return last_partition
316 This is accomplished by shrinking the last filesystem in the
317 disk and then updating the partition table. The new disk size
318 (in bytes) is returned.
320 ATTENTION: make sure unmount is called before shrink
322 get_fstype = lambda p: \
323 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
324 is_logical = lambda p: \
325 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
326 is_extended = lambda p: \
327 self.meta['PARTITION_TABLE'] == 'msdos' and \
328 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
330 part_add = lambda ptype, start, stop: \
331 self.g.part_add(self.guestfs_device, ptype, start, stop)
332 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
333 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
334 part_set_id = lambda p, id: \
335 self.g.part_set_mbr_id(self.guestfs_device, p, id)
336 part_get_bootable = lambda p: \
337 self.g.part_get_bootable(self.guestfs_device, p)
338 part_set_bootable = lambda p, bootable: \
339 self.g.part_set_bootable(self.guestfs_device, p, bootable)
343 self.out.output("Shrinking image (this may take a while)...", False)
345 sector_size = self.g.blockdev_getss(self.guestfs_device)
350 last_part = self._last_partition()
351 fstype = get_fstype(last_part)
354 self.meta['SWAP'] = "%d:%s" % \
355 (last_part['part_num'],
356 (last_part['part_size'] + MB - 1) // MB)
357 part_del(last_part['part_num'])
359 elif is_extended(last_part):
360 part_del(last_part['part_num'])
363 # Most disk manipulation programs leave 2048 sectors after the last
365 new_size = last_part['part_end'] + 1 + 2048 * sector_size
366 self.size = min(self.size, new_size)
369 if not re.match("ext[234]", fstype):
370 self.out.warn("Don't know how to resize %s partitions." % fstype)
373 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
374 self.g.e2fsck_f(part_dev)
375 self.g.resize2fs_M(part_dev)
377 out = self.g.tune2fs_l(part_dev)
379 filter(lambda x: x[0] == 'Block size', out)[0][1])
381 filter(lambda x: x[0] == 'Block count', out)[0][1])
383 start = last_part['part_start'] / sector_size
384 end = start + (block_size * block_cnt) / sector_size - 1
386 if is_logical(last_part):
387 partitions = self.g.part_list(self.guestfs_device)
389 logical = [] # logical partitions
390 for partition in partitions:
391 if partition['part_num'] < 4:
394 'num': partition['part_num'],
395 'start': partition['part_start'] / sector_size,
396 'end': partition['part_end'] / sector_size,
397 'id': part_get_(partition['part_num']),
398 'bootable': part_get_bootable(partition['part_num'])
401 logical[-1]['end'] = end # new end after resize
403 # Recreate the extended partition
404 extended = [p for p in partitions if self._is_extended(p)][0]
405 part_del(extended['part_num'])
406 part_add('e', extended['part_start'], end)
408 # Create all the logical partitions back
410 part_add('l', l['start'], l['end'])
411 part_set_id(l['num'], l['id'])
412 part_set_bootable(l['num'], l['bootable'])
414 # Recreate the last partition
415 if self.meta['PARTITION_TABLE'] == 'msdos':
416 last_part['id'] = part_get_id(last_part['part_num'])
418 last_part['bootable'] = part_get_bootable(last_part['part_num'])
419 part_del(last_part['part_num'])
420 part_add('p', start, end)
421 part_set_bootable(last_part['part_num'], last_part['bootable'])
423 if self.meta['PARTITION_TABLE'] == 'msdos':
424 part_set_id(last_part['part_num'], last_part['id'])
426 new_size = (end + 1) * sector_size
428 assert (new_size <= self.size)
430 if self.meta['PARTITION_TABLE'] == 'gpt':
431 ptable = GPTPartitionTable(self.real_device)
432 self.size = ptable.shrink(new_size, self.size)
434 self.size = min(new_size + 2048 * sector_size, self.size)
436 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
440 def dump(self, outfile):
441 """Dumps the content of device into a file.
443 This method will only dump the actual payload, found by reading the
444 partition table. Empty space in the end of the device will be ignored.
447 blocksize = 4 * MB # 4MB
449 progr_size = (size + MB - 1) // MB # in MB
450 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
452 with open(self.real_device, 'r') as src:
453 with open(outfile, "w") as dst:
458 length = min(left, blocksize)
459 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
463 progressbar.goto((size - left) // MB)
464 progressbar.success('image file %s was successfully created' % outfile)
466 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :