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 _map_partition(self, dev, index, start, end):
83 name = "%sp%d" % (os.path.basename(dev), index)
84 tablefd, table = tempfile.mkstemp()
86 size = end - start + 1
87 os.write(tablefd, "0 %d linear %s %d" % (start, dev, size))
88 dmsetup('create', name, table)
92 def _unmap_partition(self, dev, index):
93 name = "%sp%d" % (os.path.basename(dev), index)
94 if not os.path.exists("/dev/mapper/%s" % name):
97 dmsetup('remove', name)
100 def _dir_to_disk(self):
101 if self.source == '/':
102 bundle = BundleVolume(self.out, self.meta)
103 return self._losetup(bundle.create_image())
104 raise FatalError("Using a directory as media source is supported")
107 """Cleanup internal data. This needs to be called before the
111 while len(self._devices):
112 device = self._devices.pop()
115 # Make sure those are executed even if one of the device.destroy
116 # methods throws exeptions.
117 while len(self._cleanup_jobs):
118 job, args = self._cleanup_jobs.pop()
122 """Creates a snapshot of the original source media of the Disk
126 self.out.output("Examining source media `%s'..." % self.source, False)
127 sourcedev = self.source
128 mode = os.stat(self.source).st_mode
129 if stat.S_ISDIR(mode):
130 self.out.success('looks like a directory')
131 return self._dir_to_disk()
132 elif stat.S_ISREG(mode):
133 self.out.success('looks like an image file')
134 sourcedev = self._losetup(self.source)
135 elif not stat.S_ISBLK(mode):
136 raise ValueError("Invalid media source. Only block devices, "
137 "regular files and directories are supported.")
139 self.out.success('looks like a block device')
141 # Take a snapshot and return it to the user
142 self.out.output("Snapshotting media source...", False)
143 size = blockdev('--getsz', sourcedev)
144 cowfd, cow = tempfile.mkstemp()
146 self._add_cleanup(os.unlink, cow)
147 # Create cow sparse file
148 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
149 cowdev = self._losetup(cow)
151 snapshot = uuid.uuid4().hex
152 tablefd, table = tempfile.mkstemp()
154 os.write(tablefd, "0 %d snapshot %s %s n 8" %
155 (int(size), sourcedev, cowdev))
156 dmsetup('create', snapshot, table)
157 self._add_cleanup(dmsetup, 'remove', snapshot)
158 # Sometimes dmsetup remove fails with Device or resource busy,
159 # although everything is cleaned up and the snapshot is not
160 # used by anyone. Add a 2 seconds delay to be on the safe side.
161 self._add_cleanup(time.sleep, 2)
165 self.out.success('done')
166 return "/dev/mapper/%s" % snapshot
168 def get_device(self, media):
169 """Returns a newly created DiskDevice instance."""
171 new_device = DiskDevice(media, self.out)
172 self._devices.append(new_device)
176 def destroy_device(self, device):
177 """Destroys a DiskDevice instance previously created by
180 self._devices.remove(device)
184 class DiskDevice(object):
185 """This class represents a block device hosting an Operating System
186 as created by the device-mapper.
189 def __init__(self, device, output, bootable=True, meta={}):
190 """Create a new DiskDevice."""
192 self.real_device = device
194 self.bootable = bootable
196 self.progress_bar = None
197 self.guestfs_device = None
200 self.g = guestfs.GuestFS()
201 self.g.add_drive_opts(self.real_device, readonly=0)
203 # Before version 1.17.14 the recovery process, which is a fork of the
204 # original process that called libguestfs, did not close its inherited
205 # file descriptors. This can cause problems especially if the parent
206 # process has opened pipes. Since the recovery process is an optional
207 # feature of libguestfs, it's better to disable it.
208 self.g.set_recovery_proc(0)
209 version = self.g.version()
210 if version['major'] > 1 or \
211 (version['major'] == 1 and (version['minor'] >= 18 or
212 (version['minor'] == 17 and
213 version['release'] >= 14))):
214 self.g.set_recovery_proc(1)
215 self.out.output("Enabling recovery proc")
218 #self.g.set_verbose(1)
220 self.guestfs_enabled = False
223 """Enable a newly created DiskDevice"""
224 self.progressbar = self.out.Progress(100, "Launching helper VM",
226 eh = self.g.set_event_callback(self.progress_callback,
227 guestfs.EVENT_PROGRESS)
229 self.guestfs_enabled = True
230 self.g.delete_event_callback(eh)
231 self.progressbar.success('done')
232 self.progressbar = None
234 self.out.output('Inspecting Operating System...', False)
235 roots = self.g.inspect_os()
237 raise FatalError("No operating system found")
239 raise FatalError("Multiple operating systems found."
240 "We only support images with one OS.")
242 self.guestfs_device = self.g.part_to_dev(self.root)
243 self.size = self.g.blockdev_getsize64(self.guestfs_device)
244 self.meta['PARTITION_TABLE'] = \
245 self.g.part_get_parttype(self.guestfs_device)
247 self.ostype = self.g.inspect_get_type(self.root)
248 self.distro = self.g.inspect_get_distro(self.root)
249 self.out.success('found a(n) %s system' % self.distro)
252 """Destroy this DiskDevice instance."""
254 # In new guestfs versions, there is a handy shutdown method for this
256 if self.guestfs_enabled:
260 # Close the guestfs handler if open
263 def progress_callback(self, ev, eh, buf, array):
267 self.progressbar.goto((position * 100) // total)
269 def mount(self, readonly=False):
270 """Mount all disk partitions in a correct order."""
272 mount = self.g.mount_ro if readonly else self.g.mount
273 msg = " read-only" if readonly else ""
274 self.out.output("Mounting the media%s..." % msg, False)
275 mps = self.g.inspect_get_mountpoints(self.root)
277 # Sort the keys to mount the fs in a correct order.
278 # / should be mounted befor /boot, etc
280 if len(a[0]) > len(b[0]):
282 elif len(a[0]) == len(b[0]):
290 except RuntimeError as msg:
291 self.out.warn("%s (ignored)" % msg)
292 self.out.success("done")
295 """Umount all mounted filesystems."""
298 def _last_partition(self):
299 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
300 msg = "Unsupported partition table: %s. Only msdos and gpt " \
301 "partition tables are supported" % self.meta['PARTITION_TABLE']
302 raise FatalError(msg)
304 is_extended = lambda p: \
305 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
306 is_logical = lambda p: \
307 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
309 partitions = self.g.part_list(self.guestfs_device)
310 last_partition = partitions[-1]
312 if is_logical(last_partition):
313 # The disk contains extended and logical partitions....
314 extended = [p for p in partitions if is_extended(p)][0]
315 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
317 # check if extended is the last primary partition
318 if last_primary['part_num'] > extended['part_num']:
319 last_partition = last_primary
321 return last_partition
326 This is accomplished by shrinking the last filesystem in the
327 disk and then updating the partition table. The new disk size
328 (in bytes) is returned.
330 ATTENTION: make sure unmount is called before shrink
332 get_fstype = lambda p: \
333 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
334 is_logical = lambda p: \
335 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
336 is_extended = lambda p: \
337 self.meta['PARTITION_TABLE'] == 'msdos' and \
338 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
340 part_add = lambda ptype, start, stop: \
341 self.g.part_add(self.guestfs_device, ptype, start, stop)
342 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
343 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
344 part_set_id = lambda p, id: \
345 self.g.part_set_mbr_id(self.guestfs_device, p, id)
346 part_get_bootable = lambda p: \
347 self.g.part_get_bootable(self.guestfs_device, p)
348 part_set_bootable = lambda p, bootable: \
349 self.g.part_set_bootable(self.guestfs_device, p, bootable)
353 self.out.output("Shrinking image (this may take a while)...", False)
355 sector_size = self.g.blockdev_getss(self.guestfs_device)
360 last_part = self._last_partition()
361 fstype = get_fstype(last_part)
364 self.meta['SWAP'] = "%d:%s" % \
365 (last_part['part_num'],
366 (last_part['part_size'] + MB - 1) // MB)
367 part_del(last_part['part_num'])
369 elif is_extended(last_part):
370 part_del(last_part['part_num'])
373 # Most disk manipulation programs leave 2048 sectors after the last
375 new_size = last_part['part_end'] + 1 + 2048 * sector_size
376 self.size = min(self.size, new_size)
379 if not re.match("ext[234]", fstype):
380 self.out.warn("Don't know how to resize %s partitions." % fstype)
383 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
384 self.g.e2fsck_f(part_dev)
385 self.g.resize2fs_M(part_dev)
387 out = self.g.tune2fs_l(part_dev)
389 filter(lambda x: x[0] == 'Block size', out)[0][1])
391 filter(lambda x: x[0] == 'Block count', out)[0][1])
393 start = last_part['part_start'] / sector_size
394 end = start + (block_size * block_cnt) / sector_size - 1
396 if is_logical(last_part):
397 partitions = self.g.part_list(self.guestfs_device)
399 logical = [] # logical partitions
400 for partition in partitions:
401 if partition['part_num'] < 4:
404 'num': partition['part_num'],
405 'start': partition['part_start'] / sector_size,
406 'end': partition['part_end'] / sector_size,
407 'id': part_get_(partition['part_num']),
408 'bootable': part_get_bootable(partition['part_num'])
411 logical[-1]['end'] = end # new end after resize
413 # Recreate the extended partition
414 extended = [p for p in partitions if self._is_extended(p)][0]
415 part_del(extended['part_num'])
416 part_add('e', extended['part_start'], end)
418 # Create all the logical partitions back
420 part_add('l', l['start'], l['end'])
421 part_set_id(l['num'], l['id'])
422 part_set_bootable(l['num'], l['bootable'])
424 # Recreate the last partition
425 if self.meta['PARTITION_TABLE'] == 'msdos':
426 last_part['id'] = part_get_id(last_part['part_num'])
428 last_part['bootable'] = part_get_bootable(last_part['part_num'])
429 part_del(last_part['part_num'])
430 part_add('p', start, end)
431 part_set_bootable(last_part['part_num'], last_part['bootable'])
433 if self.meta['PARTITION_TABLE'] == 'msdos':
434 part_set_id(last_part['part_num'], last_part['id'])
436 new_size = (end + 1) * sector_size
438 assert (new_size <= self.size)
440 if self.meta['PARTITION_TABLE'] == 'gpt':
441 ptable = GPTPartitionTable(self.real_device)
442 self.size = ptable.shrink(new_size, self.size)
444 self.size = min(new_size + 2048 * sector_size, self.size)
446 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
450 def dump(self, outfile):
451 """Dumps the content of device into a file.
453 This method will only dump the actual payload, found by reading the
454 partition table. Empty space in the end of the device will be ignored.
457 blocksize = 4 * MB # 4MB
459 progr_size = (size + MB - 1) // MB # in MB
460 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
462 with open(self.real_device, 'r') as src:
463 with open(outfile, "w") as dst:
468 length = min(left, blocksize)
469 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
473 progressbar.goto((size - left) // MB)
474 progressbar.success('image file %s was successfully created' % outfile)
476 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :