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, warn, progress_generator, success
35 from image_creator import FatalError
36 from clint.textui import puts
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 puts("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 puts("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_generator("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 puts('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."""
224 mps = self.g.inspect_get_mountpoints(self.root)
226 # Sort the keys to mount the fs in a correct order.
227 # / should be mounted befor /boot, etc
229 if len(a[0]) > len(b[0]):
231 elif len(a[0]) == len(b[0]):
238 self.g.mount(dev, mp)
239 except RuntimeError as msg:
240 print "%s (ignored)" % msg
243 """Umount all mounted filesystems."""
249 This is accomplished by shrinking the last filesystem in the
250 disk and then updating the partition table. The new disk size
251 (in bytes) is returned.
253 puts("Shrinking image (this may take a while)...", False)
256 dev = self.g.part_to_dev(self.root)
257 parttype = self.g.part_get_parttype(dev)
258 if parttype != 'msdos':
259 raise FatalError("You have a %s partition table. "
260 "Only msdos partitions are supported" % parttype)
262 last_partition = self.g.part_list(dev)[-1]
264 if last_partition['part_num'] > 4:
265 raise FatalError("This disk contains logical partitions. "
266 "Only primary partitions are supported.")
268 part_dev = "%s%d" % (dev, last_partition['part_num'])
269 fs_type = self.g.vfs_type(part_dev)
270 if not re.match("ext[234]", fs_type):
271 warn("Don't know how to resize %s partitions." % vfs_type)
274 self.g.e2fsck_f(part_dev)
275 self.g.resize2fs_M(part_dev)
277 output = self.g.tune2fs_l(part_dev)
279 filter(lambda x: x[0] == 'Block size', output)[0][1])
281 filter(lambda x: x[0] == 'Block count', output)[0][1])
283 sector_size = self.g.blockdev_getss(dev)
285 start = last_partition['part_start'] / sector_size
286 end = start + (block_size * block_cnt) / sector_size - 1
288 self.g.part_del(dev, last_partition['part_num'])
289 self.g.part_add(dev, 'p', start, end)
291 new_size = (end + 1) * sector_size
292 success("new image size is %dMB" %
293 ((new_size + 2 ** 20 - 1) // 2 ** 20))
297 """Returns the "payload" size of the device.
299 The size returned by this method is the size of the space occupied by
300 the partitions (including the space before the first partition).
302 dev = self.g.part_to_dev(self.root)
303 last = self.g.part_list(dev)[-1]
305 return last['part_end'] + 1
307 def dump(self, outfile):
308 """Dumps the content of device into a file.
310 This method will only dump the actual payload, found by reading the
311 partition table. Empty space in the end of the device will be ignored.
313 blocksize = 2 ** 22 # 4MB
315 progress_size = (size + 2 ** 20 - 1) // 2 ** 20 # in MB
316 progressbar = progress_generator("Dumping image file: ", progress_size)
318 source = open(self.device, "r")
320 dest = open(outfile, "w")
326 length = min(left, blocksize)
327 sent = sendfile(dest.fileno(), source.fileno(), offset,
331 for i in range((length + 2 ** 20 - 1) // 2 ** 20):
338 success('Image file %s was successfully created' % outfile)
340 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :