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 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
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
51 self.vm = vm
52
53
54 self.size = parse_size(size)
55
56 self.preallocated = preallocated
57
58
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
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
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
92
93
94 logging.info('Adding partition table to disk image: %s' % self.filename)
95 run_cmd('parted', '--script', self.filename, 'mklabel', 'msdos')
96
97
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
119
120
121
122 logging.info("Creating file systems")
123 for part in self.partitions:
124 part.mkfs()
125
127 """
128 @rtype: string
129 @return: name of the disk as known by grub
130 """
131 return '(hd%d)' % self.get_index()
132
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
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
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
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
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
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
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
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
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
250 """The name of the partition as known by grub"""
251 return '(hd%d,%d)' % (self.disk.get_index(), self.get_index())
252
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
259 """Index of the disk (starting from 0)"""
260 return self.disk.partitions.index(self)
261
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
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
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
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
321
324
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
339
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
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
357 """Index of the disk (starting from 0)"""
358 return self.vm.filesystems.index(self)
359
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
386 try:
387 return str_to_type_map[type]
388 except KeyError, e:
389 raise Exception('Unknown partition type: %s' % type)
390
394
402
404 for filesystem in vm.filesystems:
405 filesystem.create()
406
410
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
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
430
432 if not devname:
433 return 0
434 return 26 * devname_to_index_rec(devname[:-1]) + (string.ascii_lowercase.index(devname[-1]) + 1)
435
437 if index < 0:
438 return suffix
439 return suffix + index_to_devname(index / 26 -1, string.ascii_lowercase[index % 26])
440
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