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 assert self.progress_bar is not None
178 print 'posisition/total: %s/%s' % (position, total)
179 self.progress_bar.send((position * 100)//total)
181 if position == total:
182 self.progress_bar = None
185 """Mount all disk partitions in a correct order."""
186 mps = self.g.inspect_get_mountpoints(self.root)
188 # Sort the keys to mount the fs in a correct order.
189 # / should be mounted befor /boot, etc
191 if len(a[0]) > len(b[0]):
193 elif len(a[0]) == len(b[0]):
200 self.g.mount(dev, mp)
201 except RuntimeError as msg:
202 print "%s (ignored)" % msg
205 """Umount all mounted filesystems."""
211 This is accomplished by shrinking the last filesystem in the
212 disk and then updating the partition table. The new disk size
213 (in bytes) is returned.
215 dev = self.g.part_to_dev(self.root)
216 parttype = self.g.part_get_parttype(dev)
217 if parttype != 'msdos':
218 raise FatalError("You have a %s partition table. "
219 "Only msdos partitions are supported" % parttype)
221 last_partition = self.g.part_list(dev)[-1]
223 if last_partition['part_num'] > 4:
224 raise FatalError("This disk contains logical partitions. "
225 "Only primary partitions are supported.")
227 part_dev = "%s%d" % (dev, last_partition['part_num'])
228 fs_type = self.g.vfs_type(part_dev)
229 if not re.match("ext[234]", fs_type):
230 print "Warning: Don't know how to resize %s partitions." % vfs_type
233 self.g.e2fsck_f(part_dev)
234 self.g.resize2fs_M(part_dev)
235 output = self.g.tune2fs_l(part_dev)
236 block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
237 block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
239 sector_size = self.g.blockdev_getss(dev)
241 start = last_partition['part_start'] / sector_size
242 end = start + (block_size * block_cnt) / sector_size - 1
244 self.g.part_del(dev, last_partition['part_num'])
245 self.g.part_add(dev, 'p', start, end)
247 return (end + 1) * sector_size
250 """Returns the "payload" size of the device.
252 The size returned by this method is the size of the space occupied by
253 the partitions (including the space before the first partition).
255 dev = self.g.part_to_dev(self.root)
256 last = self.g.part_list(dev)[-1]
258 return last['part_end']
260 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :