1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import logging
22 import os.path
23 import re
24 import stat
25 import string
26 import tempfile
27 import time
28 import VMBuilder
29 from VMBuilder.util import run_cmd
30 from VMBuilder.exception import VMBuilderUserError, VMBuilderException
31
32 TYPE_EXT2 = 0
33 TYPE_EXT3 = 1
34 TYPE_XFS = 2
35 TYPE_SWAP = 3
36
38 - def __init__(self, vm, size='5G', preallocated=False, filename=None):
39 """
40 @type size: string or number
41 @param size: The size of the disk image (passed to L{parse_size})
42
43 @type preallocated: boolean
44 @param preallocated: if True, the disk image already exists and will not be created (useful for raw devices)
45
46 @type filename: string
47 @param filename: force a certain filename or to give the name of the preallocated disk image
48 """
49
50
51 self.vm = vm
52
53
54 self.size = parse_size(size)
55
56 self.preallocated = preallocated
57
58
59 if filename:
60 self.filename = filename
61 else:
62 if self.preallocated:
63 raise VMBuilderUserError('Preallocated was set, but no filename given')
64 self.filename = 'disk%d.img' % len(self.vm.disks)
65
66 self.partitions = []
67
69 """
70 @rtype: string
71 @return: the series of letters that ought to correspond to the device inside
72 the VM. E.g. the first disk of a VM would return 'a', while the 702nd would return 'zz'
73 """
74
75 return index_to_devname(self.vm.disks.index(self))
76
78 """
79 Creates the disk image (unless preallocated), partitions it, creates the partition mapping devices and mkfs's the partitions
80
81 @type directory: string
82 @param directory: If set, the disk image is created in this directory
83 """
84
85 if not self.preallocated:
86 if directory:
87 self.filename = '%s/%s' % (directory, self.filename)
88 logging.info('Creating disk image: %s' % self.filename)
89 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
90 os.chmod(self.filename, stat.S_IRUSR | stat.S_IWUSR)
91
92
93
94
95 logging.info('Adding partition table to disk image: %s' % self.filename)
96 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos')
97
98
99 for part in self.partitions:
100 part.create(self)
101
102 logging.info('Creating loop devices corresponding to the created partitions')
103 self.vm.add_clean_cb(lambda : self.unmap(ignore_fail=True))
104 kpartx_output = run_cmd('kpartx', '-av', self.filename)
105 parts = []
106 for line in kpartx_output.split('\n'):
107 if line == "" or line.startswith("gpt:") or line.startswith("dos:"):
108 continue
109 if line.startswith("add"):
110 parts.append(line)
111 continue
112 logging.error('Skipping unknown line in kpartx output (%s)' % line)
113 mapdevs = []
114 for line in parts:
115 mapdevs.append(line.split(' ')[2])
116 for (part, mapdev) in zip(self.partitions, mapdevs):
117 part.mapdev = '/dev/mapper/%s' % mapdev
118
119
120
121
122
123 logging.info("Creating file systems")
124 for part in self.partitions:
125 part.mkfs()
126
128 """
129 @rtype: string
130 @return: name of the disk as known by grub
131 """
132 return '(hd%d)' % self.get_index()
133
135 """
136 @rtype: number
137 @return: index of the disk (starting from 0)
138 """
139 return self.vm.disks.index(self)
140
141 - def unmap(self, ignore_fail=False):
142 """
143 Destroy all mapping devices
144 """
145
146 time.sleep(3)
147
148 tries = 0
149 max_tries = 3
150 while tries < max_tries:
151 try:
152 run_cmd('kpartx', '-d', self.filename, ignore_fail=False)
153 break
154 except:
155 pass
156 tries += 1
157 time.sleep(3)
158
159 if tries >= max_tries:
160
161 logging.info("Could not unmount '%s' after '%d' attempts. Final attempt" % (self.filename, tries))
162 run_cmd('kpartx', '-d', self.filename, ignore_fail=ignore_fail)
163
164 for part in self.partitions:
165 self.mapdev = None
166
167 - def add_part(self, begin, length, type, mntpnt):
168 """
169 Add a partition to the disk
170
171 @type begin: number
172 @param begin: Start offset of the new partition (in megabytes)
173 @type length:
174 @param length: Size of the new partition (in megabytes)
175 @type type: string
176 @param type: Type of the new partition. Valid options are: ext2 ext3 xfs swap linux-swap
177 @type mntpnt: string
178 @param mntpnt: Intended mountpoint inside the guest of the new partition
179 """
180 end = begin+length-1
181 logging.debug("add_part - begin %d, length %d, end %d" % (begin, length, end))
182 for part in self.partitions:
183 if (begin >= part.begin and begin <= part.end) or \
184 (end >= part.begin and end <= part.end):
185 raise Exception('Partitions are overlapping')
186 if begin > end:
187 raise Exception('Partition\'s last block is before its first')
188 if begin < 0 or end > self.size:
189 raise Exception('Partition is out of bounds. start=%d, end=%d, disksize=%d' % (begin,end,self.size))
190 part = self.Partition(disk=self, begin=begin, end=end, type=str_to_type(type), mntpnt=mntpnt)
191 self.partitions.append(part)
192
193
194 self.partitions.sort(cmp=lambda x,y: x.begin - y.begin)
195
196 - def convert(self, destdir, format):
197 """
198 Convert the disk image
199
200 @type destdir: string
201 @param destdir: Target location of converted disk image
202 @type format: string
203 @param format: The target format (as understood by qemu-img or vdi)
204 @rtype: string
205 @return: the name of the converted image
206 """
207 if self.preallocated:
208
209 return self.filename
210
211 filename = os.path.basename(self.filename)
212 if '.' in filename:
213 filename = filename[:filename.rindex('.')]
214 destfile = '%s/%s.%s' % (destdir, filename, format)
215
216 logging.info('Converting %s to %s, format %s' % (self.filename, format, destfile))
217 if format == 'vdi':
218 run_cmd(vbox_manager_path(), 'convertfromraw', '-format', 'VDI', self.filename, destfile)
219 else:
220 run_cmd(qemu_img_path(), 'convert', '-O', format, self.filename, destfile)
221 os.unlink(self.filename)
222 self.filename = os.path.abspath(destfile)
223 return destfile
224
226 - def __init__(self, disk, begin, end, type, mntpnt):
227 self.disk = disk
228 self.begin = begin
229 self.end = end
230 self.type = type
231 self.mntpnt = mntpnt
232 self.mapdev = None
233
235 """
236 @rtype: string
237 @return: the filesystem type of the partition suitable for passing to parted
238 """
239 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext2', TYPE_XFS: 'ext2', TYPE_SWAP: 'linux-swap(new)' }[self.type]
240
242 """Adds partition to the disk image (does not mkfs or anything like that)"""
243 logging.info('Adding type %d partition to disk image: %s' % (self.type, disk.filename))
244 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
245
247 """Adds Filesystem object"""
248 if not self.mapdev:
249 raise Exception('We can\'t mkfs before we have a mapper device')
250 self.fs = Filesystem(self.disk.vm, preallocated=True, filename=self.mapdev, type=self.type, mntpnt=self.mntpnt)
251 self.fs.mkfs()
252
254 """The name of the partition as known by grub"""
255 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
256
258 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.
259 This allows other parts of VMBuilder to set the prefix to something suitable."""
260 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
261
263 """Index of the disk (starting from 0)"""
264 return self.disk.partitions.index(self)
265
267 - def __init__(self, vm, size=0, preallocated=False, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
268 self.vm = vm
269 self.filename = filename
270 self.size = parse_size(size)
271 self.preallocated = preallocated
272 self.devletter = devletter
273 self.device = device
274 self.dummy = dummy
275
276 try:
277 if int(type) == type:
278 self.type = type
279 else:
280 self.type = str_to_type(type)
281 except ValueError, e:
282 self.type = str_to_type(type)
283
284 self.mntpnt = mntpnt
285
287 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy)))
288 if not self.preallocated:
289 logging.info('Not preallocated, so we create it.')
290 if not self.filename:
291 if self.mntpnt:
292 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower()
293 self.filename = re.sub('[\w/]', '_', self.filename)
294 if self.filename == '_':
295 self.filename = 'root'
296 elif self.type == TYPE_SWAP:
297 self.filename = 'swap'
298 else:
299 raise VMBuilderException('mntpnt not set')
300
301 self.filename = '%s/%s' % (self.vm.workdir, self.filename)
302 while os.path.exists('%s.img' % self.filename):
303 self.filename += '_'
304 self.filename += '.img'
305 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename)
306 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
307 self.mkfs()
308
310 if not self.dummy:
311 cmd = self.mkfs_fstype() + [self.filename]
312 run_cmd(*cmd)
313 if os.path.exists("/sbin/vol_id"):
314 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip()
315 elif os.path.exists("/sbin/blkid"):
316 self.uuid = run_cmd('blkid', '-sUUID', '-ovalue', self.filename).rstrip()
317
319 if self.vm.suite in ['dapper', 'edgy', 'feisty', 'gutsy']:
320 logging.debug('%s: 128 bit inode' % self.vm.suite)
321 return { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-I 128', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] }[self.type]
322 else:
323 logging.debug('%s: 256 bit inode' % self.vm.suite)
324 return { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] }[self.type]
325
328
331
333 if (self.type != TYPE_SWAP) and not self.dummy:
334 logging.debug('Mounting %s', self.mntpnt)
335 self.mntpath = '%s%s' % (self.vm.rootmnt, self.mntpnt)
336 if not os.path.exists(self.mntpath):
337 os.makedirs(self.mntpath)
338 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath)
339 self.vm.add_clean_cb(self.umount)
340
346
348 """Returns 'a4' for a device that would be called /dev/sda4 in the guest..
349 This allows other parts of VMBuilder to set the prefix to something suitable."""
350 if self.device:
351 return self.device
352 else:
353 return '%s%d' % (self.devletters(), self.get_index() + 1)
354
356 """
357 @rtype: string
358 @return: the series of letters that ought to correspond to the device inside
359 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz'
360 """
361 return self.devletter
362
364 """Index of the disk (starting from 0)"""
365 return self.vm.filesystems.index(self)
366
368 """Takes a size like qemu-img would accept it and returns the size in MB"""
369 try:
370 return int(size_str)
371 except ValueError, e:
372 pass
373
374 try:
375 num = int(size_str[:-1])
376 except ValueError, e:
377 raise VMBuilderUserError("Invalid size: %s" % size_str)
378
379 if size_str[-1:] == 'g' or size_str[-1:] == 'G':
380 return num * 1024
381 if size_str[-1:] == 'm' or size_str[-1:] == 'M':
382 return num
383 if size_str[-1:] == 'k' or size_str[-1:] == 'K':
384 return num / 1024
385
386 str_to_type_map = { 'ext2': TYPE_EXT2,
387 'ext3': TYPE_EXT3,
388 'xfs': TYPE_XFS,
389 'swap': TYPE_SWAP,
390 'linux-swap': TYPE_SWAP }
391
393 try:
394 return str_to_type_map[type]
395 except KeyError, e:
396 raise Exception('Unknown partition type: %s' % type)
397
401
409
411 for filesystem in vm.filesystems:
412 filesystem.create()
413
417
419 """Returns filesystems (self hosted as well as contained in partitions
420 in an order suitable for mounting them"""
421 fss = list(vm.filesystems)
422 for disk in vm.disks:
423 fss += [part.fs for part in disk.partitions]
424 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
425 return fss
426
428 """Returns partitions from disks in an order suitable for mounting them"""
429 parts = []
430 for disk in disks:
431 parts += disk.partitions
432 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
433 return parts
434
437
439 if not devname:
440 return 0
441 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
442
444 if index < 0:
445 return suffix
446 return suffix + index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26])
447
449 exes = ['kvm-img', 'qemu-img']
450 for dir in os.environ['PATH'].split(os.path.pathsep):
451 for exe in exes:
452 path = '%s%s%s' % (dir, os.path.sep, exe)
453 if os.access(path, os.X_OK):
454 return path
455
457 exe = 'VBoxManage'
458 for dir in os.environ['PATH'].split(os.path.pathsep):
459 path = '%s%s%s' % (dir, os.path.sep, exe)
460 if os.access(path, os.X_OK):
461 return path
462