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
80 self.format_type = None
81 "The format type of the disks. Only used for converted disks."
82
84 """
85 @rtype: string
86 @return: the series of letters that ought to correspond to the device inside
87 the VM. E.g. the first disk of a VM would return 'a', while the 702nd would return 'zz'
88 """
89
90 return index_to_devname(self.vm.disks.index(self))
91
93 """
94 Creates the disk image (if it doesn't already exist).
95
96 Once this method returns succesfully, L{filename} can be
97 expected to points to point to whatever holds the virtual disk
98 (be it a file, partition, logical volume, etc.).
99 """
100 if not os.path.exists(self.filename):
101 logging.info('Creating disk image: "%s" of size: %dMB' % (self.filename, self.size))
102 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
103
105 """
106 Partitions the disk image. First adds a partition table and then
107 adds the individual partitions.
108
109 Should only be called once and only after you've added all partitions.
110 """
111
112 logging.info('Adding partition table to disk image: %s' % self.filename)
113 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos')
114
115
116 for part in self.partitions:
117 part.create(self)
118
120 """
121 Create loop devices corresponding to the partitions.
122
123 Once this has returned succesfully, each partition's map device
124 is set as its L{filename<Disk.Partition.filename>} attribute.
125
126 Call this after L{partition}.
127 """
128 logging.info('Creating loop devices corresponding to the created partitions')
129 self.vm.add_clean_cb(lambda : self.unmap(ignore_fail=True))
130 kpartx_output = run_cmd('kpartx', '-av', self.filename)
131 parts = []
132 for line in kpartx_output.split('\n'):
133 if line == "" or line.startswith("gpt:") or line.startswith("dos:"):
134 continue
135 if line.startswith("add"):
136 parts.append(line)
137 continue
138 logging.error('Skipping unknown line in kpartx output (%s)' % line)
139 mapdevs = []
140 for line in parts:
141 mapdevs.append(line.split(' ')[2])
142 for (part, mapdev) in zip(self.partitions, mapdevs):
143 part.set_filename('/dev/mapper/%s' % mapdev)
144
146 """
147 Creates the partitions' filesystems
148 """
149 logging.info("Creating file systems")
150 for part in self.partitions:
151 part.mkfs()
152
154 """
155 @rtype: string
156 @return: name of the disk as known by grub
157 """
158 return '(hd%d)' % self.get_index()
159
161 """
162 @rtype: number
163 @return: index of the disk (starting from 0 for the hypervisor's first disk)
164 """
165 return self.vm.disks.index(self)
166
167 - def unmap(self, ignore_fail=False):
168 """
169 Destroy all mapping devices
170
171 Unsets L{Partition}s' and L{Filesystem}s' filename attribute
172 """
173
174 time.sleep(3)
175
176 tries = 0
177 max_tries = 3
178 while tries < max_tries:
179 try:
180 run_cmd('kpartx', '-d', self.filename, ignore_fail=False)
181 break
182 except:
183 pass
184 tries += 1
185 time.sleep(3)
186
187 if tries >= max_tries:
188
189 logging.info("Could not unmount '%s' after '%d' attempts. Final attempt" % (self.filename, tries))
190 run_cmd('kpartx', '-d', self.filename, ignore_fail=ignore_fail)
191
192 for part in self.partitions:
193 logging.debug("Removing partition %s" % part.filename)
194 parted_oldmap=part.filename[len("/dev/mapper/"):-1]+"p"+part.filename[-1]
195 dmsetup_output = run_cmd('dmsetup', 'info', parted_oldmap, ignore_fail=True)
196 for line in dmsetup_output.split('\n'):
197 if line.startswith("State:") and line.endswith("ACTIVE"):
198 logging.debug("Removing parted old map with 'dmsetup remove %s'" % parted_oldmap)
199 dmsetup_output=run_cmd('dmsetup', 'remove', parted_oldmap, ignore_fail=ignore_fail)
200 part.set_filename(None)
201
202 - def add_part(self, begin, length, type, mntpnt):
203 """
204 Add a partition to the disk
205
206 @type begin: number
207 @param begin: Start offset of the new partition (in megabytes)
208 @type length:
209 @param length: Size of the new partition (in megabytes)
210 @type type: string
211 @param type: Type of the new partition. Valid options are: ext2 ext3 xfs swap linux-swap
212 @type mntpnt: string
213 @param mntpnt: Intended mountpoint inside the guest of the new partition
214 """
215 length = parse_size(length)
216 end = begin+length-1
217 logging.debug("add_part - begin %d, length %d, end %d, type %s, mntpnt %s" % (begin, length, end, type, mntpnt))
218 for part in self.partitions:
219 if (begin >= part.begin and begin <= part.end) or \
220 (end >= part.begin and end <= part.end):
221 raise VMBuilderUserError('Partitions are overlapping')
222 if begin < 0 or end > self.size:
223 raise VMBuilderUserError('Partition is out of bounds. start=%d, end=%d, disksize=%d' % (begin,end,self.size))
224 part = self.Partition(disk=self, begin=begin, end=end, type=str_to_type(type), mntpnt=mntpnt)
225 self.partitions.append(part)
226
227
228 self.partitions.sort(cmp=lambda x,y: x.begin - y.begin)
229
230 - def convert(self, destdir, format):
231 """
232 Convert the disk image
233
234 @type destdir: string
235 @param destdir: Target location of converted disk image
236 @type format: string
237 @param format: The target format (as understood by qemu-img or vdi)
238 @rtype: string
239 @return: the name of the converted image
240 """
241 if self.preallocated:
242
243 return self.filename
244
245 filename = os.path.basename(self.filename)
246 if '.' in filename:
247 filename = filename[:filename.rindex('.')]
248 destfile = '%s/%s.%s' % (destdir, filename, format)
249
250 logging.info('Converting %s to %s, format %s' % (self.filename, format, destfile))
251 if format == 'vdi':
252 run_cmd(vbox_manager_path(), 'convertfromraw', '-format', 'VDI', self.filename, destfile)
253 else:
254 run_cmd(qemu_img_path(), 'convert', '-O', format, self.filename, destfile)
255 os.unlink(self.filename)
256 self.filename = os.path.abspath(destfile)
257 self.format_type = format
258 return destfile
259
261 - def __init__(self, disk, begin, end, type, mntpnt):
262 self.disk = disk
263 "The disk on which this Partition resides."
264
265 self.begin = begin
266 "The start of the partition"
267
268 self.end = end
269 "The end of the partition"
270
271 self.type = type
272 "The partition type"
273
274 self.mntpnt = mntpnt
275 "The destined mount point"
276
277 self.filename = None
278 "The filename of this partition (the map device)"
279
280 self.fs = Filesystem(vm=self.disk.vm, type=self.type, mntpnt=self.mntpnt)
281 "The enclosed filesystem"
282
284 self.filename = filename
285 self.fs.filename = filename
286
288 """
289 @rtype: string
290 @return: the filesystem type of the partition suitable for passing to parted
291 """
292 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext2', TYPE_EXT4: 'ext2', TYPE_XFS: 'ext2', TYPE_SWAP: 'linux-swap(new)' }[self.type]
293
295 """Adds partition to the disk image (does not mkfs or anything like that)"""
296 logging.info('Adding type %d partition to disk image: %s' % (self.type, disk.filename))
297 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
298
300 """Adds Filesystem object"""
301 self.fs.mkfs()
302
304 """The name of the partition as known by grub"""
305 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
306
308 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.
309 This allows other parts of VMBuilder to set the prefix to something suitable."""
310 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
311
313 """Index of the disk (starting from 0)"""
314 return self.disk.partitions.index(self)
315
317 try:
318 if int(type) == type:
319 self.type = type
320 else:
321 self.type = str_to_type(type)
322 except ValueError:
323 self.type = str_to_type(type)
324
326 - def __init__(self, vm=None, size=0, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
327 self.vm = vm
328 self.filename = filename
329 self.size = parse_size(size)
330 self.devletter = devletter
331 self.device = device
332 self.dummy = dummy
333
334 self.set_type(type)
335
336 self.mntpnt = mntpnt
337
338 self.preallocated = False
339 "Whether the file existed already (True if it did, False if we had to create it)."
340
342 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy)))
343 if not os.path.exists(self.filename):
344 logging.info('Not preallocated, so we create it.')
345 if not self.filename:
346 if self.mntpnt:
347 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower()
348 self.filename = re.sub('[\w/]', '_', self.filename)
349 if self.filename == '_':
350 self.filename = 'root'
351 elif self.type == TYPE_SWAP:
352 self.filename = 'swap'
353 else:
354 raise VMBuilderException('mntpnt not set')
355
356 self.filename = '%s/%s' % (self.vm.workdir, self.filename)
357 while os.path.exists('%s.img' % self.filename):
358 self.filename += '_'
359 self.filename += '.img'
360 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename)
361 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
362 self.mkfs()
363
365 if not self.filename:
366 raise VMBuilderException('We can\'t mkfs if filename is not set. Did you forget to call .create()?')
367 if not self.dummy:
368 cmd = self.mkfs_fstype() + [self.filename]
369 run_cmd(*cmd)
370
371 run_cmd('udevadm', 'settle')
372 if os.path.exists("/sbin/vol_id"):
373 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip()
374 elif os.path.exists("/sbin/blkid"):
375 self.uuid = run_cmd('blkid', '-c', '/dev/null', '-sUUID', '-ovalue', self.filename).rstrip()
376
384
387
390
391 - def mount(self, rootmnt):
392 if (self.type != TYPE_SWAP) and not self.dummy:
393 logging.debug('Mounting %s', self.mntpnt)
394 self.mntpath = '%s%s' % (rootmnt, self.mntpnt)
395 if not os.path.exists(self.mntpath):
396 os.makedirs(self.mntpath)
397 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath)
398 self.vm.add_clean_cb(self.umount)
399
405
407 """Returns 'a4' for a device that would be called /dev/sda4 in the guest..
408 This allows other parts of VMBuilder to set the prefix to something suitable."""
409 if self.device:
410 return self.device
411 else:
412 return '%s%d' % (self.devletters(), self.get_index() + 1)
413
415 """
416 @rtype: string
417 @return: the series of letters that ought to correspond to the device inside
418 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz'
419 """
420 return self.devletter
421
423 """Index of the disk (starting from 0)"""
424 return self.vm.filesystems.index(self)
425
427 try:
428 if int(type) == type:
429 self.type = type
430 else:
431 self.type = str_to_type(type)
432 except ValueError:
433 self.type = str_to_type(type)
434
436 """Takes a size like qemu-img would accept it and returns the size in MB"""
437 try:
438 return int(size_str)
439 except ValueError:
440 pass
441
442 try:
443 num = int(size_str[:-1])
444 except ValueError:
445 raise VMBuilderUserError("Invalid size: %s" % size_str)
446
447 if size_str[-1:] == 'g' or size_str[-1:] == 'G':
448 return num * 1024
449 if size_str[-1:] == 'm' or size_str[-1:] == 'M':
450 return num
451 if size_str[-1:] == 'k' or size_str[-1:] == 'K':
452 return num / 1024
453
454 str_to_type_map = { 'ext2': TYPE_EXT2,
455 'ext3': TYPE_EXT3,
456 'ext4': TYPE_EXT4,
457 'xfs': TYPE_XFS,
458 'swap': TYPE_SWAP,
459 'linux-swap': TYPE_SWAP }
460
462 try:
463 return str_to_type_map[type]
464 except KeyError:
465 raise Exception('Unknown partition type: %s' % type)
466
468 """Returns the partition which contains the root dir"""
469 return path_to_partition(disks, '/')
470
472 """Returns the partition which contains /boot"""
473 return path_to_partition(disks, '/boot/foo')
474
476 parts = get_ordered_partitions(disks)
477 parts.reverse()
478 for part in parts:
479 if path.startswith(part.mntpnt):
480 return part
481 raise VMBuilderException("Couldn't find partition path %s belongs to" % path)
482
484 for filesystem in vm.filesystems:
485 filesystem.create()
486
490
492 """Returns filesystems (self hosted as well as contained in partitions
493 in an order suitable for mounting them"""
494 fss = list(vm.filesystems)
495 for disk in vm.disks:
496 fss += [part.fs for part in disk.partitions]
497 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
498 return fss
499
501 """Returns partitions from disks in an order suitable for mounting them"""
502 parts = []
503 for disk in disks:
504 parts += disk.partitions
505 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
506 return parts
507
510
512 if not devname:
513 return 0
514 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
515
517 if index < 0:
518 return suffix
519 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
520
522 st = os.stat(filename)
523 if stat.S_ISREG(st.st_mode):
524 return st.st_size / 1024*1024
525 elif stat.S_ISBLK(st.st_mode):
526
527 BLKGETSIZE64 = 2148012658
528 fp = open(filename, 'r')
529 fd = fp.fileno()
530 s = fcntl.ioctl(fd, BLKGETSIZE64, ' '*8)
531 return unpack('L', s)[0] / 1024*1024
532
533 raise VMBuilderException('No idea how to find the size of %s' % filename)
534
536 exes = ['kvm-img', 'qemu-img']
537 for dir in os.environ['PATH'].split(os.path.pathsep):
538 for exe in exes:
539 path = '%s%s%s' % (dir, os.path.sep, exe)
540 if os.access(path, os.X_OK):
541 return path
542
544 exe = 'VBoxManage'
545 for dir in os.environ['PATH'].split(os.path.pathsep):
546 path = '%s%s%s' % (dir, os.path.sep, exe)
547 if os.access(path, os.X_OK):
548 return path
549