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 return self._losetup(bundle.create_image())
86 raise FatalError("Using a directory as media source is supported")
89 """Cleanup internal data. This needs to be called before the
93 while len(self._devices):
94 device = self._devices.pop()
97 # Make sure those are executed even if one of the device.destroy
98 # methods throws exeptions.
99 while len(self._cleanup_jobs):
100 job, args = self._cleanup_jobs.pop()
104 """Creates a snapshot of the original source media of the Disk
108 self.out.output("Examining source media `%s'..." % self.source, False)
109 sourcedev = self.source
110 mode = os.stat(self.source).st_mode
111 if stat.S_ISDIR(mode):
112 self.out.success('looks like a directory')
113 return self._dir_to_disk()
114 elif stat.S_ISREG(mode):
115 self.out.success('looks like an image file')
116 sourcedev = self._losetup(self.source)
117 elif not stat.S_ISBLK(mode):
118 raise ValueError("Invalid media source. Only block devices, "
119 "regular files and directories are supported.")
121 self.out.success('looks like a block device')
123 # Take a snapshot and return it to the user
124 self.out.output("Snapshotting media source...", False)
125 size = blockdev('--getsz', sourcedev)
126 cowfd, cow = tempfile.mkstemp()
128 self._add_cleanup(os.unlink, cow)
129 # Create cow sparse file
130 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
131 cowdev = self._losetup(cow)
133 snapshot = uuid.uuid4().hex
134 tablefd, table = tempfile.mkstemp()
136 os.write(tablefd, "0 %d snapshot %s %s n 8" %
137 (int(size), sourcedev, cowdev))
138 dmsetup('create', snapshot, table)
139 self._add_cleanup(dmsetup, 'remove', snapshot)
140 # Sometimes dmsetup remove fails with Device or resource busy,
141 # although everything is cleaned up and the snapshot is not
142 # used by anyone. Add a 2 seconds delay to be on the safe side.
143 self._add_cleanup(time.sleep, 2)
147 self.out.success('done')
148 return "/dev/mapper/%s" % snapshot
150 def get_device(self, media):
151 """Returns a newly created DiskDevice instance."""
153 new_device = DiskDevice(media, self.out)
154 self._devices.append(new_device)
158 def destroy_device(self, device):
159 """Destroys a DiskDevice instance previously created by
162 self._devices.remove(device)
166 class DiskDevice(object):
167 """This class represents a block device hosting an Operating System
168 as created by the device-mapper.
171 def __init__(self, device, output, bootable=True, meta={}):
172 """Create a new DiskDevice."""
174 self.real_device = device
176 self.bootable = bootable
178 self.progress_bar = None
179 self.guestfs_device = None
182 self.g = guestfs.GuestFS()
183 self.g.add_drive_opts(self.real_device, readonly=0)
185 # Before version 1.17.14 the recovery process, which is a fork of the
186 # original process that called libguestfs, did not close its inherited
187 # file descriptors. This can cause problems especially if the parent
188 # process has opened pipes. Since the recovery process is an optional
189 # feature of libguestfs, it's better to disable it.
190 self.g.set_recovery_proc(0)
191 version = self.g.version()
192 if version['major'] > 1 or \
193 (version['major'] == 1 and (version['minor'] >= 18 or
194 (version['minor'] == 17 and
195 version['release'] >= 14))):
196 self.g.set_recovery_proc(1)
197 self.out.output("Enabling recovery proc")
200 #self.g.set_verbose(1)
202 self.guestfs_enabled = False
205 """Enable a newly created DiskDevice"""
206 self.progressbar = self.out.Progress(100, "Launching helper VM",
208 eh = self.g.set_event_callback(self.progress_callback,
209 guestfs.EVENT_PROGRESS)
211 self.guestfs_enabled = True
212 self.g.delete_event_callback(eh)
213 self.progressbar.success('done')
214 self.progressbar = None
216 self.out.output('Inspecting Operating System ...', False)
217 roots = self.g.inspect_os()
219 raise FatalError("No operating system found")
221 raise FatalError("Multiple operating systems found."
222 "We only support images with one OS.")
224 self.guestfs_device = self.g.part_to_dev(self.root)
225 self.size = self.g.blockdev_getsize64(self.guestfs_device)
226 self.meta['PARTITION_TABLE'] = \
227 self.g.part_get_parttype(self.guestfs_device)
229 self.ostype = self.g.inspect_get_type(self.root)
230 self.distro = self.g.inspect_get_distro(self.root)
231 self.out.success('found a(n) %s system' % self.distro)
234 """Destroy this DiskDevice instance."""
236 # In new guestfs versions, there is a handy shutdown method for this
238 if self.guestfs_enabled:
242 # Close the guestfs handler if open
245 def progress_callback(self, ev, eh, buf, array):
249 self.progressbar.goto((position * 100) // total)
251 def mount(self, readonly=False):
252 """Mount all disk partitions in a correct order."""
254 mount = self.g.mount_ro if readonly else self.g.mount
255 msg = " read-only" if readonly else ""
256 self.out.output("Mounting the media%s..." % msg, False)
257 mps = self.g.inspect_get_mountpoints(self.root)
259 # Sort the keys to mount the fs in a correct order.
260 # / should be mounted befor /boot, etc
262 if len(a[0]) > len(b[0]):
264 elif len(a[0]) == len(b[0]):
272 except RuntimeError as msg:
273 self.out.warn("%s (ignored)" % msg)
274 self.out.success("done")
277 """Umount all mounted filesystems."""
280 def _last_partition(self):
281 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
282 msg = "Unsupported partition table: %s. Only msdos and gpt " \
283 "partition tables are supported" % self.meta['PARTITION_TABLE']
284 raise FatalError(msg)
286 is_extended = lambda p: \
287 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
288 is_logical = lambda p: \
289 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
291 partitions = self.g.part_list(self.guestfs_device)
292 last_partition = partitions[-1]
294 if is_logical(last_partition):
295 # The disk contains extended and logical partitions....
296 extended = [p for p in partitions if is_extended(p)][0]
297 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
299 # check if extended is the last primary partition
300 if last_primary['part_num'] > extended['part_num']:
301 last_partition = last_primary
303 return last_partition
308 This is accomplished by shrinking the last filesystem in the
309 disk and then updating the partition table. The new disk size
310 (in bytes) is returned.
312 ATTENTION: make sure unmount is called before shrink
314 get_fstype = lambda p: \
315 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
316 is_logical = lambda p: \
317 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
318 is_extended = lambda p: \
319 self.meta['PARTITION_TABLE'] == 'msdos' and \
320 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
322 part_add = lambda ptype, start, stop: \
323 self.g.part_add(self.guestfs_device, ptype, start, stop)
324 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
325 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
326 part_set_id = lambda p, id: \
327 self.g.part_set_mbr_id(self.guestfs_device, p, id)
328 part_get_bootable = lambda p: \
329 self.g.part_get_bootable(self.guestfs_device, p)
330 part_set_bootable = lambda p, bootable: \
331 self.g.part_set_bootable(self.guestfs_device, p, bootable)
335 self.out.output("Shrinking image (this may take a while)...", False)
337 sector_size = self.g.blockdev_getss(self.guestfs_device)
342 last_part = self._last_partition()
343 fstype = get_fstype(last_part)
346 self.meta['SWAP'] = "%d:%s" % \
347 (last_part['part_num'],
348 (last_part['part_size'] + MB - 1) // MB)
349 part_del(last_part['part_num'])
351 elif is_extended(last_part):
352 part_del(last_part['part_num'])
355 # Most disk manipulation programs leave 2048 sectors after the last
357 new_size = last_part['part_end'] + 1 + 2048 * sector_size
358 self.size = min(self.size, new_size)
361 if not re.match("ext[234]", fstype):
362 self.out.warn("Don't know how to resize %s partitions." % fstype)
365 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
366 self.g.e2fsck_f(part_dev)
367 self.g.resize2fs_M(part_dev)
369 out = self.g.tune2fs_l(part_dev)
371 filter(lambda x: x[0] == 'Block size', out)[0][1])
373 filter(lambda x: x[0] == 'Block count', out)[0][1])
375 start = last_part['part_start'] / sector_size
376 end = start + (block_size * block_cnt) / sector_size - 1
378 if is_logical(last_part):
379 partitions = self.g.part_list(self.guestfs_device)
381 logical = [] # logical partitions
382 for partition in partitions:
383 if partition['part_num'] < 4:
386 'num': partition['part_num'],
387 'start': partition['part_start'] / sector_size,
388 'end': partition['part_end'] / sector_size,
389 'id': part_get_(partition['part_num']),
390 'bootable': part_get_bootable(partition['part_num'])
393 logical[-1]['end'] = end # new end after resize
395 # Recreate the extended partition
396 extended = [p for p in partitions if self._is_extended(p)][0]
397 part_del(extended['part_num'])
398 part_add('e', extended['part_start'], end)
400 # Create all the logical partitions back
402 part_add('l', l['start'], l['end'])
403 part_set_id(l['num'], l['id'])
404 part_set_bootable(l['num'], l['bootable'])
406 # Recreate the last partition
407 if self.meta['PARTITION_TABLE'] == 'msdos':
408 last_part['id'] = part_get_id(last_part['part_num'])
410 last_part['bootable'] = part_get_bootable(last_part['part_num'])
411 part_del(last_part['part_num'])
412 part_add('p', start, end)
413 part_set_bootable(last_part['part_num'], last_part['bootable'])
415 if self.meta['PARTITION_TABLE'] == 'msdos':
416 part_set_id(last_part['part_num'], last_part['id'])
418 new_size = (end + 1) * sector_size
420 assert (new_size <= self.size)
422 if self.meta['PARTITION_TABLE'] == 'gpt':
423 ptable = GPTPartitionTable(self.real_device)
424 self.size = ptable.shrink(new_size, self.size)
426 self.size = min(new_size + 2048 * sector_size, self.size)
428 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
432 def dump(self, outfile):
433 """Dumps the content of device into a file.
435 This method will only dump the actual payload, found by reading the
436 partition table. Empty space in the end of the device will be ignored.
439 blocksize = 4 * MB # 4MB
441 progr_size = (size + MB - 1) // MB # in MB
442 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
444 with open(self.real_device, 'r') as src:
445 with open(outfile, "w") as dst:
450 length = min(left, blocksize)
451 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
455 progressbar.goto((size - left) // MB)
456 progressbar.success('image file %s was successfully created' % outfile)
458 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :