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 - def devletters(self):
81 """ 82 @rtype: string 83 @return: the series of letters that ought to correspond to the device inside 84 the VM. E.g. the first disk of a VM would return 'a', while the 702nd would return 'zz' 85 """ 86 87 return index_to_devname(self.vm.disks.index(self))
88
89 - def create(self):
90 """ 91 Creates the disk image (if it doesn't already exist). 92 93 Once this method returns succesfully, L{filename} can be 94 expected to points to point to whatever holds the virtual disk 95 (be it a file, partition, logical volume, etc.). 96 """ 97 if not os.path.exists(self.filename): 98 logging.info('Creating disk image: "%s" of size: %dMB' % (self.filename, self.size)) 99 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size)
100
101 - def partition(self):
102 """ 103 Partitions the disk image. First adds a partition table and then 104 adds the individual partitions. 105 106 Should only be called once and only after you've added all partitions. 107 """ 108 109 logging.info('Adding partition table to disk image: %s' % self.filename) 110 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos') 111 112 # Partition the disk 113 for part in self.partitions: 114 part.create(self)
115
116 - def map_partitions(self):
117 """ 118 Create loop devices corresponding to the partitions. 119 120 Once this has returned succesfully, each partition's map device 121 is set as its L{filename<Disk.Partition.filename>} attribute. 122 123 Call this after L{partition}. 124 """ 125 logging.info('Creating loop devices corresponding to the created partitions') 126 self.vm.add_clean_cb(lambda : self.unmap(ignore_fail=True)) 127 kpartx_output = run_cmd('kpartx', '-av', self.filename) 128 parts = [] 129 for line in kpartx_output.split('\n'): 130 if line == "" or line.startswith("gpt:") or line.startswith("dos:"): 131 continue 132 if line.startswith("add"): 133 parts.append(line) 134 continue 135 logging.error('Skipping unknown line in kpartx output (%s)' % line) 136 mapdevs = [] 137 for line in parts: 138 mapdevs.append(line.split(' ')[2]) 139 for (part, mapdev) in zip(self.partitions, mapdevs): 140 part.set_filename('/dev/mapper/%s' % mapdev)
141
142 - def mkfs(self):
143 """ 144 Creates the partitions' filesystems 145 """ 146 logging.info("Creating file systems") 147 for part in self.partitions: 148 part.mkfs()
149
150 - def get_grub_id(self):
151 """ 152 @rtype: string 153 @return: name of the disk as known by grub 154 """ 155 return '(hd%d)' % self.get_index()
156
157 - def get_index(self):
158 """ 159 @rtype: number 160 @return: index of the disk (starting from 0 for the hypervisor's first disk) 161 """ 162 return self.vm.disks.index(self)
163
164 - def unmap(self, ignore_fail=False):
165 """ 166 Destroy all mapping devices 167 168 Unsets L{Partition}s' and L{Filesystem}s' filename attribute 169 """ 170 # first sleep to give the loopback devices a chance to settle down 171 time.sleep(3) 172 173 tries = 0 174 max_tries = 3 175 while tries < max_tries: 176 try: 177 run_cmd('kpartx', '-d', self.filename, ignore_fail=False) 178 break 179 except: 180 pass 181 tries += 1 182 time.sleep(3) 183 184 if tries >= max_tries: 185 # try it one last time 186 logging.info("Could not unmount '%s' after '%d' attempts. Final attempt" % (self.filename, tries)) 187 run_cmd('kpartx', '-d', self.filename, ignore_fail=ignore_fail) 188 189 for part in self.partitions: 190 part.set_filename(None)
191
192 - def add_part(self, begin, length, type, mntpnt):
193 """ 194 Add a partition to the disk 195 196 @type begin: number 197 @param begin: Start offset of the new partition (in megabytes) 198 @type length: 199 @param length: Size of the new partition (in megabytes) 200 @type type: string 201 @param type: Type of the new partition. Valid options are: ext2 ext3 xfs swap linux-swap 202 @type mntpnt: string 203 @param mntpnt: Intended mountpoint inside the guest of the new partition 204 """ 205 length = parse_size(length) 206 end = begin+length-1 207 logging.debug("add_part - begin %d, length %d, end %d, type %s, mntpnt %s" % (begin, length, end, type, mntpnt)) 208 for part in self.partitions: 209 if (begin >= part.begin and begin <= part.end) or \ 210 (end >= part.begin and end <= part.end): 211 raise VMBuilderUserError('Partitions are overlapping') 212 if begin < 0 or end > self.size: 213 raise VMBuilderUserError('Partition is out of bounds. start=%d, end=%d, disksize=%d' % (begin,end,self.size)) 214 part = self.Partition(disk=self, begin=begin, end=end, type=str_to_type(type), mntpnt=mntpnt) 215 self.partitions.append(part) 216 217 # We always keep the partitions in order, so that the output from kpartx matches our understanding 218 self.partitions.sort(cmp=lambda x,y: x.begin - y.begin)
219
220 - def convert(self, destdir, format):
221 """ 222 Convert the disk image 223 224 @type destdir: string 225 @param destdir: Target location of converted disk image 226 @type format: string 227 @param format: The target format (as understood by qemu-img or vdi) 228 @rtype: string 229 @return: the name of the converted image 230 """ 231 if self.preallocated: 232 # We don't convert preallocated disk images. That would be silly. 233 return self.filename 234 235 filename = os.path.basename(self.filename) 236 if '.' in filename: 237 filename = filename[:filename.rindex('.')] 238 destfile = '%s/%s.%s' % (destdir, filename, format) 239 240 logging.info('Converting %s to %s, format %s' % (self.filename, format, destfile)) 241 if format == 'vdi': 242 run_cmd(vbox_manager_path(), 'convertfromraw', '-format', 'VDI', self.filename, destfile) 243 else: 244 run_cmd(qemu_img_path(), 'convert', '-O', format, self.filename, destfile) 245 os.unlink(self.filename) 246 self.filename = os.path.abspath(destfile) 247 return destfile
248
249 - class Partition(object):
250 - def __init__(self, disk, begin, end, type, mntpnt):
251 self.disk = disk 252 "The disk on which this Partition resides." 253 254 self.begin = begin 255 "The start of the partition" 256 257 self.end = end 258 "The end of the partition" 259 260 self.type = type 261 "The partition type" 262 263 self.mntpnt = mntpnt 264 "The destined mount point" 265 266 self.filename = None 267 "The filename of this partition (the map device)" 268 269 self.fs = Filesystem(vm=self.disk.vm, type=self.type, mntpnt=self.mntpnt) 270 "The enclosed filesystem"
271
272 - def set_filename(self, filename):
273 self.filename = filename 274 self.fs.filename = filename
275
276 - def parted_fstype(self):
277 """ 278 @rtype: string 279 @return: the filesystem type of the partition suitable for passing to parted 280 """ 281 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext2', TYPE_EXT4: 'ext2', TYPE_XFS: 'ext2', TYPE_SWAP: 'linux-swap(new)' }[self.type]
282
283 - def create(self, disk):
284 """Adds partition to the disk image (does not mkfs or anything like that)""" 285 logging.info('Adding type %d partition to disk image: %s' % (self.type, disk.filename)) 286 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
287
288 - def mkfs(self):
289 """Adds Filesystem object""" 290 self.fs.mkfs()
291
292 - def get_grub_id(self):
293 """The name of the partition as known by grub""" 294 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
295
296 - def get_suffix(self):
297 """Returns 'a4' for a device that would be called /dev/sda4 in the guest. 298 This allows other parts of VMBuilder to set the prefix to something suitable.""" 299 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
300
301 - def get_index(self):
302 """Index of the disk (starting from 0)""" 303 return self.disk.partitions.index(self)
304
305 - def set_type(self, type):
306 try: 307 if int(type) == type: 308 self.type = type 309 else: 310 self.type = str_to_type(type) 311 except ValueError: 312 self.type = str_to_type(type)
313
314 -class Filesystem(object):
315 - def __init__(self, vm=None, size=0, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
316 self.vm = vm 317 self.filename = filename 318 self.size = parse_size(size) 319 self.devletter = devletter 320 self.device = device 321 self.dummy = dummy 322 323 self.set_type(type) 324 325 self.mntpnt = mntpnt 326 327 self.preallocated = False 328 "Whether the file existed already (True if it did, False if we had to create it)."
329
330 - def create(self):
331 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy))) 332 if not os.path.exists(self.filename): 333 logging.info('Not preallocated, so we create it.') 334 if not self.filename: 335 if self.mntpnt: 336 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower() 337 self.filename = re.sub('[\w/]', '_', self.filename) 338 if self.filename == '_': 339 self.filename = 'root' 340 elif self.type == TYPE_SWAP: 341 self.filename = 'swap' 342 else: 343 raise VMBuilderException('mntpnt not set') 344 345 self.filename = '%s/%s' % (self.vm.workdir, self.filename) 346 while os.path.exists('%s.img' % self.filename): 347 self.filename += '_' 348 self.filename += '.img' 349 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename) 350 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size) 351 self.mkfs()
352
353 - def mkfs(self):
354 if not self.filename: 355 raise VMBuilderException('We can\'t mkfs if filename is not set. Did you forget to call .create()?') 356 if not self.dummy: 357 cmd = self.mkfs_fstype() + [self.filename] 358 run_cmd(*cmd) 359 # Let udev have a chance to extract the UUID for us 360 run_cmd('udevadm', 'settle') 361 if os.path.exists("/sbin/vol_id"): 362 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip() 363 elif os.path.exists("/sbin/blkid"): 364 self.uuid = run_cmd('blkid', '-c', '/dev/null', '-sUUID', '-ovalue', self.filename).rstrip()
365
366 - def mkfs_fstype(self):
367 map = { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-F'], TYPE_EXT4: ['mkfs.ext4', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] } 368 369 if not self.vm.distro.has_256_bit_inode_ext3_support(): 370 map[TYPE_EXT3] = ['mkfs.ext3', '-I 128', '-F'] 371 372 return map[self.type]
373
374 - def fstab_fstype(self):
375 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext3', TYPE_EXT4: 'ext4', TYPE_XFS: 'xfs', TYPE_SWAP: 'swap' }[self.type]
376
377 - def fstab_options(self):
378 return 'defaults'
379
380 - def mount(self, rootmnt):
381 if (self.type != TYPE_SWAP) and not self.dummy: 382 logging.debug('Mounting %s', self.mntpnt) 383 self.mntpath = '%s%s' % (rootmnt, self.mntpnt) 384 if not os.path.exists(self.mntpath): 385 os.makedirs(self.mntpath) 386 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath) 387 self.vm.add_clean_cb(self.umount)
388
389 - def umount(self):
390 self.vm.cancel_cleanup(self.umount) 391 if (self.type != TYPE_SWAP) and not self.dummy: 392 logging.debug('Unmounting %s', self.mntpath) 393 run_cmd('umount', self.mntpath)
394
395 - def get_suffix(self):
396 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.. 397 This allows other parts of VMBuilder to set the prefix to something suitable.""" 398 if self.device: 399 return self.device 400 else: 401 return '%s%d' % (self.devletters(), self.get_index() + 1)
402
403 - def devletters(self):
404 """ 405 @rtype: string 406 @return: the series of letters that ought to correspond to the device inside 407 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz' 408 """ 409 return self.devletter
410
411 - def get_index(self):
412 """Index of the disk (starting from 0)""" 413 return self.vm.filesystems.index(self)
414
415 - def set_type(self, type):
416 try: 417 if int(type) == type: 418 self.type = type 419 else: 420 self.type = str_to_type(type) 421 except ValueError: 422 self.type = str_to_type(type)
423
424 -def parse_size(size_str):
425 """Takes a size like qemu-img would accept it and returns the size in MB""" 426 try: 427 return int(size_str) 428 except ValueError: 429 pass 430 431 try: 432 num = int(size_str[:-1]) 433 except ValueError: 434 raise VMBuilderUserError("Invalid size: %s" % size_str) 435 436 if size_str[-1:] == 'g' or size_str[-1:] == 'G': 437 return num * 1024 438 if size_str[-1:] == 'm' or size_str[-1:] == 'M': 439 return num 440 if size_str[-1:] == 'k' or size_str[-1:] == 'K': 441 return num / 1024
442 443 str_to_type_map = { 'ext2': TYPE_EXT2, 444 'ext3': TYPE_EXT3, 445 'ext4': TYPE_EXT4, 446 'xfs': TYPE_XFS, 447 'swap': TYPE_SWAP, 448 'linux-swap': TYPE_SWAP } 449
450 -def str_to_type(type):
451 try: 452 return str_to_type_map[type] 453 except KeyError: 454 raise Exception('Unknown partition type: %s' % type)
455
456 -def rootpart(disks):
457 """Returns the partition which contains the root dir""" 458 return path_to_partition(disks, '/')
459
460 -def bootpart(disks):
461 """Returns the partition which contains /boot""" 462 return path_to_partition(disks, '/boot/foo')
463
464 -def path_to_partition(disks, path):
465 parts = get_ordered_partitions(disks) 466 parts.reverse() 467 for part in parts: 468 if path.startswith(part.mntpnt): 469 return part 470 raise VMBuilderException("Couldn't find partition path %s belongs to" % path)
471
472 -def create_filesystems(vm):
473 for filesystem in vm.filesystems: 474 filesystem.create()
475
476 -def create_partitions(vm):
477 for disk in vm.disks: 478 disk.create(vm.workdir)
479
480 -def get_ordered_filesystems(vm):
481 """Returns filesystems (self hosted as well as contained in partitions 482 in an order suitable for mounting them""" 483 fss = list(vm.filesystems) 484 for disk in vm.disks: 485 fss += [part.fs for part in disk.partitions] 486 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or '')) 487 return fss
488
489 -def get_ordered_partitions(disks):
490 """Returns partitions from disks in an order suitable for mounting them""" 491 parts = [] 492 for disk in disks: 493 parts += disk.partitions 494 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or '')) 495 return parts
496
497 -def devname_to_index(devname):
498 return devname_to_index_rec(devname) - 1
499
500 -def devname_to_index_rec(devname):
501 if not devname: 502 return 0 503 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
504
505 -def index_to_devname(index, suffix=''):
506 if index < 0: 507 return suffix 508 return index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26]) + suffix
509
510 -def detect_size(filename):
511 st = os.stat(filename) 512 if stat.S_ISREG(st.st_mode): 513 return st.st_size / 1024*1024 514 elif stat.S_ISBLK(st.st_mode): 515 # I really wish someone would make these available in Python 516 BLKGETSIZE64 = 2148012658 517 fp = open(filename, 'r') 518 fd = fp.fileno() 519 s = fcntl.ioctl(fd, BLKGETSIZE64, ' '*8) 520 return unpack('L', s)[0] / 1024*1024 521 522 raise VMBuilderException('No idea how to find the size of %s' % filename)
523
524 -def qemu_img_path():
525 exes = ['kvm-img', 'qemu-img'] 526 for dir in os.environ['PATH'].split(os.path.pathsep): 527 for exe in exes: 528 path = '%s%s%s' % (dir, os.path.sep, exe) 529 if os.access(path, os.X_OK): 530 return path
531
532 -def vbox_manager_path():
533 exe = 'VBoxManage' 534 for dir in os.environ['PATH'].split(os.path.pathsep): 535 path = '%s%s%s' % (dir, os.path.sep, exe) 536 if os.access(path, os.X_OK): 537 return path
538