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 import FatalError
36 from clint.textui import progress
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 """Returns a newly created DiskDevice instance.
99 This instance is a snapshot of the original source media of
102 sourcedev = self.source
103 mode = os.stat(self.source).st_mode
104 if stat.S_ISDIR(mode):
105 return self._losetup(self._dir_to_disk())
106 elif stat.S_ISREG(mode):
107 sourcedev = self._losetup(self.source)
108 elif not stat.S_ISBLK(mode):
109 raise ValueError("Value for self.source is invalid")
111 # Take a snapshot and return it to the user
112 size = blockdev('--getsize', sourcedev)
113 cowfd, cow = tempfile.mkstemp()
115 self._add_cleanup(os.unlink, cow)
116 # Create 1G cow sparse file
117 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
118 cowdev = self._losetup(cow)
120 snapshot = uuid.uuid4().hex
121 tablefd, table = tempfile.mkstemp()
123 os.write(tablefd, "0 %d snapshot %s %s n 8" % \
124 (int(size), sourcedev, cowdev))
125 dmsetup('create', snapshot, table)
126 self._add_cleanup(dmsetup, 'remove', snapshot)
129 new_device = DiskDevice("/dev/mapper/%s" % snapshot)
130 self._devices.append(new_device)
134 def destroy_device(self, device):
135 """Destroys a DiskDevice instance previously created by
138 self._devices.remove(device)
142 def progress_generator(label=''):
144 for i in progress.bar(range(100), label):
148 yield # suppress the StopIteration exception
151 class DiskDevice(object):
152 """This class represents a block device hosting an Operating System
153 as created by the device-mapper.
156 def __init__(self, device, bootable=True):
157 """Create a new DiskDevice."""
160 self.bootable = bootable
161 self.progress_bar = None
163 self.g = guestfs.GuestFS()
164 self.g.add_drive_opts(self.device, readonly=0)
167 #self.g.set_verbose(1)
169 self.guestfs_enabled = False
172 """Enable a newly created DiskDevice"""
174 self.progressbar = progress_generator("VM lauch: ")
175 self.progressbar.next()
176 eh = self.g.set_event_callback(self.progress_callback,
177 guestfs.EVENT_PROGRESS)
179 self.guestfs_enabled = True
180 self.g.delete_event_callback(eh)
181 if self.progressbar is not None:
182 self.progressbar.send(100)
183 self.progressbar = None
185 roots = self.g.inspect_os()
187 raise FatalError("No operating system found")
189 raise FatalError("Multiple operating systems found")
192 self.ostype = self.g.inspect_get_type(self.root)
193 self.distro = self.g.inspect_get_distro(self.root)
196 """Destroy this DiskDevice instance."""
198 if self.guestfs_enabled:
202 # Close the guestfs handler if open
205 def progress_callback(self, ev, eh, buf, array):
209 self.progressbar.send((position * 100) // total)
211 if position == total:
212 self.progressbar = None
215 """Mount all disk partitions in a correct order."""
216 mps = self.g.inspect_get_mountpoints(self.root)
218 # Sort the keys to mount the fs in a correct order.
219 # / should be mounted befor /boot, etc
221 if len(a[0]) > len(b[0]):
223 elif len(a[0]) == len(b[0]):
230 self.g.mount(dev, mp)
231 except RuntimeError as msg:
232 print "%s (ignored)" % msg
235 """Umount all mounted filesystems."""
241 This is accomplished by shrinking the last filesystem in the
242 disk and then updating the partition table. The new disk size
243 (in bytes) is returned.
245 dev = self.g.part_to_dev(self.root)
246 parttype = self.g.part_get_parttype(dev)
247 if parttype != 'msdos':
248 raise FatalError("You have a %s partition table. "
249 "Only msdos partitions are supported" % parttype)
251 last_partition = self.g.part_list(dev)[-1]
253 if last_partition['part_num'] > 4:
254 raise FatalError("This disk contains logical partitions. "
255 "Only primary partitions are supported.")
257 part_dev = "%s%d" % (dev, last_partition['part_num'])
258 fs_type = self.g.vfs_type(part_dev)
259 if not re.match("ext[234]", fs_type):
260 print "Warning: Don't know how to resize %s partitions." % vfs_type
263 self.g.e2fsck_f(part_dev)
264 self.g.resize2fs_M(part_dev)
265 output = self.g.tune2fs_l(part_dev)
266 block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
267 block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
269 sector_size = self.g.blockdev_getss(dev)
271 start = last_partition['part_start'] / sector_size
272 end = start + (block_size * block_cnt) / sector_size - 1
274 self.g.part_del(dev, last_partition['part_num'])
275 self.g.part_add(dev, 'p', start, end)
277 return (end + 1) * sector_size
280 """Returns the "payload" size of the device.
282 The size returned by this method is the size of the space occupied by
283 the partitions (including the space before the first partition).
285 dev = self.g.part_to_dev(self.root)
286 last = self.g.part_list(dev)[-1]
288 return last['part_end']
290 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :