1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
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
50 self.vm = vm
51
52
53 self.size = parse_size(size)
54
55 self.preallocated = preallocated
56
57
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
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
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
91
92
93 logging.info('Adding partition table to disk image: %s' % self.filename)
94 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos')
95
96
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
112
113
114
115 logging.info("Creating file systems")
116 for part in self.partitions:
117 part.mkfs()
118
120 """
121 @rtype: string
122 @return: name of the disk as known by grub
123 """
124 return '(hd%d)' % self.get_index()
125
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
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
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
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
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
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
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
225 """The name of the partition as known by grub"""
226 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
227
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
234 """Index of the disk (starting from 0)"""
235 return self.disk.partitions.index(self)
236
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
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
279 cmd = self.mkfs_fstype() + [self.filename]
280 run_cmd(*cmd)
281 self.uuid = run_cmd('vol_id', '--uuid', self.filename).rstrip()
282
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
293
296
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
311
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
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
326 """Index of the disk (starting from 0)"""
327 return self.vm.filesystems.index(self)
328
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
355 try:
356 return str_to_type_map[type]
357 except KeyError, e:
358 raise Exception('Unknown partition type: %s' % type)
359
361 """Returns the partition which contains /boot"""
362 return path_to_partition(disks, '/boot/foo')
363
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
373 for filesystem in vm.filesystems:
374 filesystem.create()
375
379
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
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
399
401 if not devname:
402 return 0
403 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
404
406 if index < 0:
407 return suffix
408 return suffix + index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26])
409
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