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 warn, progress, success, output, 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):
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 = []
72 def _add_cleanup(self, job, *args):
73 self._cleanup_jobs.append((job, args))
75 def _losetup(self, fname):
76 loop = losetup('-f', '--show', fname)
77 loop = loop.strip() # remove the new-line char
78 self._add_cleanup(losetup, '-d', loop)
81 def _dir_to_disk(self):
82 raise NotImplementedError
85 """Cleanup internal data. This needs to be called before the
88 while len(self._devices):
89 device = self._devices.pop()
92 while len(self._cleanup_jobs):
93 job, args = self._cleanup_jobs.pop()
97 """Creates a snapshot of the original source media of the Disk
101 output("Examining source media `%s'..." % self.source, False)
102 sourcedev = self.source
103 mode = os.stat(self.source).st_mode
104 if stat.S_ISDIR(mode):
105 success('looks like a directory')
106 return self._losetup(self._dir_to_disk())
107 elif stat.S_ISREG(mode):
108 success('looks like an image file')
109 sourcedev = self._losetup(self.source)
110 elif not stat.S_ISBLK(mode):
111 raise ValueError("Invalid media source. Only block devices, "
112 "regular files and directories are supported.")
114 success('looks like a block device')
116 # Take a snapshot and return it to the user
117 output("Snapshotting media source...", False)
118 size = blockdev('--getsize', sourcedev)
119 cowfd, cow = tempfile.mkstemp()
121 self._add_cleanup(os.unlink, cow)
122 # Create 1G cow sparse file
123 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', \
124 '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)
142 return "/dev/mapper/%s" % snapshot
144 def get_device(self, media):
145 """Returns a newly created DiskDevice instance."""
147 new_device = DiskDevice(media)
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, bootable=True):
166 """Create a new DiskDevice."""
168 self.real_device = device
169 self.bootable = bootable
170 self.progress_bar = None
171 self.guestfs_device = None
175 self.g = guestfs.GuestFS()
176 self.g.add_drive_opts(self.real_device, readonly=0)
179 #self.g.set_verbose(1)
181 self.guestfs_enabled = False
184 """Enable a newly created DiskDevice"""
185 self.progressbar = progress("Launching helper VM: ", "percent")
186 self.progressbar.max = 100
187 self.progressbar.goto(1)
188 eh = self.g.set_event_callback(self.progress_callback,
189 guestfs.EVENT_PROGRESS)
191 self.guestfs_enabled = True
192 self.g.delete_event_callback(eh)
193 if self.progressbar is not None:
194 output("\rLaunching helper VM...\033[K", False)
196 self.progressbar = None
198 output('Inspecting Operating System...', False)
199 roots = self.g.inspect_os()
201 raise FatalError("No operating system found")
203 raise FatalError("Multiple operating systems found."
204 "We only support images with one filesystem.")
206 self.guestfs_device = self.g.part_to_dev(self.root)
207 self.size = self.g.blockdev_getsize64(self.guestfs_device)
208 self.parttype = self.g.part_get_parttype(self.guestfs_device)
210 self.ostype = self.g.inspect_get_type(self.root)
211 self.distro = self.g.inspect_get_distro(self.root)
212 success('found a(n) %s system' % self.distro)
215 """Destroy this DiskDevice instance."""
217 if self.guestfs_enabled:
221 # Close the guestfs handler if open
224 def progress_callback(self, ev, eh, buf, array):
228 self.progressbar.goto((position * 100) // total)
231 """Mount all disk partitions in a correct order."""
233 output("Mounting image...", False)
234 mps = self.g.inspect_get_mountpoints(self.root)
236 # Sort the keys to mount the fs in a correct order.
237 # / should be mounted befor /boot, etc
239 if len(a[0]) > len(b[0]):
241 elif len(a[0]) == len(b[0]):
248 self.g.mount(dev, mp)
249 except RuntimeError as msg:
250 warn("%s (ignored)" % msg)
254 """Umount all mounted filesystems."""
260 This is accomplished by shrinking the last filesystem in the
261 disk and then updating the partition table. The new disk size
262 (in bytes) is returned.
264 ATTENTION: make sure unmount is called before shrink
266 output("Shrinking image (this may take a while)...", False)
268 if self.parttype not in 'msdos' 'gpt':
269 raise FatalError("You have a %s partition table. "
270 "Only msdos and gpt partitions are supported" % self.parttype)
272 last_partition = self.g.part_list(self.guestfs_device)[-1]
274 if self.parttype == 'msdos' and last_partition['part_num'] > 4:
275 raise FatalError("This disk contains logical partitions. "
276 "Only primary partitions are supported.")
278 part_dev = "%s%d" % (self.guestfs_device, last_partition['part_num'])
279 fs_type = self.g.vfs_type(part_dev)
280 if not re.match("ext[234]", fs_type):
281 warn("Don't know how to resize %s partitions." % fs_type)
284 self.g.e2fsck_f(part_dev)
285 self.g.resize2fs_M(part_dev)
287 out = self.g.tune2fs_l(part_dev)
289 filter(lambda x: x[0] == 'Block size', out)[0][1])
291 filter(lambda x: x[0] == 'Block count', out)[0][1])
293 sector_size = self.g.blockdev_getss(self.guestfs_device)
295 start = last_partition['part_start'] / sector_size
296 end = start + (block_size * block_cnt) / sector_size - 1
298 self.g.part_del(self.guestfs_device, last_partition['part_num'])
299 self.g.part_add(self.guestfs_device, 'p', start, end)
301 self.size = (end + 1) * sector_size
302 success("new image size is %dMB" %
303 ((self.size + 2 ** 20 - 1) // 2 ** 20))
305 if self.parttype == 'gpt':
306 ptable = GPTPartitionTable(self.real_device)
307 self.size = ptable.shrink(self.size)
311 def dump(self, outfile):
312 """Dumps the content of device into a file.
314 This method will only dump the actual payload, found by reading the
315 partition table. Empty space in the end of the device will be ignored.
317 blocksize = 2 ** 22 # 4MB
318 progress_size = (self.size + 2 ** 20 - 1) // 2 ** 20 # in MB
319 progressbar = progress("Dumping image file: ", 'mb')
320 progressbar.max = progress_size
321 with open(self.real_device, 'r') as source:
322 with open(outfile, "w") as dest:
327 length = min(left, blocksize)
328 sent = sendfile(dest.fileno(), source.fileno(), offset,
332 progressbar.goto((self.size - left) // 2 ** 20)
333 output("\rDumping image file...\033[K", False)
334 success('image file %s was successfully created' % outfile)
336 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :