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', '-asv', 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 unmap '%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 if self.begin == 0:
298 logging.info('Partition at beginning of disk - reserving first cylinder')
299 partition_start = "63s"
300 else:
301 partition_start = self.begin
302 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), partition_start, self.end)
303
305 """Adds Filesystem object"""
306 self.fs.mkfs()
307
309 """The name of the partition as known by grub"""
310 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
311
313 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.
314 This allows other parts of VMBuilder to set the prefix to something suitable."""
315 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
316
318 """Index of the disk (starting from 0)"""
319 return self.disk.partitions.index(self)
320
322 try:
323 if int(type) == type:
324 self.type = type
325 else:
326 self.type = str_to_type(type)
327 except ValueError:
328 self.type = str_to_type(type)
329
331 - def __init__(self, vm=None, size=0, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
332 self.vm = vm
333 self.filename = filename
334 self.size = parse_size(size)
335 self.devletter = devletter
336 self.device = device
337 self.dummy = dummy
338
339 self.set_type(type)
340
341 self.mntpnt = mntpnt
342
343 self.preallocated = False
344 "Whether the file existed already (True if it did, False if we had to create it)."
345
347 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy)))
348 if not os.path.exists(self.filename):
349 logging.info('Not preallocated, so we create it.')
350 if not self.filename:
351 if self.mntpnt:
352 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower()
353 self.filename = re.sub('[\w/]', '_', self.filename)
354 if self.filename == '_':
355 self.filename = 'root'
356 elif self.type == TYPE_SWAP:
357 self.filename = 'swap'
358 else:
359 raise VMBuilderException('mntpnt not set')
360
361 self.filename = '%s/%s' % (self.vm.workdir, self.filename)
362 while os.path.exists('%s.img' % self.filename):
363 self.filename += '_'
364 self.filename += '.img'
365 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename)
366 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
367 self.mkfs()
368
370 if not self.filename:
371 raise VMBuilderException('We can\'t mkfs if filename is not set. Did you forget to call .create()?')
372 if not self.dummy:
373 cmd = self.mkfs_fstype() + [self.filename]
374 run_cmd(*cmd)
375
376 run_cmd('udevadm', 'settle')
377 if os.path.exists("/sbin/vol_id"):
378 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip()
379 elif os.path.exists("/sbin/blkid"):
380 self.uuid = run_cmd('blkid', '-c', '/dev/null', '-sUUID', '-ovalue', self.filename).rstrip()
381
389
392
395
396 - def mount(self, rootmnt):
397 if (self.type != TYPE_SWAP) and not self.dummy:
398 logging.debug('Mounting %s', self.mntpnt)
399 self.mntpath = '%s%s' % (rootmnt, self.mntpnt)
400 if not os.path.exists(self.mntpath):
401 os.makedirs(self.mntpath)
402 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath)
403 self.vm.add_clean_cb(self.umount)
404
410
412 """Returns 'a4' for a device that would be called /dev/sda4 in the guest..
413 This allows other parts of VMBuilder to set the prefix to something suitable."""
414 if self.device:
415 return self.device
416 else:
417 return '%s%d' % (self.devletters(), self.get_index() + 1)
418
420 """
421 @rtype: string
422 @return: the series of letters that ought to correspond to the device inside
423 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz'
424 """
425 return self.devletter
426
428 """Index of the disk (starting from 0)"""
429 return self.vm.filesystems.index(self)
430
432 try:
433 if int(type) == type:
434 self.type = type
435 else:
436 self.type = str_to_type(type)
437 except ValueError:
438 self.type = str_to_type(type)
439
441 """Takes a size like qemu-img would accept it and returns the size in MB"""
442 try:
443 return int(size_str)
444 except ValueError:
445 pass
446
447 try:
448 num = int(size_str[:-1])
449 except ValueError:
450 raise VMBuilderUserError("Invalid size: %s" % size_str)
451
452 if size_str[-1:] == 'g' or size_str[-1:] == 'G':
453 return num * 1024
454 if size_str[-1:] == 'm' or size_str[-1:] == 'M':
455 return num
456 if size_str[-1:] == 'k' or size_str[-1:] == 'K':
457 return num / 1024
458
459 str_to_type_map = { 'ext2': TYPE_EXT2,
460 'ext3': TYPE_EXT3,
461 'ext4': TYPE_EXT4,
462 'xfs': TYPE_XFS,
463 'swap': TYPE_SWAP,
464 'linux-swap': TYPE_SWAP }
465
467 try:
468 return str_to_type_map[type]
469 except KeyError:
470 raise Exception('Unknown partition type: %s' % type)
471
473 """Returns the partition which contains the root dir"""
474 return path_to_partition(disks, '/')
475
477 """Returns the partition which contains /boot"""
478 return path_to_partition(disks, '/boot/foo')
479
481 parts = get_ordered_partitions(disks)
482 parts.reverse()
483 for part in parts:
484 if path.startswith(part.mntpnt):
485 return part
486 raise VMBuilderException("Couldn't find partition path %s belongs to" % path)
487
489 for filesystem in vm.filesystems:
490 filesystem.create()
491
495
497 """Returns filesystems (self hosted as well as contained in partitions
498 in an order suitable for mounting them"""
499 fss = list(vm.filesystems)
500 for disk in vm.disks:
501 fss += [part.fs for part in disk.partitions]
502 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
503 return fss
504
506 """Returns partitions from disks in an order suitable for mounting them"""
507 parts = []
508 for disk in disks:
509 parts += disk.partitions
510 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
511 return parts
512
515
517 if not devname:
518 return 0
519 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
520
522 if index < 0:
523 return suffix
524 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
525
527 st = os.stat(filename)
528 if stat.S_ISREG(st.st_mode):
529 return st.st_size / 1024*1024
530 elif stat.S_ISBLK(st.st_mode):
531
532 BLKGETSIZE64 = 2148012658
533 fp = open(filename, 'r')
534 fd = fp.fileno()
535 s = fcntl.ioctl(fd, BLKGETSIZE64, ' '*8)
536 return unpack('L', s)[0] / 1024*1024
537
538 raise VMBuilderException('No idea how to find the size of %s' % filename)
539
541 exes = ['kvm-img', 'qemu-img']
542 for dir in os.environ['PATH'].split(os.path.pathsep):
543 for exe in exes:
544 path = '%s%s%s' % (dir, os.path.sep, exe)
545 if os.access(path, os.X_OK):
546 return path
547
549 exe = 'VBoxManage'
550 for dir in os.environ['PATH'].split(os.path.pathsep):
551 path = '%s%s%s' % (dir, os.path.sep, exe)
552 if os.access(path, os.X_OK):
553 return path
554