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 part.set_filename(None)
194
195 - def add_part(self, begin, length, type, mntpnt):
196 """
197 Add a partition to the disk
198
199 @type begin: number
200 @param begin: Start offset of the new partition (in megabytes)
201 @type length:
202 @param length: Size of the new partition (in megabytes)
203 @type type: string
204 @param type: Type of the new partition. Valid options are: ext2 ext3 xfs swap linux-swap
205 @type mntpnt: string
206 @param mntpnt: Intended mountpoint inside the guest of the new partition
207 """
208 length = parse_size(length)
209 end = begin+length-1
210 logging.debug("add_part - begin %d, length %d, end %d, type %s, mntpnt %s" % (begin, length, end, type, mntpnt))
211 for part in self.partitions:
212 if (begin >= part.begin and begin <= part.end) or \
213 (end >= part.begin and end <= part.end):
214 raise VMBuilderUserError('Partitions are overlapping')
215 if begin < 0 or end > self.size:
216 raise VMBuilderUserError('Partition is out of bounds. start=%d, end=%d, disksize=%d' % (begin,end,self.size))
217 part = self.Partition(disk=self, begin=begin, end=end, type=str_to_type(type), mntpnt=mntpnt)
218 self.partitions.append(part)
219
220
221 self.partitions.sort(cmp=lambda x,y: x.begin - y.begin)
222
223 - def convert(self, destdir, format):
224 """
225 Convert the disk image
226
227 @type destdir: string
228 @param destdir: Target location of converted disk image
229 @type format: string
230 @param format: The target format (as understood by qemu-img or vdi)
231 @rtype: string
232 @return: the name of the converted image
233 """
234 if self.preallocated:
235
236 return self.filename
237
238 filename = os.path.basename(self.filename)
239 if '.' in filename:
240 filename = filename[:filename.rindex('.')]
241 destfile = '%s/%s.%s' % (destdir, filename, format)
242
243 logging.info('Converting %s to %s, format %s' % (self.filename, format, destfile))
244 if format == 'vdi':
245 run_cmd(vbox_manager_path(), 'convertfromraw', '-format', 'VDI', self.filename, destfile)
246 else:
247 run_cmd(qemu_img_path(), 'convert', '-O', format, self.filename, destfile)
248 os.unlink(self.filename)
249 self.filename = os.path.abspath(destfile)
250 self.format_type = format
251 return destfile
252
254 - def __init__(self, disk, begin, end, type, mntpnt):
255 self.disk = disk
256 "The disk on which this Partition resides."
257
258 self.begin = begin
259 "The start of the partition"
260
261 self.end = end
262 "The end of the partition"
263
264 self.type = type
265 "The partition type"
266
267 self.mntpnt = mntpnt
268 "The destined mount point"
269
270 self.filename = None
271 "The filename of this partition (the map device)"
272
273 self.fs = Filesystem(vm=self.disk.vm, type=self.type, mntpnt=self.mntpnt)
274 "The enclosed filesystem"
275
277 self.filename = filename
278 self.fs.filename = filename
279
281 """
282 @rtype: string
283 @return: the filesystem type of the partition suitable for passing to parted
284 """
285 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext2', TYPE_EXT4: 'ext2', TYPE_XFS: 'ext2', TYPE_SWAP: 'linux-swap(new)' }[self.type]
286
288 """Adds partition to the disk image (does not mkfs or anything like that)"""
289 logging.info('Adding type %d partition to disk image: %s' % (self.type, disk.filename))
290 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
291
293 """Adds Filesystem object"""
294 self.fs.mkfs()
295
297 """The name of the partition as known by grub"""
298 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
299
301 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.
302 This allows other parts of VMBuilder to set the prefix to something suitable."""
303 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
304
306 """Index of the disk (starting from 0)"""
307 return self.disk.partitions.index(self)
308
310 try:
311 if int(type) == type:
312 self.type = type
313 else:
314 self.type = str_to_type(type)
315 except ValueError:
316 self.type = str_to_type(type)
317
319 - def __init__(self, vm=None, size=0, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
320 self.vm = vm
321 self.filename = filename
322 self.size = parse_size(size)
323 self.devletter = devletter
324 self.device = device
325 self.dummy = dummy
326
327 self.set_type(type)
328
329 self.mntpnt = mntpnt
330
331 self.preallocated = False
332 "Whether the file existed already (True if it did, False if we had to create it)."
333
335 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy)))
336 if not os.path.exists(self.filename):
337 logging.info('Not preallocated, so we create it.')
338 if not self.filename:
339 if self.mntpnt:
340 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower()
341 self.filename = re.sub('[\w/]', '_', self.filename)
342 if self.filename == '_':
343 self.filename = 'root'
344 elif self.type == TYPE_SWAP:
345 self.filename = 'swap'
346 else:
347 raise VMBuilderException('mntpnt not set')
348
349 self.filename = '%s/%s' % (self.vm.workdir, self.filename)
350 while os.path.exists('%s.img' % self.filename):
351 self.filename += '_'
352 self.filename += '.img'
353 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename)
354 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
355 self.mkfs()
356
358 if not self.filename:
359 raise VMBuilderException('We can\'t mkfs if filename is not set. Did you forget to call .create()?')
360 if not self.dummy:
361 cmd = self.mkfs_fstype() + [self.filename]
362 run_cmd(*cmd)
363
364 run_cmd('udevadm', 'settle')
365 if os.path.exists("/sbin/vol_id"):
366 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip()
367 elif os.path.exists("/sbin/blkid"):
368 self.uuid = run_cmd('blkid', '-c', '/dev/null', '-sUUID', '-ovalue', self.filename).rstrip()
369
377
380
383
384 - def mount(self, rootmnt):
385 if (self.type != TYPE_SWAP) and not self.dummy:
386 logging.debug('Mounting %s', self.mntpnt)
387 self.mntpath = '%s%s' % (rootmnt, self.mntpnt)
388 if not os.path.exists(self.mntpath):
389 os.makedirs(self.mntpath)
390 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath)
391 self.vm.add_clean_cb(self.umount)
392
398
400 """Returns 'a4' for a device that would be called /dev/sda4 in the guest..
401 This allows other parts of VMBuilder to set the prefix to something suitable."""
402 if self.device:
403 return self.device
404 else:
405 return '%s%d' % (self.devletters(), self.get_index() + 1)
406
408 """
409 @rtype: string
410 @return: the series of letters that ought to correspond to the device inside
411 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz'
412 """
413 return self.devletter
414
416 """Index of the disk (starting from 0)"""
417 return self.vm.filesystems.index(self)
418
420 try:
421 if int(type) == type:
422 self.type = type
423 else:
424 self.type = str_to_type(type)
425 except ValueError:
426 self.type = str_to_type(type)
427
429 """Takes a size like qemu-img would accept it and returns the size in MB"""
430 try:
431 return int(size_str)
432 except ValueError:
433 pass
434
435 try:
436 num = int(size_str[:-1])
437 except ValueError:
438 raise VMBuilderUserError("Invalid size: %s" % size_str)
439
440 if size_str[-1:] == 'g' or size_str[-1:] == 'G':
441 return num * 1024
442 if size_str[-1:] == 'm' or size_str[-1:] == 'M':
443 return num
444 if size_str[-1:] == 'k' or size_str[-1:] == 'K':
445 return num / 1024
446
447 str_to_type_map = { 'ext2': TYPE_EXT2,
448 'ext3': TYPE_EXT3,
449 'ext4': TYPE_EXT4,
450 'xfs': TYPE_XFS,
451 'swap': TYPE_SWAP,
452 'linux-swap': TYPE_SWAP }
453
455 try:
456 return str_to_type_map[type]
457 except KeyError:
458 raise Exception('Unknown partition type: %s' % type)
459
461 """Returns the partition which contains the root dir"""
462 return path_to_partition(disks, '/')
463
465 """Returns the partition which contains /boot"""
466 return path_to_partition(disks, '/boot/foo')
467
469 parts = get_ordered_partitions(disks)
470 parts.reverse()
471 for part in parts:
472 if path.startswith(part.mntpnt):
473 return part
474 raise VMBuilderException("Couldn't find partition path %s belongs to" % path)
475
477 for filesystem in vm.filesystems:
478 filesystem.create()
479
483
485 """Returns filesystems (self hosted as well as contained in partitions
486 in an order suitable for mounting them"""
487 fss = list(vm.filesystems)
488 for disk in vm.disks:
489 fss += [part.fs for part in disk.partitions]
490 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
491 return fss
492
494 """Returns partitions from disks in an order suitable for mounting them"""
495 parts = []
496 for disk in disks:
497 parts += disk.partitions
498 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or ''))
499 return parts
500
503
505 if not devname:
506 return 0
507 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
508
510 if index < 0:
511 return suffix
512 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
513
515 st = os.stat(filename)
516 if stat.S_ISREG(st.st_mode):
517 return st.st_size / 1024*1024
518 elif stat.S_ISBLK(st.st_mode):
519
520 BLKGETSIZE64 = 2148012658
521 fp = open(filename, 'r')
522 fd = fp.fileno()
523 s = fcntl.ioctl(fd, BLKGETSIZE64, ' '*8)
524 return unpack('L', s)[0] / 1024*1024
525
526 raise VMBuilderException('No idea how to find the size of %s' % filename)
527
529 exes = ['kvm-img', 'qemu-img']
530 for dir in os.environ['PATH'].split(os.path.pathsep):
531 for exe in exes:
532 path = '%s%s%s' % (dir, os.path.sep, exe)
533 if os.access(path, os.X_OK):
534 return path
535
537 exe = 'VBoxManage'
538 for dir in os.environ['PATH'].split(os.path.pathsep):
539 path = '%s%s%s' % (dir, os.path.sep, exe)
540 if os.access(path, os.X_OK):
541 return path
542