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 bundle_volume
46 from sendfile import sendfile
49 dd = get_command('dd')
50 dmsetup = get_command('dmsetup')
51 losetup = get_command('losetup')
52 blockdev = get_command('blockdev')
56 """This class represents a hard disk hosting an Operating System
58 A Disk instance never alters the source media it is created from.
59 Any change is done on a snapshot created by the device-mapper of
63 def __init__(self, source, output):
64 """Create a new Disk instance out of a source media. The source
65 media can be an image file, a block device or a directory."""
66 self._cleanup_jobs = []
71 def _add_cleanup(self, job, *args):
72 self._cleanup_jobs.append((job, args))
74 def _losetup(self, fname):
75 loop = losetup('-f', '--show', fname)
76 loop = loop.strip() # remove the new-line char
77 self._add_cleanup(losetup, '-d', loop)
80 def _dir_to_disk(self):
81 if self.source == '/':
82 return bundle_volume(self.out)
83 raise FatalError("Using a directory as media source is supported")
86 """Cleanup internal data. This needs to be called before the
90 while len(self._devices):
91 device = self._devices.pop()
94 # Make sure those are executed even if one of the device.destroy
95 # methods throws exeptions.
96 while len(self._cleanup_jobs):
97 job, args = self._cleanup_jobs.pop()
101 """Creates a snapshot of the original source media of the Disk
105 self.out.output("Examining source media `%s'..." % self.source, False)
106 sourcedev = self.source
107 mode = os.stat(self.source).st_mode
108 if stat.S_ISDIR(mode):
109 self.out.success('looks like a directory')
110 return self._losetup(self._dir_to_disk())
111 elif stat.S_ISREG(mode):
112 self.out.success('looks like an image file')
113 sourcedev = self._losetup(self.source)
114 elif not stat.S_ISBLK(mode):
115 raise ValueError("Invalid media source. Only block devices, "
116 "regular files and directories are supported.")
118 self.out.success('looks like a block device')
120 # Take a snapshot and return it to the user
121 self.out.output("Snapshotting media source...", False)
122 size = blockdev('--getsz', sourcedev)
123 cowfd, cow = tempfile.mkstemp()
125 self._add_cleanup(os.unlink, cow)
126 # Create cow sparse file
127 dd('if=/dev/null', 'of=%s' % cow, 'bs=512', 'seek=%d' % int(size))
128 cowdev = self._losetup(cow)
130 snapshot = uuid.uuid4().hex
131 tablefd, table = tempfile.mkstemp()
133 os.write(tablefd, "0 %d snapshot %s %s n 8" %
134 (int(size), sourcedev, cowdev))
135 dmsetup('create', snapshot, table)
136 self._add_cleanup(dmsetup, 'remove', snapshot)
137 # Sometimes dmsetup remove fails with Device or resource busy,
138 # although everything is cleaned up and the snapshot is not
139 # used by anyone. Add a 2 seconds delay to be on the safe side.
140 self._add_cleanup(time.sleep, 2)
144 self.out.success('done')
145 return "/dev/mapper/%s" % snapshot
147 def get_device(self, media):
148 """Returns a newly created DiskDevice instance."""
150 new_device = DiskDevice(media, self.out)
151 self._devices.append(new_device)
155 def destroy_device(self, device):
156 """Destroys a DiskDevice instance previously created by
159 self._devices.remove(device)
163 class DiskDevice(object):
164 """This class represents a block device hosting an Operating System
165 as created by the device-mapper.
168 def __init__(self, device, output, bootable=True):
169 """Create a new DiskDevice."""
171 self.real_device = device
173 self.bootable = bootable
174 self.progress_bar = None
175 self.guestfs_device = None
179 self.g = guestfs.GuestFS()
180 self.g.add_drive_opts(self.real_device, readonly=0)
182 # Before version 1.17.14 the recovery process, which is a fork of the
183 # original process that called libguestfs, did not close its inherited
184 # file descriptors. This can cause problems especially if the parent
185 # process has opened pipes. Since the recovery process is an optional
186 # feature of libguestfs, it's better to disable it.
187 self.g.set_recovery_proc(0)
188 version = self.g.version()
189 if version['major'] > 1 or \
190 (version['major'] == 1 and (version['minor'] >= 18 or
191 (version['minor'] == 17 and
192 version['release'] >= 14))):
193 self.g.set_recovery_proc(1)
194 self.out.output("Enabling recovery proc")
197 #self.g.set_verbose(1)
199 self.guestfs_enabled = False
202 """Enable a newly created DiskDevice"""
203 self.progressbar = self.out.Progress(100, "Launching helper VM",
205 eh = self.g.set_event_callback(self.progress_callback,
206 guestfs.EVENT_PROGRESS)
208 self.guestfs_enabled = True
209 self.g.delete_event_callback(eh)
210 self.progressbar.success('done')
211 self.progressbar = None
213 self.out.output('Inspecting Operating System...', False)
214 roots = self.g.inspect_os()
216 raise FatalError("No operating system found")
218 raise FatalError("Multiple operating systems found."
219 "We only support images with one OS.")
221 self.guestfs_device = self.g.part_to_dev(self.root)
222 self.size = self.g.blockdev_getsize64(self.guestfs_device)
223 self.meta['PARTITION_TABLE'] = \
224 self.g.part_get_parttype(self.guestfs_device)
226 self.ostype = self.g.inspect_get_type(self.root)
227 self.distro = self.g.inspect_get_distro(self.root)
228 self.out.success('found a(n) %s system' % self.distro)
231 """Destroy this DiskDevice instance."""
233 # In new guestfs versions, there is a handy shutdown method for this
235 if self.guestfs_enabled:
239 # Close the guestfs handler if open
242 def progress_callback(self, ev, eh, buf, array):
246 self.progressbar.goto((position * 100) // total)
248 def mount(self, readonly=False):
249 """Mount all disk partitions in a correct order."""
251 mount = self.g.mount_ro if readonly else self.g.mount
252 msg = " read-only" if readonly else ""
253 self.out.output("Mounting the media%s..." % msg, False)
254 mps = self.g.inspect_get_mountpoints(self.root)
256 # Sort the keys to mount the fs in a correct order.
257 # / should be mounted befor /boot, etc
259 if len(a[0]) > len(b[0]):
261 elif len(a[0]) == len(b[0]):
269 except RuntimeError as msg:
270 self.out.warn("%s (ignored)" % msg)
271 self.out.success("done")
274 """Umount all mounted filesystems."""
277 def _last_partition(self):
278 if self.meta['PARTITION_TABLE'] not in 'msdos' 'gpt':
279 msg = "Unsupported partition table: %s. Only msdos and gpt " \
280 "partition tables are supported" % self.meta['PARTITION_TABLE']
281 raise FatalError(msg)
283 is_extended = lambda p: \
284 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
285 is_logical = lambda p: \
286 self.meta['PARTITION_TABLE'] != 'msdos' and p['part_num'] > 4
288 partitions = self.g.part_list(self.guestfs_device)
289 last_partition = partitions[-1]
291 if is_logical(last_partition):
292 # The disk contains extended and logical partitions....
293 extended = [p for p in partitions if is_extended(p)][0]
294 last_primary = [p for p in partitions if p['part_num'] <= 4][-1]
296 # check if extended is the last primary partition
297 if last_primary['part_num'] > extended['part_num']:
298 last_partition = last_primary
300 return last_partition
305 This is accomplished by shrinking the last filesystem in the
306 disk and then updating the partition table. The new disk size
307 (in bytes) is returned.
309 ATTENTION: make sure unmount is called before shrink
311 get_fstype = lambda p: \
312 self.g.vfs_type("%s%d" % (self.guestfs_device, p['part_num']))
313 is_logical = lambda p: \
314 self.meta['PARTITION_TABLE'] == 'msdos' and p['part_num'] > 4
315 is_extended = lambda p: \
316 self.meta['PARTITION_TABLE'] == 'msdos' and \
317 self.g.part_get_mbr_id(self.guestfs_device, p['part_num']) == 5
319 part_add = lambda ptype, start, stop: \
320 self.g.part_add(self.guestfs_device, ptype, start, stop)
321 part_del = lambda p: self.g.part_del(self.guestfs_device, p)
322 part_get_id = lambda p: self.g.part_get_mbr_id(self.guestfs_device, p)
323 part_set_id = lambda p, id: \
324 self.g.part_set_mbr_id(self.guestfs_device, p, id)
325 part_get_bootable = lambda p: \
326 self.g.part_get_bootable(self.guestfs_device, p)
327 part_set_bootable = lambda p, bootable: \
328 self.g.part_set_bootable(self.guestfs_device, p, bootable)
332 self.out.output("Shrinking image (this may take a while)...", False)
334 sector_size = self.g.blockdev_getss(self.guestfs_device)
339 last_part = self._last_partition()
340 fstype = get_fstype(last_part)
343 self.meta['SWAP'] = "%d:%s" % \
344 (last_part['part_num'],
345 (last_part['part_size'] + MB - 1) // MB)
346 part_del(last_part['part_num'])
348 elif is_extended(last_part):
349 part_del(last_part['part_num'])
352 # Most disk manipulation programs leave 2048 sectors after the last
354 new_size = last_part['part_end'] + 1 + 2048 * sector_size
355 self.size = min(self.size, new_size)
358 if not re.match("ext[234]", fstype):
359 self.out.warn("Don't know how to resize %s partitions." % fstype)
362 part_dev = "%s%d" % (self.guestfs_device, last_part['part_num'])
363 self.g.e2fsck_f(part_dev)
364 self.g.resize2fs_M(part_dev)
366 out = self.g.tune2fs_l(part_dev)
368 filter(lambda x: x[0] == 'Block size', out)[0][1])
370 filter(lambda x: x[0] == 'Block count', out)[0][1])
372 start = last_part['part_start'] / sector_size
373 end = start + (block_size * block_cnt) / sector_size - 1
375 if is_logical(last_part):
376 partitions = self.g.part_list(self.guestfs_device)
378 logical = [] # logical partitions
379 for partition in partitions:
380 if partition['part_num'] < 4:
383 'num': partition['part_num'],
384 'start': partition['part_start'] / sector_size,
385 'end': partition['part_end'] / sector_size,
386 'id': part_get_(partition['part_num']),
387 'bootable': part_get_bootable(partition['part_num'])
390 logical[-1]['end'] = end # new end after resize
392 # Recreate the extended partition
393 extended = [p for p in partitions if self._is_extended(p)][0]
394 part_del(extended['part_num'])
395 part_add('e', extended['part_start'], end)
397 # Create all the logical partitions back
399 part_add('l', l['start'], l['end'])
400 part_set_id(l['num'], l['id'])
401 part_set_bootable(l['num'], l['bootable'])
403 # Recreate the last partition
404 if self.meta['PARTITION_TABLE'] == 'msdos':
405 last_part['id'] = part_get_id(last_part['part_num'])
407 last_part['bootable'] = part_get_bootable(last_part['part_num'])
408 part_del(last_part['part_num'])
409 part_add('p', start, end)
410 part_set_bootable(last_part['part_num'], last_part['bootable'])
412 if self.meta['PARTITION_TABLE'] == 'msdos':
413 part_set_id(last_part['part_num'], last_part['id'])
415 new_size = (end + 1) * sector_size
417 assert (new_size <= self.size)
419 if self.meta['PARTITION_TABLE'] == 'gpt':
420 ptable = GPTPartitionTable(self.real_device)
421 self.size = ptable.shrink(new_size, self.size)
423 self.size = min(new_size + 2048 * sector_size, self.size)
425 self.out.success("new size is %dMB" % ((self.size + MB - 1) // MB))
429 def dump(self, outfile):
430 """Dumps the content of device into a file.
432 This method will only dump the actual payload, found by reading the
433 partition table. Empty space in the end of the device will be ignored.
436 blocksize = 4 * MB # 4MB
438 progr_size = (size + MB - 1) // MB # in MB
439 progressbar = self.out.Progress(progr_size, "Dumping image file", 'mb')
441 with open(self.real_device, 'r') as src:
442 with open(outfile, "w") as dst:
447 length = min(left, blocksize)
448 _, sent = sendfile(dst.fileno(), src.fileno(), offset,
452 progressbar.goto((size - left) // MB)
453 progressbar.success('image file %s was successfully created' % outfile)
455 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :