1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import fcntl
22 import logging
23 import os
24 import os.path
25 import re
26 import stat
27 import string
28 import time
29 from VMBuilder.util import run_cmd
30 from VMBuilder.exception import VMBuilderUserError, VMBuilderException
31 from struct import unpack
32
33 TYPE_EXT2 = 0
34 TYPE_EXT3 = 1
35 TYPE_XFS = 2
36 TYPE_SWAP = 3
37 TYPE_EXT4 = 4
38
40 """
41 Virtual disk.
42
43 @type vm: Hypervisor
44 @param vm: The Hypervisor to which the disk belongs
45 @type filename: string
46 @param filename: filename of the disk image
47 @type size: string or number
48 @param size: The size of the disk image to create (passed to
49 L{parse_size}). If specified and filename already exists,
50 L{VMBuilderUserError} will be raised. Otherwise, a disk image of
51 this size will be created once L{create}() is called.
52 """
53
54 - def __init__(self, vm, filename, size=None):
55 self.vm = vm
56 "The hypervisor to which the disk belongs."
57
58 self.filename = filename
59 "The filename of the disk image."
60
61 self.partitions = []
62 "The list of partitions on the disk. Is kept in order by L{add_part}."
63
64 self.preallocated = False
65 "Whether the file existed already (True if it did, False if we had to create it)."
66
67 self.size = 0
68 "The size of the disk. For preallocated disks, this is detected."
69
70 if not os.path.exists(self.filename):
71 if not size:
72 raise VMBuilderUserError('%s does not exist, but no size was given.' % (self.filename))
73 self.size = parse_size(size)
74 else:
75 if size:
76 raise VMBuilderUserError('%s exists, but size was given.' % (self.filename))
77 self.preallocated = True
78 self.size = detect_size(self.filename)
79
81 """
82 @rtype: string
83 @return: the series of letters that ought to correspond to the device inside
84 the VM. E.g. the first disk of a VM would return 'a', while the 702nd would return 'zz'
85 """
86
87 return index_to_devname(self.vm.disks.index(self))
88
90 """
91 Creates the disk image (if it doesn't already exist).
92
93 Once this method returns succesfully, L{filename} can be
94 expected to points to point to whatever holds the virtual disk
95 (be it a file, partition, logical volume, etc.).
96 """
97 if not os.path.exists(self.filename):
98 logging.info('Creating disk image: "%s" of size: %dMB' % (self.filename, self.size))
99 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
100
102 """
103 Partitions the disk image. First adds a partition table and then
104 adds the individual partitions.
105
106 Should only be called once and only after you've added all partitions.
107 """
108
109 logging.info('Adding partition table to disk image: %s' % self.filename)
110 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos')
111
112
113 for part in self.partitions:
114 part.create(self)
115
117 """
118 Create loop devices corresponding to the partitions.
119
120 Once this has returned succesfully, each partition's map device
121 is set as its L{filename<Disk.Partition.filename>} attribute.
122
123 Call this after L{partition}.
124 """
125 logging.info('Creating loop devices corresponding to the created partitions')
126 self.vm.add_clean_cb(lambda : self.unmap(ignore_fail=True))
127 kpartx_output = run_cmd('kpartx', '-av', self.filename)
128 parts = []
129 for line in kpartx_output.split('\n'):
130 if line == "" or line.startswith("gpt:") or line.startswith("dos:"):
131 continue
132 if line.startswith("add"):
133 parts.append(line)
134 continue
135 logging.error('Skipping unknown line in kpartx output (%s)' % line)
136 mapdevs = []
137 for line in parts:
138 mapdevs.append(line.split(' ')[2])
139 for (part, mapdev) in zip(self.partitions, mapdevs):
140 part.set_filename('/dev/mapper/%s' % mapdev)
141
143 """
144 Creates the partitions' filesystems
145 """
146 logging.info("Creating file systems")
147 for part in self.partitions:
148 part.mkfs()
149
151 """
152 @rtype: string
153 @return: name of the disk as known by grub
154 """
155 return '(hd%d)' % self.get_index()
156
158 """
159 @rtype: number
160 @return: index of the disk (starting from 0 for the hypervisor's first disk)
161 """
162 return self.vm.disks.index(self)
163
164 - def unmap(self, ignore_fail=False):
165 """
166 Destroy all mapping devices
167
168 Unsets L{Partition}s' and L{Filesystem}s' filename attribute
169 """
170
171 time.sleep(3)
172
173 tries = 0
174 max_tries = 3
175 while tries < max_tries:
176 try:
177 run_cmd('kpartx', '-d', self.filename, ignore_fail=False)
178 break
179 except:
180 pass
181 tries += 1
182 time.sleep(3)
183
184 if tries >= max_tries:
185
186 logging.info("Could not unmount '%s' after '%d' attempts. Final attempt" % (self.filename, tries))
187 run_cmd('kpartx', '-d', self.filename, ignore_fail=ignore_fail)
188
189 for part in self.partitions:
190 part.set_filename(None)
191
192 - def add_part(self, begin, length, type, mntpnt):
193 """
194 Add a partition to the disk
195
196 @type begin: number
197 @param begin: Start offset of the new partition (in megabytes)
198 @type length:
199 @param length: Size of the new partition (in megabytes)
200 @type type: string
201 @param type: Type of the new partition. Valid options are: ext2 ext3 xfs swap linux-swap
202 @type mntpnt: string
203 @param mntpnt: Intended mountpoint inside the guest of the new partition
204 """
205 length = parse_size(length)
206 end = begin+length-1
207 logging.debug("add_part - begin %d, length %d, end %d, type %s, mntpnt %s" % (begin, length, end, type, mntpnt))
208 for part in self.partitions:
209 if (begin >= part.begin and begin <= part.end) or \
210 (end >= part.begin and end <= part.end):
211 raise VMBuilderUserError('Partitions are overlapping')
212 if begin < 0 or end > self.size:
213 raise VMBuilderUserError('Partition is out of bounds. start=%d, end=%d, disksize=%d' % (begin,end,self.size))
214 part = self.Partition(disk=self, begin=begin, end=end, type=str_to_type(type), mntpnt=mntpnt)
215 self.partitions.append(part)
216
217
218 self.partitions.sort(cmp=lambda x,y: x.begin - y.begin)
219
220 - def convert(self, destdir, format):
221 """
222 Convert the disk image
223
224 @type destdir: string
225 @param destdir: Target location of converted disk image
226 @type format: string
227 @param format: The target format (as understood by qemu-img or vdi)
228 @rtype: string
229 @return: the name of the converted image
230 """
231 if self.preallocated:
232
233 return self.filename
234
235 filename = os.path.basename(self.filename)
236 if '.' in filename:
237 filename = filename[:filename.rindex('.')]
238 destfile = '%s/%s.%s' % (destdir, filename, format)
239
240 logging.info('Converting %s to %s, format %s' % (self.filename, format, destfile))
241 if format == 'vdi':
242 run_cmd(vbox_manager_path(), 'convertfromraw', '-format', 'VDI', self.filename, destfile)
243 else:
244 run_cmd(qemu_img_path(), 'convert', '-O', format, self.filename, destfile)
245 os.unlink(self.filename)
246 self.filename = os.path.abspath(destfile)
247 return destfile
248
250 - def __init__(self, disk, begin, end, type, mntpnt):
251 self.disk = disk
252 "The disk on which this Partition resides."
253
254 self.begin = begin
255 "The start of the partition"
256
257 self.end = end
258 "The end of the partition"
259
260 self.type = type
261 "The partition type"
262
263 self.mntpnt = mntpnt
264 "The destined mount point"
265
266 self.filename = None
267 "The filename of this partition (the map device)"
268
269 self.fs = Filesystem(vm=self.disk.vm, type=self.type, mntpnt=self.mntpnt)
270 "The enclosed filesystem"
271
273 self.filename = filename
274 self.fs.filename = filename
275
277 """
278 @rtype: string
279 @return: the filesystem type of the partition suitable for passing to parted
280 """
281 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext2', TYPE_EXT4: 'ext2', TYPE_XFS: 'ext2', TYPE_SWAP: 'linux-swap(new)' }[self.type]
282
284 """Adds partition to the disk image (does not mkfs or anything like that)"""
285 logging.info('Adding type %d partition to disk image: %s' % (self.type, disk.filename))
286 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
287
289 """Adds Filesystem object"""
290 self.fs.mkfs()
291
293 """The name of the partition as known by grub"""
294 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
295
297 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.
298 This allows other parts of VMBuilder to set the prefix to something suitable."""
299 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
300
302 """Index of the disk (starting from 0)"""
303 return self.disk.partitions.index(self)
304
306 try:
307 if int(type) == type:
308 self.type = type
309 else:
310 self.type = str_to_type(type)
311 except ValueError:
312 self.type = str_to_type(type)
313
315 - def __init__(self, vm=None, size=0, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
316 self.vm = vm
317 self.filename = filename
318 self.size = parse_size(size)
319 self.devletter = devletter
320 self.device = device
321 self.dummy = dummy
322
323 self.set_type(type)
324
325 self.mntpnt = mntpnt
326
327 self.preallocated = False
328 "Whether the file existed already (True if it did, False if we had to create it)."
329
331 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy)))
332 if not os.path.exists(self.filename):
333 logging.info('Not preallocated, so we create it.')
334 if not self.filename:
335 if self.mntpnt:
336 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower()
337 self.filename = re.sub('[\w/]', '_', self.filename)
338 if self.filename == '_':
339 self.filename = 'root'
340 elif self.type == TYPE_SWAP:
341 self.filename = 'swap'
342 else:
343 raise VMBuilderException('mntpnt not set')
344
345 self.filename = '%s/%s' % (self.vm.workdir, self.filename)
346 while os.path.exists('%s.img' % self.filename):
347 self.filename += '_'
348 self.filename += '.img'
349 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename)
350 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
351 self.mkfs()
352
354 if not self.filename:
355 raise VMBuilderException('We can\'t mkfs if filename is not set. Did you forget to call .create()?')
356 if not self.dummy:
357 cmd = self.mkfs_fstype() + [self.filename]
358 run_cmd(*cmd)
359
360 run_cmd('udevadm', 'settle')
361 if os.path.exists("/sbin/vol_id"):
362 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip()
363 elif os.path.exists("/sbin/blkid"):
364 self.uuid = run_cmd('blkid', '-p', '-sUUID', '-ovalue', self.filename).rstrip()
365
373
376
379
380 - def mount(self, rootmnt):
381 if (self.type != TYPE_SWAP) and not self.dummy:
382 logging.debug('Mounting %s', self.mntpnt)
383 self.mntpath = '%s%s' % (rootmnt, self.mntpnt)
384 if not os.path.exists(self.mntpath):
385 os.makedirs(self.mntpath)
386 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath)
387 self.vm.add_clean_cb(self.umount)
388
394
396 """Returns 'a4' for a device that would be called /dev/sda4 in the guest..
397 This allows other parts of VMBuilder to set the prefix to something suitable."""
398 if self.device:
399 return self.device
400 else:
401 return '%s%d' % (self.devletters(), self.get_index() + 1)
402
404 """
405 @rtype: string
406 @return: the series of letters that ought to correspond to the device inside
407 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz'
408 """
409 return self.devletter
410
412 """Index of the disk (starting from 0)"""
413 return self.vm.filesystems.index(self)
414
416 try:
417 if int(type) == type:
418 self.type = type
419 else:
420 self.type = str_to_type(type)
421 except ValueError:
422 self.type = str_to_type(type)
423
425 """Takes a size like qemu-img would accept it and returns the size in MB"""
426 try:
427 return int(size_str)
428 except ValueError:
429 pass
430
431 try:
432 num = int(size_str[:-1])
433 except ValueError:
434 raise VMBuilderUserError("Invalid size: %s" % size_str)
435
436 if size_str[-1:] == 'g' or size_str[-1:] == 'G':
437 return num * 1024
438 if size_str[-1:] == 'm' or size_str[-1:] == 'M':
439 return num
440 if size_str[-1:] == 'k' or size_str[-1:] == 'K':
441 return num / 1024
442
443 str_to_type_map = { 'ext2': TYPE_EXT2,
444 'ext3': TYPE_EXT3,
445 'ext4': TYPE_EXT4,
446 'xfs': TYPE_XFS,
447 'swap': TYPE_SWAP,
448 'linux-swap': TYPE_SWAP }
449
451 try:
452 return str_to_type_map[type]
453 except KeyError:
454 raise Exception('Unknown partition type: %s' % type)
455
457 """Returns the partition which contains the root dir"""
458 return path_to_partition(disks, '/')
459
461 """Returns the partition which contains /boot"""
462 return path_to_partition(disks, '/boot/foo')
463
465 parts = get_ordered_partitions(disks)
466 parts.reverse()
467 for part in parts:
468 if path.startswith(part.mntpnt):
469 return part
470 raise VMBuilderException("Couldn't find partition path %s belongs to" % path)
471
473 for filesystem in vm.filesystems:
474 filesystem.create()
475
479
481 """Returns filesystems (self hosted as well as contained in partitions
482 in an order suitable for mounting them"""
483 fss = list(vm.filesystems)
484 for disk in vm.disks:
485 fss += [part.fs for part in disk.partitions]
486 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
487 return fss
488
490 """Returns partitions from disks in an order suitable for mounting them"""
491 parts = []
492 for disk in disks:
493 parts += disk.partitions
494 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
495 return parts
496
499
501 if not devname:
502 return 0
503 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
504
506 if index < 0:
507 return suffix
508 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
509
511 st = os.stat(filename)
512 if stat.S_ISREG(st.st_mode):
513 return st.st_size / 1024*1024
514 elif stat.S_ISBLK(st.st_mode):
515
516 BLKGETSIZE64 = 2148012658
517 fp = open(filename, 'r')
518 fd = fp.fileno()
519 s = fcntl.ioctl(fd, BLKGETSIZE64, ' '*8)
520 return unpack('L', s)[0] / 1024*1024
521
522 raise VMBuilderException('No idea how to find the size of %s' % filename)
523
525 exes = ['kvm-img', 'qemu-img']
526 for dir in os.environ['PATH'].split(os.path.pathsep):
527 for exe in exes:
528 path = '%s%s%s' % (dir, os.path.sep, exe)
529 if os.access(path, os.X_OK):
530 return path
531
533 exe = 'VBoxManage'
534 for dir in os.environ['PATH'].split(os.path.pathsep):
535 path = '%s%s%s' % (dir, os.path.sep, exe)
536 if os.access(path, os.X_OK):
537 return path
538