Package VMBuilder :: Module disk
[frames] | no frames]

Source Code for Module VMBuilder.disk

  1  # 
  2  #    Uncomplicated VM Builder 
  3  #    Copyright (C) 2007-2010 Canonical Ltd. 
  4  # 
  5  #    See AUTHORS for list of contributors 
  6  # 
  7  #    This program is free software: you can redistribute it and/or modify 
  8  #    it under the terms of the GNU General Public License version 3, as 
  9  #    published by the Free Software Foundation. 
 10  # 
 11  #    This program is distributed in the hope that it will be useful, 
 12  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  #    GNU General Public License for more details. 
 15  # 
 16  #    You should have received a copy of the GNU General Public License 
 17  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 18  # 
 19  #    Virtual disk management 
 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   
39 -class Disk(object):
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
83 - def devletters(self):
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
92 - def create(self):
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
104 - def partition(self):
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 # Partition the disk 116 for part in self.partitions: 117 part.create(self)
118
119 - def map_partitions(self):
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
145 - def mkfs(self):
146 """ 147 Creates the partitions' filesystems 148 """ 149 logging.info("Creating file systems") 150 for part in self.partitions: 151 part.mkfs()
152
153 - def get_grub_id(self):
154 """ 155 @rtype: string 156 @return: name of the disk as known by grub 157 """ 158 return '(hd%d)' % self.get_index()
159
160 - def get_index(self):
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 # first sleep to give the loopback devices a chance to settle down 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 # try it one last time 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 # We always keep the partitions in order, so that the output from kpartx matches our understanding 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 # We don't convert preallocated disk images. That would be silly. 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
253 - class Partition(object):
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
276 - def set_filename(self, filename):
277 self.filename = filename 278 self.fs.filename = filename
279
280 - def parted_fstype(self):
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
287 - def create(self, disk):
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
297 - def mkfs(self):
298 """Adds Filesystem object""" 299 self.fs.mkfs()
300
301 - def get_grub_id(self):
302 """The name of the partition as known by grub""" 303 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
304
305 - def get_suffix(self):
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
310 - def get_index(self):
311 """Index of the disk (starting from 0)""" 312 return self.disk.partitions.index(self)
313
314 - def set_type(self, type):
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
323 -class Filesystem(object):
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
339 - def create(self):
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
362 - def mkfs(self):
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 # Let udev have a chance to extract the UUID for us 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
375 - def mkfs_fstype(self):
376 map = { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-F'], TYPE_EXT4: ['mkfs.ext4', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] } 377 378 if not self.vm.distro.has_256_bit_inode_ext3_support(): 379 map[TYPE_EXT3] = ['mkfs.ext3', '-I 128', '-F'] 380 381 return map[self.type]
382
383 - def fstab_fstype(self):
384 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext3', TYPE_EXT4: 'ext4', TYPE_XFS: 'xfs', TYPE_SWAP: 'swap' }[self.type]
385
386 - def fstab_options(self):
387 return 'defaults'
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
398 - def umount(self):
399 self.vm.cancel_cleanup(self.umount) 400 if (self.type != TYPE_SWAP) and not self.dummy: 401 logging.debug('Unmounting %s', self.mntpath) 402 run_cmd('umount', self.mntpath)
403
404 - def get_suffix(self):
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
412 - def devletters(self):
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
420 - def get_index(self):
421 """Index of the disk (starting from 0)""" 422 return self.vm.filesystems.index(self)
423
424 - def set_type(self, type):
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
433 -def parse_size(size_str):
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
459 -def str_to_type(type):
460 try: 461 return str_to_type_map[type] 462 except KeyError: 463 raise Exception('Unknown partition type: %s' % type)
464
465 -def rootpart(disks):
466 """Returns the partition which contains the root dir""" 467 return path_to_partition(disks, '/')
468
469 -def bootpart(disks):
470 """Returns the partition which contains /boot""" 471 return path_to_partition(disks, '/boot/foo')
472
473 -def path_to_partition(disks, path):
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
481 -def create_filesystems(vm):
482 for filesystem in vm.filesystems: 483 filesystem.create()
484
485 -def create_partitions(vm):
486 for disk in vm.disks: 487 disk.create(vm.workdir)
488
489 -def get_ordered_filesystems(vm):
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
498 -def get_ordered_partitions(disks):
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
506 -def devname_to_index(devname):
507 return devname_to_index_rec(devname) - 1
508
509 -def devname_to_index_rec(devname):
510 if not devname: 511 return 0 512 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
513
514 -def index_to_devname(index, suffix=''):
515 if index < 0: 516 return suffix 517 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
518
519 -def detect_size(filename):
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 # I really wish someone would make these available in Python 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
533 -def qemu_img_path():
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
541 -def vbox_manager_path():
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