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 = '/var/tmp/%s.diskdump' % uuid.uuid4().hex
87 def check_unlink(path):
88 if os.path.exists(path):
91 self._add_cleanup(check_unlink, image)
92 bundle.create_image(image)
93 return self._losetup(image)
94 raise FatalError("Using a directory as media source is supported")
97 """Cleanup internal data. This needs to be called before the
101 while len(self._devices):
102 device = self._devices.pop()
105 # Make sure those are executed even if one of the device.destroy
106 # methods throws exeptions.
107 while len(self._cleanup_jobs):
108 job, args = self._cleanup_jobs.pop()
112 """Creates a snapshot of the original source media of the Disk
116 self.out.output("Examining source media `%s'..." % self.source, False)
117 sourcedev = self.source
118 mode = os.stat(self.source).st_mode
119 if stat.S_ISDIR(mode):
120 self.out.success('looks like a directory')
121 return self._dir_to_disk()
122 elif stat.S_ISREG(mode):
123 self.out.success('looks like an image file')
124 sourcedev = self._losetup(self.source)
125 elif not stat.S_ISBLK(mode):
126 raise ValueError("Invalid media source. Only block devices, "
127 "regular files and directories are supported.")
129 self.out.success('looks like a block device')
131 # Take a snapshot and return it to the user
132 self.out.output("Snapshotting media source...", False)
133 size = blockdev('--getsz', sourcedev)
134 cowfd, cow = tempfile.mkstemp()
136 self._add_cleanup(os.unlink, cow)
137 # Create cow sparse file
138 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
139 cowdev = self._losetup(cow)
141 snapshot = uuid.uuid4().hex
142 tablefd, table = tempfile.mkstemp()
144 os.write(tablefd, "0 %d snapshot %s %s n 8" %
145 (int(size), sourcedev, cowdev))
146 dmsetup('create', snapshot, table)
147 self._add_cleanup(dmsetup, 'remove', snapshot)
148 # Sometimes dmsetup remove fails with Device or resource busy,
149 # although everything is cleaned up and the snapshot is not
150 # used by anyone. Add a 2 seconds delay to be on the safe side.
151 self._add_cleanup(time.sleep, 2)
155 self.out.success('done')
156 return "/dev/mapper/%s" % snapshot
158 def get_device(self, media):
159 """Returns a newly created DiskDevice instance."""
161 new_device = DiskDevice(media, self.out)
162 self._devices.append(new_device)
166 def destroy_device(self, device):
167 """Destroys a DiskDevice instance previously created by
170 self._devices.remove(device)
174 class DiskDevice(object):
175 """This class represents a block device hosting an Operating System
176 as created by the device-mapper.
179 def __init__(self, device, output, bootable=True, meta={}):
180 """Create a new DiskDevice."""
182 self.real_device = device
184 self.bootable = bootable
186 self.progress_bar = None
187 self.guestfs_device = None
190 self.g = guestfs.GuestFS()
191 self.g.add_drive_opts(self.real_device, readonly=0)
193 # Before version 1.17.14 the recovery process, which is a fork of the
194 # original process that called libguestfs, did not close its inherited
195 # file descriptors. This can cause problems especially if the parent
196 # process has opened pipes. Since the recovery process is an optional
197 # feature of libguestfs, it's better to disable it.
198 self.g.set_recovery_proc(0)
199 version = self.g.version()
200 if version['major'] > 1 or \
201 (version['major'] == 1 and (version['minor'] >= 18 or
202 (version['minor'] == 17 and
203 version['release'] >= 14))):
204 self.g.set_recovery_proc(1)
205 self.out.output("Enabling recovery proc")
208 #self.g.set_verbose(1)
210 self.guestfs_enabled = False
213 """Enable a newly created DiskDevice"""
215 self.out.output('Launching helper VM (may take a while) ...', False)
216 # self.progressbar = self.out.Progress(100, "Launching helper VM",
218 # eh = self.g.set_event_callback(self.progress_callback,
219 # guestfs.EVENT_PROGRESS)
221 self.guestfs_enabled = True
222 # self.g.delete_event_callback(eh)
223 # self.progressbar.success('done')
224 # self.progressbar = None
225 self.out.success('done')
227 self.out.output('Inspecting Operating System ...', False)
228 roots = self.g.inspect_os()
230 raise FatalError("No operating system found")
232 raise FatalError("Multiple operating systems found."
233 "We only support images with one OS.")
235 self.guestfs_device = self.g.part_to_dev(self.root)
236 self.size = self.g.blockdev_getsize64(self.guestfs_device)
237 self.meta['PARTITION_TABLE'] = \
238 self.g.part_get_parttype(self.guestfs_device)
240 self.ostype = self.g.inspect_get_type(self.root)
241 self.distro = self.g.inspect_get_distro(self.root)
242 self.out.success('found a(n) %s system' % self.distro)
245 """Destroy this DiskDevice instance."""
247 # In new guestfs versions, there is a handy shutdown method for this
249 if self.guestfs_enabled:
253 # Close the guestfs handler if open
256 # def progress_callback(self, ev, eh, buf, array):
257 # position = array[2]
260 # self.progressbar.goto((position * 100) // total)
262 def mount(self, readonly=False):
263 """Mount all disk partitions in a correct order."""
265 mount = self.g.mount_ro if readonly else self.g.mount
266 msg = " read-only" if readonly else ""
267 self.out.output("Mounting the media%s..." % msg, False)
268 mps = self.g.inspect_get_mountpoints(self.root)
270 # Sort the keys to mount the fs in a correct order.
271 # / should be mounted befor /boot, etc
273 if len(a[0]) > len(b[0]):
275 elif len(a[0]) == len(b[0]):
283 except RuntimeError as msg:
284 self.out.warn("%s (ignored)" % msg)
285 self.out.success("done")
288 """Umount all mounted filesystems."""
291 def _last_partition(self):
292 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
293 msg = "Unsupported partition table: %s. Only msdos and gpt " \
294 "partition tables are supported" % self.meta['PARTITION_TABLE']
295 raise FatalError(msg)
297 is_extended = lambda p: \
298 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
299 is_logical = lambda p: \
300 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
302 partitions = self.g.part_list(self.guestfs_device)
303 last_partition = partitions[-1]
305 if is_logical(last_partition):
306 # The disk contains extended and logical partitions....
307 extended = [p for p in partitions if is_extended(p)][0]
308 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
310 # check if extended is the last primary partition
311 if last_primary['part_num'] > extended['part_num']:
312 last_partition = last_primary
314 return last_partition
319 This is accomplished by shrinking the last filesystem in the
320 disk and then updating the partition table. The new disk size
321 (in bytes) is returned.
323 ATTENTION: make sure unmount is called before shrink
325 get_fstype = lambda p: \
326 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
327 is_logical = lambda p: \
328 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
329 is_extended = lambda p: \
330 self.meta['PARTITION_TABLE'] == 'msdos' and \
331 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
333 part_add = lambda ptype, start, stop: \
334 self.g.part_add(self.guestfs_device, ptype, start, stop)
335 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
336 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
337 part_set_id = lambda p, id: \
338 self.g.part_set_mbr_id(self.guestfs_device, p, id)
339 part_get_bootable = lambda p: \
340 self.g.part_get_bootable(self.guestfs_device, p)
341 part_set_bootable = lambda p, bootable: \
342 self.g.part_set_bootable(self.guestfs_device, p, bootable)
346 self.out.output("Shrinking image (this may take a while)...", False)
348 sector_size = self.g.blockdev_getss(self.guestfs_device)
353 last_part = self._last_partition()
354 fstype = get_fstype(last_part)
357 self.meta['SWAP'] = "%d:%s" % \
358 (last_part['part_num'],
359 (last_part['part_size'] + MB - 1) // MB)
360 part_del(last_part['part_num'])
362 elif is_extended(last_part):
363 part_del(last_part['part_num'])
366 # Most disk manipulation programs leave 2048 sectors after the last
368 new_size = last_part['part_end'] + 1 + 2048 * sector_size
369 self.size = min(self.size, new_size)
372 if not re.match("ext[234]", fstype):
373 self.out.warn("Don't know how to resize %s partitions." % fstype)
376 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
377 self.g.e2fsck_f(part_dev)
378 self.g.resize2fs_M(part_dev)
380 out = self.g.tune2fs_l(part_dev)
382 filter(lambda x: x[0] == 'Block size', out)[0][1])
384 filter(lambda x: x[0] == 'Block count', out)[0][1])
386 start = last_part['part_start'] / sector_size
387 end = start + (block_size * block_cnt) / sector_size - 1
389 if is_logical(last_part):
390 partitions = self.g.part_list(self.guestfs_device)
392 logical = [] # logical partitions
393 for partition in partitions:
394 if partition['part_num'] < 4:
397 'num': partition['part_num'],
398 'start': partition['part_start'] / sector_size,
399 'end': partition['part_end'] / sector_size,
400 'id': part_get_(partition['part_num']),
401 'bootable': part_get_bootable(partition['part_num'])
404 logical[-1]['end'] = end # new end after resize
406 # Recreate the extended partition
407 extended = [p for p in partitions if self._is_extended(p)][0]
408 part_del(extended['part_num'])
409 part_add('e', extended['part_start'], end)
411 # Create all the logical partitions back
413 part_add('l', l['start'], l['end'])
414 part_set_id(l['num'], l['id'])
415 part_set_bootable(l['num'], l['bootable'])
417 # Recreate the last partition
418 if self.meta['PARTITION_TABLE'] == 'msdos':
419 last_part['id'] = part_get_id(last_part['part_num'])
421 last_part['bootable'] = part_get_bootable(last_part['part_num'])
422 part_del(last_part['part_num'])
423 part_add('p', start, end)
424 part_set_bootable(last_part['part_num'], last_part['bootable'])
426 if self.meta['PARTITION_TABLE'] == 'msdos':
427 part_set_id(last_part['part_num'], last_part['id'])
429 new_size = (end + 1) * sector_size
431 assert (new_size <= self.size)
433 if self.meta['PARTITION_TABLE'] == 'gpt':
434 ptable = GPTPartitionTable(self.real_device)
435 self.size = ptable.shrink(new_size, self.size)
437 self.size = min(new_size + 2048 * sector_size, self.size)
439 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
443 def dump(self, outfile):
444 """Dumps the content of device into a file.
446 This method will only dump the actual payload, found by reading the
447 partition table. Empty space in the end of the device will be ignored.
450 blocksize = 4 * MB # 4MB
452 progr_size = (size + MB - 1) // MB # in MB
453 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
455 with open(self.real_device, 'r') as src:
456 with open(outfile, "w") as dst:
461 length = min(left, blocksize)
462 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
466 progressbar.goto((size - left) // MB)
467 progressbar.success('image file %s was successfully created' % outfile)
469 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :