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', '-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
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 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 # We always keep the partitions in order, so that the output from kpartx matches our understanding 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 # We don't convert preallocated disk images. That would be silly. 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
260 - class Partition(object):
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
283 - def set_filename(self, filename):
284 self.filename = filename 285 self.fs.filename = filename
286
287 - def parted_fstype(self):
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
294 - def create(self, disk):
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
304 - def mkfs(self):
305 """Adds Filesystem object""" 306 self.fs.mkfs()
307
308 - def get_grub_id(self):
309 """The name of the partition as known by grub""" 310 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
311
312 - def get_suffix(self):
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
317 - def get_index(self):
318 """Index of the disk (starting from 0)""" 319 return self.disk.partitions.index(self)
320
321 - def set_type(self, type):
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
330 -class Filesystem(object):
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
346 - def create(self):
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
369 - def mkfs(self):
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 # Let udev have a chance to extract the UUID for us 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
382 - def mkfs_fstype(self):
383 map = { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-F'], TYPE_EXT4: ['mkfs.ext4', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] } 384 385 if not self.vm.distro.has_256_bit_inode_ext3_support(): 386 map[TYPE_EXT3] = ['mkfs.ext3', '-I 128', '-F'] 387 388 return map[self.type]
389
390 - def fstab_fstype(self):
391 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext3', TYPE_EXT4: 'ext4', TYPE_XFS: 'xfs', TYPE_SWAP: 'swap' }[self.type]
392
393 - def fstab_options(self):
394 return 'defaults'
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
405 - def umount(self):
406 self.vm.cancel_cleanup(self.umount) 407 if (self.type != TYPE_SWAP) and not self.dummy: 408 logging.debug('Unmounting %s', self.mntpath) 409 run_cmd('umount', self.mntpath)
410
411 - def get_suffix(self):
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
419 - def devletters(self):
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
427 - def get_index(self):
428 """Index of the disk (starting from 0)""" 429 return self.vm.filesystems.index(self)
430
431 - def set_type(self, type):
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
440 -def parse_size(size_str):
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
466 -def str_to_type(type):
467 try: 468 return str_to_type_map[type] 469 except KeyError: 470 raise Exception('Unknown partition type: %s' % type)
471
472 -def rootpart(disks):
473 """Returns the partition which contains the root dir""" 474 return path_to_partition(disks, '/')
475
476 -def bootpart(disks):
477 """Returns the partition which contains /boot""" 478 return path_to_partition(disks, '/boot/foo')
479
480 -def path_to_partition(disks, path):
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
488 -def create_filesystems(vm):
489 for filesystem in vm.filesystems: 490 filesystem.create()
491
492 -def create_partitions(vm):
493 for disk in vm.disks: 494 disk.create(vm.workdir)
495
496 -def get_ordered_filesystems(vm):
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
505 -def get_ordered_partitions(disks):
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
513 -def devname_to_index(devname):
514 return devname_to_index_rec(devname) - 1
515
516 -def devname_to_index_rec(devname):
517 if not devname: 518 return 0 519 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
520
521 -def index_to_devname(index, suffix=''):
522 if index < 0: 523 return suffix 524 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
525
526 -def detect_size(filename):
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 # I really wish someone would make these available in Python 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
540 -def qemu_img_path():
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
548 -def vbox_manager_path():
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