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
45 from sendfile import sendfile
48 class DiskError(Exception):
51 dd = get_command('dd')
52 dmsetup = get_command('dmsetup')
53 losetup = get_command('losetup')
54 blockdev = get_command('blockdev')
58 """This class represents a hard disk hosting an Operating System
60 A Disk instance never alters the source media it is created from.
61 Any change is done on a snapshot created by the device-mapper of
65 def __init__(self, source, output):
66 """Create a new Disk instance out of a source media. The source
67 media can be an image file, a block device or a directory."""
68 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 raise NotImplementedError
86 """Cleanup internal data. This needs to be called before the
89 while len(self._devices):
90 device = self._devices.pop()
93 while len(self._cleanup_jobs):
94 job, args = self._cleanup_jobs.pop()
98 """Creates a snapshot of the original source media of the Disk
102 self.out.output("Examining source media `%s'..." % self.source, False)
103 sourcedev = self.source
104 mode = os.stat(self.source).st_mode
105 if stat.S_ISDIR(mode):
106 success('looks like a directory')
107 return self._losetup(self._dir_to_disk())
108 elif stat.S_ISREG(mode):
109 success('looks like an image file')
110 sourcedev = self._losetup(self.source)
111 elif not stat.S_ISBLK(mode):
112 raise ValueError("Invalid media source. Only block devices, "
113 "regular files and directories are supported.")
115 self.out.success('looks like a block device')
117 # Take a snapshot and return it to the user
118 self.out.output("Snapshotting media source...", False)
119 size = blockdev('--getsize', sourcedev)
120 cowfd, cow = tempfile.mkstemp()
122 self._add_cleanup(os.unlink, cow)
123 # Create 1G cow sparse file
124 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
125 cowdev = self._losetup(cow)
127 snapshot = uuid.uuid4().hex
128 tablefd, table = tempfile.mkstemp()
130 os.write(tablefd, "0 %d snapshot %s %s n 8" % \
131 (int(size), sourcedev, cowdev))
132 dmsetup('create', snapshot, table)
133 self._add_cleanup(dmsetup, 'remove', snapshot)
134 # Sometimes dmsetup remove fails with Device or resource busy,
135 # although everything is cleaned up and the snapshot is not
136 # used by anyone. Add a 2 seconds delay to be on the safe side.
137 self._add_cleanup(time.sleep, 2)
141 self.out.success('done')
142 return "/dev/mapper/%s" % snapshot
144 def get_device(self, media):
145 """Returns a newly created DiskDevice instance."""
147 new_device = DiskDevice(media, self.out)
148 self._devices.append(new_device)
152 def destroy_device(self, device):
153 """Destroys a DiskDevice instance previously created by
156 self._devices.remove(device)
160 class DiskDevice(object):
161 """This class represents a block device hosting an Operating System
162 as created by the device-mapper.
165 def __init__(self, device, output, bootable=True):
166 """Create a new DiskDevice."""
168 self.real_device = device
170 self.bootable = bootable
171 self.progress_bar = None
172 self.guestfs_device = None
175 self.g = guestfs.GuestFS()
176 self.g.add_drive_opts(self.real_device, readonly=0)
178 # Before version 1.17.14 the recovery process, which is a fork of the
179 # original process that called libguestfs, did not close its inherited
180 # file descriptors. This can cause problems especially if the parent
181 # process has opened pipes. Since the recovery process is an optional
182 # feature of libguestfs, it's better to disable it.
183 self.g.set_recovery_proc(0)
184 version = self.g.version()
185 if version['major'] > 1 or (version['major'] == 1 and
186 (version['minor'] >= 18 or \
187 (version['minor'] == 17 and version['release'] >= 14))):
188 self.g.set_recovery_proc(1)
189 self.out.output("Enabling recovery proc")
192 #self.g.set_verbose(1)
194 self.guestfs_enabled = False
197 """Enable a newly created DiskDevice"""
198 self.progressbar = self.out.Progress(100, "Launching helper VM",
200 eh = self.g.set_event_callback(self.progress_callback,
201 guestfs.EVENT_PROGRESS)
203 self.guestfs_enabled = True
204 self.g.delete_event_callback(eh)
205 self.progressbar.success('done')
206 self.progressbar = None
208 self.out.output('Inspecting Operating System...', False)
209 roots = self.g.inspect_os()
211 raise FatalError("No operating system found")
213 raise FatalError("Multiple operating systems found."
214 "We only support images with one filesystem.")
216 self.guestfs_device = self.g.part_to_dev(self.root)
217 self.meta['SIZE'] = self.g.blockdev_getsize64(self.guestfs_device)
218 self.meta['PARTITION_TABLE'] = \
219 self.g.part_get_parttype(self.guestfs_device)
221 self.ostype = self.g.inspect_get_type(self.root)
222 self.distro = self.g.inspect_get_distro(self.root)
223 self.out.success('found a(n) %s system' % self.distro)
226 """Destroy this DiskDevice instance."""
228 if self.guestfs_enabled:
232 # Close the guestfs handler if open
235 def progress_callback(self, ev, eh, buf, array):
239 self.progressbar.goto((position * 100) // total)
241 def mount(self, readonly=False):
242 """Mount all disk partitions in a correct order."""
244 mount = self.g.mount_ro if readonly else self.g.mount
245 self.out.output("Mounting image...", False)
246 mps = self.g.inspect_get_mountpoints(self.root)
248 # Sort the keys to mount the fs in a correct order.
249 # / should be mounted befor /boot, etc
251 if len(a[0]) > len(b[0]):
253 elif len(a[0]) == len(b[0]):
261 except RuntimeError as msg:
262 self.out.warn("%s (ignored)" % msg)
263 self.out.success("done")
266 """Umount all mounted filesystems."""
269 def _last_partition(self):
270 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
271 msg = "Unsupported partition table: %s. Only msdos and gpt " \
272 "partition tables are supported" % self.meta['PARTITION_TABLE']
273 raise FatalError(msg)
275 is_extended = lambda p: self.g.part_get_mbr_id(
276 self.guestfs_device, p['part_num']) == 5
277 is_logical = lambda p: self.meta['PARTITION_TABLE'] != 'msdos' and \
280 partitions = self.g.part_list(self.guestfs_device)
281 last_partition = partitions[-1]
283 if is_logical(last_partition):
284 # The disk contains extended and logical partitions....
285 extended = [p for p in partitions if is_extended(p)][0]
286 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
288 # check if extended is the last primary partition
289 if last_primary['part_num'] > extended['part_num']:
290 last_partition = last_primary
292 return last_partition
297 This is accomplished by shrinking the last filesystem in the
298 disk and then updating the partition table. The new disk size
299 (in bytes) is returned.
301 ATTENTION: make sure unmount is called before shrink
303 get_fstype = lambda p: self.g.vfs_type("%s%d" % \
304 (self.guestfs_device, p['part_num']))
305 is_logical = lambda p: self.meta['PARTITION_TABLE'] == 'msdos' and \
307 is_extended = lambda p: self.meta['PARTITION_TABLE'] == 'msdos' and \
308 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
310 part_add = lambda ptype, start, stop: \
311 self.g.part_add(self.guestfs_device, ptype, start, stop)
312 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
313 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
314 part_set_id = lambda p, id: self.g.part_set_mbr_id(
315 self.guestfs_device, p, id)
316 part_get_bootable = lambda p: self.g.part_get_bootable(
317 self.guestfs_device, p)
318 part_set_bootable = lambda p, bootable: self.g.part_set_bootable(
319 self.guestfs_device, p, bootable)
323 self.out.output("Shrinking image (this may take a while)...", False)
328 last_part = self._last_partition()
329 fstype = get_fstype(last_part)
332 self.meta['SWAP'] = "%d:%s" % \
333 (last_part['part_num'],
334 (last_part['part_size'] + MB - 1) // MB)
335 part_del(last_part['part_num'])
337 elif is_extended(last_part):
338 part_del(last_part['part_num'])
341 self.meta['SIZE'] = last_part['part_end'] + 1
344 if not re.match("ext[234]", fstype):
345 self.out.warn("Don't know how to resize %s partitions." % fstype)
346 return self.meta['SIZE']
348 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
349 self.g.e2fsck_f(part_dev)
350 self.g.resize2fs_M(part_dev)
352 out = self.g.tune2fs_l(part_dev)
354 filter(lambda x: x[0] == 'Block size', out)[0][1])
356 filter(lambda x: x[0] == 'Block count', out)[0][1])
358 sector_size = self.g.blockdev_getss(self.guestfs_device)
359 start = last_part['part_start'] / sector_size
360 end = start + (block_size * block_cnt) / sector_size - 1
362 if is_logical(last_part):
363 partitions = self.g.part_list(self.guestfs_device)
365 logical = [] # logical partitions
366 for partition in partitions:
367 if partition['part_num'] < 4:
370 'num': partition['part_num'],
371 'start': partition['part_start'] / sector_size,
372 'end': partition['part_end'] / sector_size,
373 'id': part_get_(partition['part_num']),
374 'bootable': part_get_bootable(partition['part_num'])
377 logical[-1]['end'] = end # new end after resize
379 # Recreate the extended partition
380 extended = [p for p in partitions if self._is_extended(p)][0]
381 part_del(extended['part_num'])
382 part_add('e', extended['part_start'], end)
384 # Create all the logical partitions back
386 part_add('l', l['start'], l['end'])
387 part_set_id(l['num'], l['id'])
388 part_set_bootable(l['num'], l['bootable'])
390 # Recreate the last partition
391 if self.meta['PARTITION_TABLE'] == 'msdos':
392 last_part['id'] = part_get_id(last_part['part_num'])
394 last_part['bootable'] = part_get_bootable(last_part['part_num'])
395 part_del(last_part['part_num'])
396 part_add('p', start, end)
397 part_set_bootable(last_part['part_num'], last_part['bootable'])
399 if self.meta['PARTITION_TABLE'] == 'msdos':
400 part_set_id(last_part['part_num'], last_part['id'])
402 new_size = (end + 1) * sector_size
403 self.out.success("new size is %dMB" % ((new_size + MB - 1) // MB))
405 if self.meta['PARTITION_TABLE'] == 'gpt':
406 ptable = GPTPartitionTable(self.real_device)
407 self.meta['SIZE'] = ptable.shrink(new_size)
409 self.meta['SIZE'] = new_size
411 return self.meta['SIZE']
413 def dump(self, outfile):
414 """Dumps the content of device into a file.
416 This method will only dump the actual payload, found by reading the
417 partition table. Empty space in the end of the device will be ignored.
420 blocksize = 4 * MB # 4MB
421 size = self.meta['SIZE']
422 progr_size = (size + MB - 1) // MB # in MB
423 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
425 with open(self.real_device, 'r') as src:
426 with open(outfile, "w") as dst:
431 length = min(left, blocksize)
432 sent = sendfile(dst.fileno(), src.fileno(), offset, length)
435 progressbar.goto((size - left) // MB)
436 progressbar.success('image file %s was successfully created' % outfile)
438 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :