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
36 from image_creator import FatalError
46 from sendfile import sendfile
49 class DiskError(Exception):
52 dd = get_command('dd')
53 dmsetup = get_command('dmsetup')
54 losetup = get_command('losetup')
55 blockdev = get_command('blockdev')
59 """This class represents a hard disk hosting an Operating System
61 A Disk instance never alters the source media it is created from.
62 Any change is done on a snapshot created by the device-mapper of
66 def __init__(self, source):
67 """Create a new Disk instance out of a source media. The source
68 media can be an image file, a block device or a directory."""
69 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 """Returns a newly created DiskDevice instance.
100 This instance is a snapshot of the original source media of
104 output("Examining source media `%s'..." % self.source, False)
105 sourcedev = self.source
106 mode = os.stat(self.source).st_mode
107 if stat.S_ISDIR(mode):
108 success('looks like a directory')
109 return self._losetup(self._dir_to_disk())
110 elif stat.S_ISREG(mode):
111 success('looks like an image file')
112 sourcedev = self._losetup(self.source)
113 elif not stat.S_ISBLK(mode):
114 raise ValueError("Invalid media source. Only block devices, "
115 "regular files and directories are supported.")
117 success('looks like a block device')
119 # Take a snapshot and return it to the user
120 output("Snapshotting media source...", False)
121 size = blockdev('--getsize', sourcedev)
122 cowfd, cow = tempfile.mkstemp()
124 self._add_cleanup(os.unlink, cow)
125 # Create 1G cow sparse file
126 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', \
127 'seek=%d' % (1024 * 1024))
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)
145 new_device = DiskDevice("/dev/mapper/%s" % snapshot)
146 self._devices.append(new_device)
150 def destroy_device(self, device):
151 """Destroys a DiskDevice instance previously created by
154 self._devices.remove(device)
158 class DiskDevice(object):
159 """This class represents a block device hosting an Operating System
160 as created by the device-mapper.
163 def __init__(self, device, bootable=True):
164 """Create a new DiskDevice."""
167 self.bootable = bootable
168 self.progress_bar = None
170 self.g = guestfs.GuestFS()
171 self.g.add_drive_opts(self.device, readonly=0)
174 #self.g.set_verbose(1)
176 self.guestfs_enabled = False
179 """Enable a newly created DiskDevice"""
180 self.progressbar = progress("Launching helper VM: ")
181 self.progressbar.next()
182 eh = self.g.set_event_callback(self.progress_callback,
183 guestfs.EVENT_PROGRESS)
185 self.guestfs_enabled = True
186 self.g.delete_event_callback(eh)
187 if self.progressbar is not None:
188 self.progressbar.send(100)
189 self.progressbar = None
191 output('Inspecting Operating System...', False)
192 roots = self.g.inspect_os()
194 raise FatalError("No operating system found")
196 raise FatalError("Multiple operating systems found."
197 "We only support images with one filesystem.")
199 self.ostype = self.g.inspect_get_type(self.root)
200 self.distro = self.g.inspect_get_distro(self.root)
201 success('found a %s system' % self.distro)
204 """Destroy this DiskDevice instance."""
206 if self.guestfs_enabled:
210 # Close the guestfs handler if open
213 def progress_callback(self, ev, eh, buf, array):
217 self.progressbar.send((position * 100) // total)
219 if position == total:
220 self.progressbar = None
223 """Mount all disk partitions in a correct order."""
225 output("Mounting image...", False)
226 mps = self.g.inspect_get_mountpoints(self.root)
228 # Sort the keys to mount the fs in a correct order.
229 # / should be mounted befor /boot, etc
231 if len(a[0]) > len(b[0]):
233 elif len(a[0]) == len(b[0]):
240 self.g.mount(dev, mp)
241 except RuntimeError as msg:
242 warn("%s (ignored)" % msg)
246 """Umount all mounted filesystems."""
252 This is accomplished by shrinking the last filesystem in the
253 disk and then updating the partition table. The new disk size
254 (in bytes) is returned.
256 output("Shrinking image (this may take a while)...", False)
258 dev = self.g.part_to_dev(self.root)
259 parttype = self.g.part_get_parttype(dev)
260 if parttype != 'msdos':
261 raise FatalError("You have a %s partition table. "
262 "Only msdos partitions are supported" % parttype)
264 last_partition = self.g.part_list(dev)[-1]
266 if last_partition['part_num'] > 4:
267 raise FatalError("This disk contains logical partitions. "
268 "Only primary partitions are supported.")
270 part_dev = "%s%d" % (dev, last_partition['part_num'])
271 fs_type = self.g.vfs_type(part_dev)
272 if not re.match("ext[234]", fs_type):
273 warn("Don't know how to resize %s partitions." % vfs_type)
276 self.g.e2fsck_f(part_dev)
277 self.g.resize2fs_M(part_dev)
279 output = self.g.tune2fs_l(part_dev)
281 filter(lambda x: x[0] == 'Block size', output)[0][1])
283 filter(lambda x: x[0] == 'Block count', output)[0][1])
285 sector_size = self.g.blockdev_getss(dev)
287 start = last_partition['part_start'] / sector_size
288 end = start + (block_size * block_cnt) / sector_size - 1
290 self.g.part_del(dev, last_partition['part_num'])
291 self.g.part_add(dev, 'p', start, end)
293 new_size = (end + 1) * sector_size
294 success("new image size is %dMB" %
295 ((new_size + 2 ** 20 - 1) // 2 ** 20))
299 """Returns the "payload" size of the device.
301 The size returned by this method is the size of the space occupied by
302 the partitions (including the space before the first partition).
304 dev = self.g.part_to_dev(self.root)
305 last = self.g.part_list(dev)[-1]
307 return last['part_end'] + 1
309 def dump(self, outfile):
310 """Dumps the content of device into a file.
312 This method will only dump the actual payload, found by reading the
313 partition table. Empty space in the end of the device will be ignored.
315 blocksize = 2 ** 22 # 4MB
317 progress_size = (size + 2 ** 20 - 1) // 2 ** 20 # in MB
318 progressbar = progress("Dumping image file: ", progress_size)
320 source = open(self.device, "r")
322 dest = open(outfile, "w")
328 length = min(left, blocksize)
329 sent = sendfile(dest.fileno(), source.fileno(), offset,
333 for i in range((length + 2 ** 20 - 1) // 2 ** 20):
340 success('Image file %s was successfully created' % outfile)
342 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :