3 from image_creator.util import get_command
4 from clint.textui import progress
16 class DiskError(Exception):
19 dd = get_command('dd')
20 dmsetup = get_command('dmsetup')
21 losetup = get_command('losetup')
22 blockdev = get_command('blockdev')
26 """This class represents a hard disk hosting an Operating System
28 A Disk instance never alters the source media it is created from.
29 Any change is done on a snapshot created by the device-mapper of
33 def __init__(self, source):
34 """Create a new Disk instance out of a source media. The source
35 media can be an image file, a block device or a directory."""
36 self._cleanup_jobs = []
40 def _add_cleanup(self, job, *args):
41 self._cleanup_jobs.append((job, args))
43 def _losetup(self, fname):
44 loop = losetup('-f', '--show', fname)
45 loop = loop.strip() # remove the new-line char
46 self._add_cleanup(losetup, '-d', loop)
49 def _dir_to_disk(self):
50 raise NotImplementedError
53 """Cleanup internal data. This needs to be called before the
56 while len(self._devices):
57 device = self._devices.pop()
60 while len(self._cleanup_jobs):
61 job, args = self._cleanup_jobs.pop()
65 """Returns a newly created DiskDevice instance.
67 This instance is a snapshot of the original source media of
70 sourcedev = self.source
71 mode = os.stat(self.source).st_mode
72 if stat.S_ISDIR(mode):
73 return self._losetup(self._dir_to_disk())
74 elif stat.S_ISREG(mode):
75 sourcedev = self._losetup(self.source)
76 elif not stat.S_ISBLK(mode):
77 raise ValueError("Value for self.source is invalid")
79 # Take a snapshot and return it to the user
80 size = blockdev('--getsize', sourcedev)
81 cowfd, cow = tempfile.mkstemp()
83 self._add_cleanup(os.unlink, cow)
84 # Create 1G cow sparse file
85 dd('if=/dev/null', 'of=%s' % cow, 'bs=1k', 'seek=%d' % (1024 * 1024))
86 cowdev = self._losetup(cow)
88 snapshot = uuid.uuid4().hex
89 tablefd, table = tempfile.mkstemp()
91 os.write(tablefd, "0 %d snapshot %s %s n 8" % \
92 (int(size), sourcedev, cowdev))
93 dmsetup('create', snapshot, table)
94 self._add_cleanup(dmsetup, 'remove', snapshot)
97 new_device = DiskDevice("/dev/mapper/%s" % snapshot)
98 self._devices.append(new_device)
102 def destroy_device(self, device):
103 """Destroys a DiskDevice instance previously created by
106 self._devices.remove(device)
110 def progress_generator(label=''):
112 for i in progress.bar(range(100),''):
116 yield #suppress the StopIteration exception
119 class DiskDevice(object):
120 """This class represents a block device hosting an Operating System
121 as created by the device-mapper.
124 def __init__(self, device, bootable=True):
125 """Create a new DiskDevice."""
128 self.bootable = bootable
129 self.progress_bar = None
131 self.g = guestfs.GuestFS()
132 self.g.add_drive_opts(self.device, readonly=0)
135 #self.g.set_verbose(1)
137 self.guestfs_enabled = False
140 """Enable a newly created DiskDevice"""
142 self.progressbar = progress_generator()
143 self.progressbar.next()
144 eh = self.g.set_event_callback(self.progress_callback, guestfs.EVENT_PROGRESS)
146 self.guestfs_enabled = True
147 self.g.delete_event_callback(eh)
148 if self.progressbar is not None:
149 self.progressbar.send(100)
150 self.progressbar = None
152 roots = self.g.inspect_os()
154 raise DiskError("No operating system found")
156 raise DiskError("Multiple operating systems found")
159 self.ostype = self.g.inspect_get_type(self.root)
160 self.distro = self.g.inspect_get_distro(self.root)
163 """Destroy this DiskDevice instance."""
165 if self.guestfs_enabled:
169 # Close the guestfs handler if open
172 def progress_callback(self, ev, eh, buf, array):
176 assert self.progress_bar is not None
178 self.progress_bar.send((position * 100)//total)
180 if position == total:
181 self.progress_bar = None
184 """Mount all disk partitions in a correct order."""
185 mps = self.g.inspect_get_mountpoints(self.root)
187 # Sort the keys to mount the fs in a correct order.
188 # / should be mounted befor /boot, etc
190 if len(a[0]) > len(b[0]):
192 elif len(a[0]) == len(b[0]):
199 self.g.mount(dev, mp)
200 except RuntimeError as msg:
201 print "%s (ignored)" % msg
204 """Umount all mounted filesystems."""
210 This is accomplished by shrinking the last filesystem in the
211 disk and then updating the partition table. The new disk size
212 (in bytes) is returned.
214 dev = self.g.part_to_dev(self.root)
215 parttype = self.g.part_get_parttype(dev)
216 if parttype != 'msdos':
217 raise DiskError("You have a %s partition table. "
218 "Only msdos partitions are supported" % parttype)
220 last_partition = self.g.part_list(dev)[-1]
222 if last_partition['part_num'] > 4:
223 raise DiskError("This disk contains logical partitions. "
224 "Only primary partitions are supported.")
226 part_dev = "%s%d" % (dev, last_partition['part_num'])
227 fs_type = self.g.vfs_type(part_dev)
228 if not re.match("ext[234]", fs_type):
229 print "Warning: Don't know how to resize %s partitions." % vfs_type
232 self.g.e2fsck_f(part_dev)
233 self.g.resize2fs_M(part_dev)
234 output = self.g.tune2fs_l(part_dev)
235 block_size = int(filter(lambda x: x[0] == 'Block size', output)[0][1])
236 block_cnt = int(filter(lambda x: x[0] == 'Block count', output)[0][1])
238 sector_size = self.g.blockdev_getss(dev)
240 start = last_partition['part_start'] / sector_size
241 end = start + (block_size * block_cnt) / sector_size - 1
243 self.g.part_del(dev, last_partition['part_num'])
244 self.g.part_add(dev, 'p', start, end)
246 return (end + 1) * sector_size
249 """Returns the "payload" size of the device.
251 The size returned by this method is the size of the space occupied by
252 the partitions (including the space before the first partition).
254 dev = self.g.part_to_dev(self.root)
255 last = self.g.part_list(dev)[-1]
257 return last['part_end']
259 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :