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