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

Source Code for Module VMBuilder.disk

  1  # 
  2  #    Uncomplicated VM Builder 
  3  #    Copyright (C) 2007-2009 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 logging 
 22  import os.path 
 23  import re 
 24  import stat 
 25  import string 
 26  import tempfile 
 27  import time 
 28  import VMBuilder 
 29  from   VMBuilder.util      import run_cmd  
 30  from   VMBuilder.exception import VMBuilderUserError, VMBuilderException 
 31   
 32  TYPE_EXT2 = 0 
 33  TYPE_EXT3 = 1 
 34  TYPE_XFS = 2 
 35  TYPE_SWAP = 3 
 36   
37 -class Disk(object):
38 - def __init__(self, vm, size='5G', preallocated=False, filename=None):
39 """ 40 @type size: string or number 41 @param size: The size of the disk image (passed to L{parse_size}) 42 43 @type preallocated: boolean 44 @param preallocated: if True, the disk image already exists and will not be created (useful for raw devices) 45 46 @type filename: string 47 @param filename: force a certain filename or to give the name of the preallocated disk image 48 """ 49 50 # We need this for "introspection" 51 self.vm = vm 52 53 # Perhaps this should be the frontend's responsibility? 54 self.size = parse_size(size) 55 56 self.preallocated = preallocated 57 58 # If filename isn't given, make one up 59 if filename: 60 self.filename = filename 61 else: 62 if self.preallocated: 63 raise VMBuilderUserError('Preallocated was set, but no filename given') 64 self.filename = 'disk%d.img' % len(self.vm.disks) 65 66 self.partitions = []
67
68 - def devletters(self):
69 """ 70 @rtype: string 71 @return: the series of letters that ought to correspond to the device inside 72 the VM. E.g. the first disk of a VM would return 'a', while the 702nd would return 'zz' 73 """ 74 75 return index_to_devname(self.vm.disks.index(self))
76
77 - def create(self, directory):
78 """ 79 Creates the disk image (unless preallocated), partitions it, creates the partition mapping devices and mkfs's the partitions 80 81 @type directory: string 82 @param directory: If set, the disk image is created in this directory 83 """ 84 85 if not self.preallocated: 86 if directory: 87 self.filename = '%s/%s' % (directory, self.filename) 88 logging.info('Creating disk image: %s' % self.filename) 89 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size) 90 os.chmod(self.filename, stat.S_IRUSR | stat.S_IWUSR) 91 92 # From here, we assume that self.filename refers to whatever holds the disk image, 93 # be it a file, a partition, logical volume, actual disk.. 94 95 logging.info('Adding partition table to disk image: %s' % self.filename) 96 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos') 97 98 # Partition the disk 99 for part in self.partitions: 100 part.create(self) 101 102 logging.info('Creating loop devices corresponding to the created partitions') 103 self.vm.add_clean_cb(lambda : self.unmap(ignore_fail=True)) 104 kpartx_output = run_cmd('kpartx', '-av', self.filename) 105 parts = [] 106 for line in kpartx_output.split('\n'): 107 if line == "" or line.startswith("gpt:") or line.startswith("dos:"): 108 continue 109 if line.startswith("add"): 110 parts.append(line) 111 continue 112 logging.error('Skipping unknown line in kpartx output (%s)' % line) 113 mapdevs = [] 114 for line in parts: 115 mapdevs.append(line.split(' ')[2]) 116 for (part, mapdev) in zip(self.partitions, mapdevs): 117 part.mapdev = '/dev/mapper/%s' % mapdev 118 119 # At this point, all partitions are created and their mapping device has been 120 # created and set as .mapdev 121 122 # Adds a filesystem to the partition 123 logging.info("Creating file systems") 124 for part in self.partitions: 125 part.mkfs()
126
127 - def get_grub_id(self):
128 """ 129 @rtype: string 130 @return: name of the disk as known by grub 131 """ 132 return '(hd%d)' % self.get_index()
133
134 - def get_index(self):
135 """ 136 @rtype: number 137 @return: index of the disk (starting from 0) 138 """ 139 return self.vm.disks.index(self)
140
141 - def unmap(self, ignore_fail=False):
142 """ 143 Destroy all mapping devices 144 """ 145 # first sleep to give the loopback devices a chance to settle down 146 time.sleep(3) 147 148 tries = 0 149 max_tries = 3 150 while tries < max_tries: 151 try: 152 run_cmd('kpartx', '-d', self.filename, ignore_fail=False) 153 break 154 except: 155 pass 156 tries += 1 157 time.sleep(3) 158 159 if tries >= max_tries: 160 # try it one last time 161 logging.info("Could not unmount '%s' after '%d' attempts. Final attempt" % (self.filename, tries)) 162 run_cmd('kpartx', '-d', self.filename, ignore_fail=ignore_fail) 163 164 for part in self.partitions: 165 self.mapdev = None
166
167 - def add_part(self, begin, length, type, mntpnt):
168 """ 169 Add a partition to the disk 170 171 @type begin: number 172 @param begin: Start offset of the new partition (in megabytes) 173 @type length: 174 @param length: Size of the new partition (in megabytes) 175 @type type: string 176 @param type: Type of the new partition. Valid options are: ext2 ext3 xfs swap linux-swap 177 @type mntpnt: string 178 @param mntpnt: Intended mountpoint inside the guest of the new partition 179 """ 180 end = begin+length-1 181 logging.debug("add_part - begin %d, length %d, end %d" % (begin, length, end)) 182 for part in self.partitions: 183 if (begin >= part.begin and begin <= part.end) or \ 184 (end >= part.begin and end <= part.end): 185 raise Exception('Partitions are overlapping') 186 if begin > end: 187 raise Exception('Partition\'s last block is before its first') 188 if begin < 0 or end > self.size: 189 raise Exception('Partition is out of bounds. start=%d, end=%d, disksize=%d' % (begin,end,self.size)) 190 part = self.Partition(disk=self, begin=begin, end=end, type=str_to_type(type), mntpnt=mntpnt) 191 self.partitions.append(part) 192 193 # We always keep the partitions in order, so that the output from kpartx matches our understanding 194 self.partitions.sort(cmp=lambda x,y: x.begin - y.begin)
195
196 - def convert(self, destdir, format):
197 """ 198 Convert the disk image 199 200 @type destdir: string 201 @param destdir: Target location of converted disk image 202 @type format: string 203 @param format: The target format (as understood by qemu-img or vdi) 204 @rtype: string 205 @return: the name of the converted image 206 """ 207 if self.preallocated: 208 # We don't convert preallocated disk images. That would be silly. 209 return self.filename 210 211 filename = os.path.basename(self.filename) 212 if '.' in filename: 213 filename = filename[:filename.rindex('.')] 214 destfile = '%s/%s.%s' % (destdir, filename, format) 215 216 logging.info('Converting %s to %s, format %s' % (self.filename, format, destfile)) 217 if format == 'vdi': 218 run_cmd(vbox_manager_path(), 'convertfromraw', '-format', 'VDI', self.filename, destfile) 219 else: 220 run_cmd(qemu_img_path(), 'convert', '-O', format, self.filename, destfile) 221 os.unlink(self.filename) 222 self.filename = os.path.abspath(destfile) 223 return destfile
224
225 - class Partition(object):
226 - def __init__(self, disk, begin, end, type, mntpnt):
227 self.disk = disk 228 self.begin = begin 229 self.end = end 230 self.type = type 231 self.mntpnt = mntpnt 232 self.mapdev = None
233
234 - def parted_fstype(self):
235 """ 236 @rtype: string 237 @return: the filesystem type of the partition suitable for passing to parted 238 """ 239 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext2', TYPE_XFS: 'ext2', TYPE_SWAP: 'linux-swap(new)' }[self.type]
240
241 - def create(self, disk):
242 """Adds partition to the disk image (does not mkfs or anything like that)""" 243 logging.info('Adding type %d partition to disk image: %s' % (self.type, disk.filename)) 244 run_cmd('parted', '--script', '--', disk.filename, 'mkpart', 'primary', self.parted_fstype(), self.begin, self.end)
245
246 - def mkfs(self):
247 """Adds Filesystem object""" 248 if not self.mapdev: 249 raise Exception('We can\'t mkfs before we have a mapper device') 250 self.fs = Filesystem(self.disk.vm, preallocated=True, filename=self.mapdev, type=self.type, mntpnt=self.mntpnt) 251 self.fs.mkfs()
252
253 - def get_grub_id(self):
254 """The name of the partition as known by grub""" 255 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
256
257 - def get_suffix(self):
258 """Returns 'a4' for a device that would be called /dev/sda4 in the guest. 259 This allows other parts of VMBuilder to set the prefix to something suitable.""" 260 return '%s%d' % (self.disk.devletters(), self.get_index() + 1)
261
262 - def get_index(self):
263 """Index of the disk (starting from 0)""" 264 return self.disk.partitions.index(self)
265
266 -class Filesystem(object):
267 - def __init__(self, vm, size=0, preallocated=False, type=None, mntpnt=None, filename=None, devletter='a', device='', dummy=False):
268 self.vm = vm 269 self.filename = filename 270 self.size = parse_size(size) 271 self.preallocated = preallocated 272 self.devletter = devletter 273 self.device = device 274 self.dummy = dummy 275 276 try: 277 if int(type) == type: 278 self.type = type 279 else: 280 self.type = str_to_type(type) 281 except ValueError, e: 282 self.type = str_to_type(type) 283 284 self.mntpnt = mntpnt
285
286 - def create(self):
287 logging.info('Creating filesystem: %s, size: %d, dummy: %s' % (self.mntpnt, self.size, repr(self.dummy))) 288 if not self.preallocated: 289 logging.info('Not preallocated, so we create it.') 290 if not self.filename: 291 if self.mntpnt: 292 self.filename = re.sub('[^\w\s/]', '', self.mntpnt).strip().lower() 293 self.filename = re.sub('[\w/]', '_', self.filename) 294 if self.filename == '_': 295 self.filename = 'root' 296 elif self.type == TYPE_SWAP: 297 self.filename = 'swap' 298 else: 299 raise VMBuilderException('mntpnt not set') 300 301 self.filename = '%s/%s' % (self.vm.workdir, self.filename) 302 while os.path.exists('%s.img' % self.filename): 303 self.filename += '_' 304 self.filename += '.img' 305 logging.info('A name wasn\'t specified either, so we make one up: %s' % self.filename) 306 run_cmd(qemu_img_path(), 'create', '-f', 'raw', self.filename, '%dM' % self.size) 307 self.mkfs()
308
309 - def mkfs(self):
310 if not self.dummy: 311 cmd = self.mkfs_fstype() + [self.filename] 312 run_cmd(*cmd) 313 if os.path.exists("/sbin/vol_id"): 314 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip() 315 elif os.path.exists("/sbin/blkid"): 316 self.uuid = run_cmd('blkid', '-sUUID', '-ovalue', self.filename).rstrip()
317
318 - def mkfs_fstype(self):
319 if self.vm.suite in ['dapper', 'edgy', 'feisty', 'gutsy']: 320 logging.debug('%s: 128 bit inode' % self.vm.suite) 321 return { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-I 128', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] }[self.type] 322 else: 323 logging.debug('%s: 256 bit inode' % self.vm.suite) 324 return { TYPE_EXT2: ['mkfs.ext2', '-F'], TYPE_EXT3: ['mkfs.ext3', '-F'], TYPE_XFS: ['mkfs.xfs'], TYPE_SWAP: ['mkswap'] }[self.type]
325
326 - def fstab_fstype(self):
327 return { TYPE_EXT2: 'ext2', TYPE_EXT3: 'ext3', TYPE_XFS: 'xfs', TYPE_SWAP: 'swap' }[self.type]
328
329 - def fstab_options(self):
330 return 'defaults'
331
332 - def mount(self):
333 if (self.type != TYPE_SWAP) and not self.dummy: 334 logging.debug('Mounting %s', self.mntpnt) 335 self.mntpath = '%s%s' % (self.vm.rootmnt, self.mntpnt) 336 if not os.path.exists(self.mntpath): 337 os.makedirs(self.mntpath) 338 run_cmd('mount', '-o', 'loop', self.filename, self.mntpath) 339 self.vm.add_clean_cb(self.umount)
340
341 - def umount(self):
342 self.vm.cancel_cleanup(self.umount) 343 if (self.type != TYPE_SWAP) and not self.dummy: 344 logging.debug('Unmounting %s', self.mntpath) 345 run_cmd('umount', self.mntpath)
346
347 - def get_suffix(self):
348 """Returns 'a4' for a device that would be called /dev/sda4 in the guest.. 349 This allows other parts of VMBuilder to set the prefix to something suitable.""" 350 if self.device: 351 return self.device 352 else: 353 return '%s%d' % (self.devletters(), self.get_index() + 1)
354
355 - def devletters(self):
356 """ 357 @rtype: string 358 @return: the series of letters that ought to correspond to the device inside 359 the VM. E.g. the first filesystem of a VM would return 'a', while the 702nd would return 'zz' 360 """ 361 return self.devletter
362
363 - def get_index(self):
364 """Index of the disk (starting from 0)""" 365 return self.vm.filesystems.index(self)
366
367 -def parse_size(size_str):
368 """Takes a size like qemu-img would accept it and returns the size in MB""" 369 try: 370 return int(size_str) 371 except ValueError, e: 372 pass 373 374 try: 375 num = int(size_str[:-1]) 376 except ValueError, e: 377 raise VMBuilderUserError("Invalid size: %s" % size_str) 378 379 if size_str[-1:] == 'g' or size_str[-1:] == 'G': 380 return num * 1024 381 if size_str[-1:] == 'm' or size_str[-1:] == 'M': 382 return num 383 if size_str[-1:] == 'k' or size_str[-1:] == 'K': 384 return num / 1024
385 386 str_to_type_map = { 'ext2': TYPE_EXT2, 387 'ext3': TYPE_EXT3, 388 'xfs': TYPE_XFS, 389 'swap': TYPE_SWAP, 390 'linux-swap': TYPE_SWAP } 391
392 -def str_to_type(type):
393 try: 394 return str_to_type_map[type] 395 except KeyError, e: 396 raise Exception('Unknown partition type: %s' % type)
397
398 -def bootpart(disks):
399 """Returns the partition which contains /boot""" 400 return path_to_partition(disks, '/boot/foo')
401
402 -def path_to_partition(disks, path):
403 parts = get_ordered_partitions(disks) 404 parts.reverse() 405 for part in parts: 406 if path.startswith(part.mntpnt): 407 return part 408 raise VMBuilderException("Couldn't find partition path %s belongs to" % path)
409
410 -def create_filesystems(vm):
411 for filesystem in vm.filesystems: 412 filesystem.create()
413
414 -def create_partitions(vm):
415 for disk in vm.disks: 416 disk.create(vm.workdir)
417
418 -def get_ordered_filesystems(vm):
419 """Returns filesystems (self hosted as well as contained in partitions 420 in an order suitable for mounting them""" 421 fss = list(vm.filesystems) 422 for disk in vm.disks: 423 fss += [part.fs for part in disk.partitions] 424 fss.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or '')) 425 return fss
426
427 -def get_ordered_partitions(disks):
428 """Returns partitions from disks in an order suitable for mounting them""" 429 parts = [] 430 for disk in disks: 431 parts += disk.partitions 432 parts.sort(lambda x,y: len(x.mntpnt or '')-len(y.mntpnt or '')) 433 return parts
434
435 -def devname_to_index(devname):
436 return devname_to_index_rec(devname) - 1
437
438 -def devname_to_index_rec(devname):
439 if not devname: 440 return 0 441 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
442
443 -def index_to_devname(index, suffix=''):
444 if index < 0: 445 return suffix 446 return suffix + index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26])
447
448 -def qemu_img_path():
449 exes = ['kvm-img', 'qemu-img'] 450 for dir in os.environ['PATH'].split(os.path.pathsep): 451 for exe in exes: 452 path = '%s%s%s' % (dir, os.path.sep, exe) 453 if os.access(path, os.X_OK): 454 return path
455
456 -def vbox_manager_path():
457 exe = 'VBoxManage' 458 for dir in os.environ['PATH'].split(os.path.pathsep): 459 path = '%s%s%s' % (dir, os.path.sep, exe) 460 if os.access(path, os.X_OK): 461 return path
462