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

Source Code for Module VMBuilder.disk

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