3 from image_creator.util import get_command
4 from image_creator import FatalError
5 from clint.textui import progress
17 class DiskError(Exception):
20 dd = get_command('dd')
21 dmsetup = get_command('dmsetup')
22 losetup = get_command('losetup')
23 blockdev = get_command('blockdev')
27 """This class represents a hard disk hosting an Operating System
29 A Disk instance never alters the source media it is created from.
30 Any change is done on a snapshot created by the device-mapper of
34 def __init__(self, source):
35 """Create a new Disk instance out of a source media. The source
36 media can be an image file, a block device or a directory."""
37 self._cleanup_jobs = []
41 def _add_cleanup(self, job, *args):
42 self._cleanup_jobs.append((job, args))
44 def _losetup(self, fname):
45 loop = losetup('-f', '--show', fname)
46 loop = loop.strip() # remove the new-line char
47 self._add_cleanup(losetup, '-d', loop)
50 def _dir_to_disk(self):
51 raise NotImplementedError
54 """Cleanup internal data. This needs to be called before the
57 while len(self._devices):
58 device = self._devices.pop()
61 while len(self._cleanup_jobs):
62 job, args = self._cleanup_jobs.pop()
66 """Returns a newly created DiskDevice instance.
68 This instance is a snapshot of the original source media of
71 sourcedev = self.source
72 mode = os.stat(self.source).st_mode
73 if stat.S_ISDIR(mode):
74 return self._losetup(self._dir_to_disk())
75 elif stat.S_ISREG(mode):
76 sourcedev = self._losetup(self.source)
77 elif not stat.S_ISBLK(mode):
78 raise ValueError("Value for self.source is invalid")
80 # Take a snapshot and return it to the user
81 size = blockdev('--getsize', sourcedev)
82 cowfd, cow = tempfile.mkstemp()
84 self._add_cleanup(os.unlink, cow)
85 # Create 1G cow sparse file
86 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
87 cowdev = self._losetup(cow)
89 snapshot = uuid.uuid4().hex
90 tablefd, table = tempfile.mkstemp()
92 os.write(tablefd, "0 %d snapshot %s %s n 8" % \
93 (int(size), sourcedev, cowdev))
94 dmsetup('create', snapshot, table)
95 self._add_cleanup(dmsetup, 'remove', snapshot)
98 new_device = DiskDevice("/dev/mapper/%s" % snapshot)
99 self._devices.append(new_device)
103 def destroy_device(self, device):
104 """Destroys a DiskDevice instance previously created by
107 self._devices.remove(device)
111 def progress_generator(label=''):
113 for i in progress.bar(range(100),label):
117 yield #suppress the StopIteration exception
120 class DiskDevice(object):
121 """This class represents a block device hosting an Operating System
122 as created by the device-mapper.
125 def __init__(self, device, bootable=True):
126 """Create a new DiskDevice."""
129 self.bootable = bootable
130 self.progress_bar = None
132 self.g = guestfs.GuestFS()
133 self.g.add_drive_opts(self.device, readonly=0)
136 #self.g.set_verbose(1)
138 self.guestfs_enabled = False
141 """Enable a newly created DiskDevice"""
143 self.progressbar = progress_generator("VM lauch: ")
144 self.progressbar.next()
145 eh = self.g.set_event_callback(self.progress_callback, guestfs.EVENT_PROGRESS)
147 self.guestfs_enabled = True
148 self.g.delete_event_callback(eh)
149 if self.progressbar is not None:
150 self.progressbar.send(100)
151 self.progressbar = None
153 roots = self.g.inspect_os()
155 raise FatalError("No operating system found")
157 raise FatalError("Multiple operating systems found")
160 self.ostype = self.g.inspect_get_type(self.root)
161 self.distro = self.g.inspect_get_distro(self.root)
164 """Destroy this DiskDevice instance."""
166 if self.guestfs_enabled:
170 # Close the guestfs handler if open
173 def progress_callback(self, ev, eh, buf, array):
177 self.progressbar.send((position * 100)//total)
179 if position == total:
180 self.progressbar = None
183 """Mount all disk partitions in a correct order."""
184 mps = self.g.inspect_get_mountpoints(self.root)
186 # Sort the keys to mount the fs in a correct order.
187 # / should be mounted befor /boot, etc
189 if len(a[0]) > len(b[0]):
191 elif len(a[0]) == len(b[0]):
198 self.g.mount(dev, mp)
199 except RuntimeError as msg:
200 print "%s (ignored)" % msg
203 """Umount all mounted filesystems."""
209 This is accomplished by shrinking the last filesystem in the
210 disk and then updating the partition table. The new disk size
211 (in bytes) is returned.
213 dev = self.g.part_to_dev(self.root)
214 parttype = self.g.part_get_parttype(dev)
215 if parttype != 'msdos':
216 raise FatalError("You have a %s partition table. "
217 "Only msdos partitions are supported" % parttype)
219 last_partition = self.g.part_list(dev)[-1]
221 if last_partition['part_num'] > 4:
222 raise FatalError("This disk contains logical partitions. "
223 "Only primary partitions are supported.")
225 part_dev = "%s%d" % (dev, last_partition['part_num'])
226 fs_type = self.g.vfs_type(part_dev)
227 if not re.match("ext[234]", fs_type):
228 print "Warning: Don't know how to resize %s partitions." % vfs_type
231 self.g.e2fsck_f(part_dev)
232 self.g.resize2fs_M(part_dev)
233 output = self.g.tune2fs_l(part_dev)
234 block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
235 block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
237 sector_size = self.g.blockdev_getss(dev)
239 start = last_partition['part_start'] / sector_size
240 end = start + (block_size * block_cnt) / sector_size - 1
242 self.g.part_del(dev, last_partition['part_num'])
243 self.g.part_add(dev, 'p', start, end)
245 return (end + 1) * sector_size
248 """Returns the "payload" size of the device.
250 The size returned by this method is the size of the space occupied by
251 the partitions (including the space before the first partition).
253 dev = self.g.part_to_dev(self.root)
254 last = self.g.part_list(dev)[-1]
256 return last['part_end']
258 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :