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 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 # 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 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
291
292 - def mkfs(self):
293 """Adds Filesystem object""" 294 self.fs.mkfs()
295
296 - def get_grub_id(self):
297 """The name of the partition as known by grub""" 298 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
299
300 - def get_suffix(self):
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
305 - def get_index(self):
306 """Index of the disk (starting from 0)""" 307 return self.disk.partitions.index(self)
308
309 - def set_type(self, type):
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
318 -class Filesystem(object):
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
334 - def create(self):
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
357 - def mkfs(self):
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 # Let udev have a chance to extract the UUID for us 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
370 - def mkfs_fstype(self):
371 map = { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-F'], TYPE_EXT4: ['mkfs.ext4', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] } 372 373 if not self.vm.distro.has_256_bit_inode_ext3_support(): 374 map[TYPE_EXT3] = ['mkfs.ext3', '-I 128', '-F'] 375 376 return map[self.type]
377
378 - def fstab_fstype(self):
379 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext3', TYPE_EXT4: 'ext4', TYPE_XFS: 'xfs', TYPE_SWAP: 'swap' }[self.type]
380
381 - def fstab_options(self):
382 return 'defaults'
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
393 - def umount(self):
394 self.vm.cancel_cleanup(self.umount) 395 if (self.type != TYPE_SWAP) and not self.dummy: 396 logging.debug('Unmounting %s', self.mntpath) 397 run_cmd('umount', self.mntpath)
398
399 - def get_suffix(self):
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
407 - def devletters(self):
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
415 - def get_index(self):
416 """Index of the disk (starting from 0)""" 417 return self.vm.filesystems.index(self)
418
419 - def set_type(self, type):
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
428 -def parse_size(size_str):
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
454 -def str_to_type(type):
455 try: 456 return str_to_type_map[type] 457 except KeyError: 458 raise Exception('Unknown partition type: %s' % type)
459
460 -def rootpart(disks):
461 """Returns the partition which contains the root dir""" 462 return path_to_partition(disks, '/')
463
464 -def bootpart(disks):
465 """Returns the partition which contains /boot""" 466 return path_to_partition(disks, '/boot/foo')
467
468 -def path_to_partition(disks, path):
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
476 -def create_filesystems(vm):
477 for filesystem in vm.filesystems: 478 filesystem.create()
479
480 -def create_partitions(vm):
481 for disk in vm.disks: 482 disk.create(vm.workdir)
483
484 -def get_ordered_filesystems(vm):
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
493 -def get_ordered_partitions(disks):
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
501 -def devname_to_index(devname):
502 return devname_to_index_rec(devname) - 1
503
504 -def devname_to_index_rec(devname):
505 if not devname: 506 return 0 507 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
508
509 -def index_to_devname(index, suffix=''):
510 if index < 0: 511 return suffix 512 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
513
514 -def detect_size(filename):
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 # I really wish someone would make these available in Python 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
528 -def qemu_img_path():
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
536 -def vbox_manager_path():
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