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