]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
allow setting rootfstype to support ext3
[forks/vmdebootstrap.git] / vmdebootstrap
1 #! /usr/bin/python
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
4 # Copyright 2014 Neil Williams <codehelp@debian.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
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 import cliapp
20 import crypt
21 import logging
22 import os
23 import re
24 import shutil
25 import subprocess
26 import tempfile
27 import time
28
29
30 __version__ = '0.6'
31
32 # pylint: disable=invalid-name
33
34
35 class VmDebootstrap(cliapp.Application): # pylint: disable=too-many-public-methods
36
37 def __init__(self, progname=None, version=__version__, description=None, epilog=None):
38 super(VmDebootstrap, self).__init__(progname, version, description, epilog)
39 self.remove_dirs = []
40 self.mount_points = []
41
42 def add_settings(self):
43 default_arch = subprocess.check_output(
44 ["dpkg", "--print-architecture"]).strip()
45
46 self.settings.boolean(
47 ['verbose'], 'report what is going on')
48 self.settings.string(
49 ['image'], 'put created disk image in FILE',
50 metavar='FILE')
51 self.settings.bytesize(
52 ['size'],
53 'create a disk image of size SIZE (%default)',
54 metavar='SIZE',
55 default='1G')
56 self.settings.bytesize(
57 ['bootsize'],
58 'create boot partition of size SIZE (%default)',
59 metavar='BOOTSIZE',
60 default='0%')
61 self.settings.string(
62 ['boottype'],
63 'specify file system type for /boot/',
64 default='ext2')
65 self.settings.bytesize(
66 ['bootoffset'],
67 'Space to leave at start of the image for bootloader',
68 default='0')
69 self.settings.string(
70 ['part-type'],
71 'Partition type to use for this image',
72 default='msdos')
73 self.settings.string(
74 ['roottype'],
75 'specify file system type for /',
76 default='ext4')
77 self.settings.string(
78 ['foreign'],
79 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
80 self.settings.string(
81 ['variant'],
82 'select debootstrap variant it not using the default')
83 self.settings.boolean(
84 ['extlinux'],
85 'install extlinux?',
86 default=True)
87 self.settings.string(
88 ['tarball'],
89 "tar up the disk's contents in FILE",
90 metavar='FILE')
91 self.settings.string(
92 ['apt-mirror'],
93 'configure apt to use MIRROR',
94 metavar='URL')
95 self.settings.string(
96 ['mirror'],
97 'use MIRROR as package source (%default)',
98 metavar='URL',
99 default='http://http.debian.net/debian/')
100 self.settings.string(
101 ['arch'],
102 'architecture to use (%default)',
103 metavar='ARCH',
104 default=default_arch)
105 self.settings.string(
106 ['distribution'],
107 'release to use (%default)',
108 metavar='NAME',
109 default='stable')
110 self.settings.string_list(
111 ['package'],
112 'install PACKAGE onto system')
113 self.settings.string_list(
114 ['custom-package'],
115 'install package in DEB file onto system (not from mirror)',
116 metavar='DEB')
117 self.settings.boolean(
118 ['no-kernel'],
119 'do not install a linux package')
120 self.settings.boolean(
121 ['enable-dhcp'],
122 'enable DHCP on eth0')
123 self.settings.string(
124 ['root-password'],
125 'set root password',
126 metavar='PASSWORD')
127 self.settings.boolean(
128 ['lock-root-password'],
129 'lock root account so they cannot login?')
130 self.settings.string(
131 ['customize'],
132 'run SCRIPT after setting up system',
133 metavar='SCRIPT')
134 self.settings.string(
135 ['hostname'],
136 'set name to HOSTNAME (%default)',
137 metavar='HOSTNAME',
138 default='debian')
139 self.settings.string_list(
140 ['user'],
141 'create USER with PASSWORD',
142 metavar='USER/PASSWORD')
143 self.settings.boolean(
144 ['serial-console'],
145 'configure image to use a serial console')
146 self.settings.string(
147 ['serial-console-command'],
148 'command to manage the serial console, appended to /etc/inittab (%default)',
149 metavar='COMMAND',
150 default='/sbin/getty -L ttyS0 115200 vt100')
151 self.settings.boolean(
152 ['sudo'],
153 'install sudo, and if user is created, add them to sudo group')
154 self.settings.string(
155 ['owner'],
156 'the user who will own the image when the build is complete.')
157 self.settings.boolean(
158 ['squash'],
159 'use squashfs on the final image.')
160 self.settings.boolean(
161 ['configure-apt'],
162 'Create an apt source based on the distribution and mirror selected.')
163 self.settings.boolean(
164 ['mbr'],
165 'Run install-mbr (default if extlinux used)')
166 self.settings.boolean(
167 ['grub'],
168 'Install and configure grub2 - disables extlinux.')
169 self.settings.boolean(
170 ['sparse'],
171 'Do not fill the image with zeros to keep a sparse disk image',
172 default=False)
173 self.settings.boolean(
174 ['pkglist'],
175 'Create a list of package names included in the image.')
176
177 def process_args(self, args): # pylint: disable=too-many-branches,too-many-statements
178 if not self.settings['image'] and not self.settings['tarball']:
179 raise cliapp.AppException(
180 'You must give disk image filename, or tarball filename')
181 if self.settings['image'] and not self.settings['size']:
182 raise cliapp.AppException(
183 'If disk image is specified, you must give image size.')
184
185 rootdir = None
186 try:
187 rootdev = None
188 roottype = self.settings['roottype']
189 bootdev = None
190 boottype = None
191 if self.settings['image']:
192 self.create_empty_image()
193 self.partition_image()
194 if self.settings['mbr'] or self.settings['extlinux']:
195 self.install_mbr()
196 (rootdev, bootdev) = self.setup_kpartx()
197 self.mkfs(rootdev, fstype=roottype)
198 rootdir = self.mount(rootdev)
199 if bootdev:
200 if self.settings['boottype']:
201 boottype = self.settings['boottype']
202 else:
203 boottype = 'ext2'
204 self.mkfs(bootdev, fstype=boottype)
205 bootdir = '%s/%s' % (rootdir, 'boot/')
206 os.mkdir(bootdir)
207 self.mount(bootdev, bootdir)
208 else:
209 rootdir = self.mkdtemp()
210 self.debootstrap(rootdir)
211 self.set_hostname(rootdir)
212 self.create_fstab(rootdir, rootdev, roottype, bootdev, boottype)
213 self.install_debs(rootdir)
214 self.cleanup_apt_cache(rootdir)
215 self.set_root_password(rootdir)
216 self.create_users(rootdir)
217 self.remove_udev_persistent_rules(rootdir)
218 self.setup_networking(rootdir)
219 if self.settings['configure-apt'] or self.settings['apt-mirror']:
220 self.configure_apt(rootdir)
221 self.customize(rootdir)
222 self.update_initramfs(rootdir)
223
224 if self.settings['image']:
225 if self.settings['grub']:
226 self.install_grub2(rootdev, rootdir)
227 elif self.settings['extlinux']:
228 self.install_extlinux(rootdev, rootdir)
229 self.append_serial_console(rootdir)
230 self.optimize_image(rootdir)
231 if self.settings['squash']:
232 self.squash()
233 if self.settings['pkglist']:
234 self.list_installed_pkgs(rootdir)
235
236 if self.settings['foreign']:
237 os.unlink('%s/usr/bin/%s' %
238 (rootdir, os.path.basename(self.settings['foreign'])))
239
240 if self.settings['tarball']:
241 self.create_tarball(rootdir)
242
243 if self.settings['owner']:
244 self.chown()
245 except BaseException, e:
246 self.message('EEEK! Something bad happened...')
247 if rootdir:
248 db_log = os.path.join(rootdir, 'debootstrap', 'debootstrap.log')
249 if os.path.exists(db_log):
250 shutil.copy(db_log, os.getcwd())
251 self.message(e)
252 self.cleanup_system()
253 raise
254 else:
255 self.cleanup_system()
256
257 def message(self, msg):
258 logging.info(msg)
259 if self.settings['verbose']:
260 print msg
261
262 def runcmd(self, argv, stdin='', ignore_fail=False, env=None, **kwargs):
263 logging.debug('runcmd: %s %s %s', argv, env, kwargs)
264 p = subprocess.Popen(argv, stdin=subprocess.PIPE,
265 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
266 env=env, **kwargs)
267 out, err = p.communicate(stdin)
268 if p.returncode != 0:
269 msg = 'command failed: %s\n%s\n%s' % (argv, out, err)
270 logging.error(msg)
271 if not ignore_fail:
272 raise cliapp.AppException(msg)
273 return out
274
275 def mkdtemp(self):
276 dirname = tempfile.mkdtemp()
277 self.remove_dirs.append(dirname)
278 logging.debug('mkdir %s', dirname)
279 return dirname
280
281 def mount(self, device, path=None):
282 if not path:
283 mount_point = self.mkdtemp()
284 else:
285 mount_point = path
286 self.message('Mounting %s on %s' % (device, mount_point))
287 self.runcmd(['mount', device, mount_point])
288 self.mount_points.append(mount_point)
289 logging.debug('mounted %s on %s', device, mount_point)
290 return mount_point
291
292 def create_empty_image(self):
293 self.message('Creating disk image')
294 self.runcmd(['qemu-img', 'create', '-f', 'raw',
295 self.settings['image'],
296 str(self.settings['size'])])
297
298 def partition_image(self):
299 """
300 Uses fat16 (msdos) partitioning by default, use part-type to change.
301 If bootoffset is specified, the first actual partition
302 starts at that offset to allow customisation scripts to
303 put bootloader images into the space, e.g. u-boot.
304 """
305 self.message('Creating partitions')
306 self.runcmd(['parted', '-s', self.settings['image'],
307 'mklabel', self.settings['part-type']])
308 partoffset = 0
309 bootsize = 0
310 if self.settings['bootoffset'] and self.settings['bootoffset'] is not '0':
311 # turn v.small offsets into something at least possible to create.
312 if self.settings['bootoffset'] < 1048576:
313 partoffset = 1
314 logging.info(
315 "Setting bootoffset %smib to allow for %s bytes",
316 partoffset, self.settings['bootoffset'])
317 else:
318 partoffset = self.settings['bootoffset'] / (1024 * 1024)
319 self.message("Using bootoffset: %smib %s bytes" % (partoffset, self.settings['bootoffset']))
320 if self.settings['bootsize'] and self.settings['bootsize'] is not '0%':
321 bootsize = self.settings['bootsize'] / (1024 * 1024)
322 bootsize += partoffset
323 self.message("Using bootsize %smib: %s bytes" % (bootsize, self.settings['bootsize']))
324 logging.debug("Starting boot partition at %sMb", bootsize)
325 self.runcmd(['parted', '-s', self.settings['image'],
326 'mkpart', 'primary', 'fat16', str(partoffset), str(bootsize)])
327 if partoffset == 0:
328 self.runcmd(['parted', '-s', self.settings['image'],
329 'mkpart', 'primary', '0%', '100%'])
330 else:
331 logging.debug("Starting root partition at %sMb", partoffset)
332 self.runcmd(['parted', '-s', self.settings['image'],
333 'mkpart', 'primary', str(bootsize), '100%'])
334 self.runcmd(['parted', '-s', self.settings['image'],
335 'set', '1', 'boot', 'on'])
336
337 def update_initramfs(self, rootdir):
338 cmd = os.path.join('usr', 'sbin', 'update-initramfs')
339 if os.path.exists(os.path.join(rootdir, cmd)):
340 self.message("Updating the initramfs")
341 self.runcmd(['chroot', rootdir, cmd, '-u'])
342
343 def install_mbr(self):
344 if os.path.exists("/sbin/install-mbr"):
345 self.message('Installing MBR')
346 self.runcmd(['install-mbr', self.settings['image']])
347 else:
348 msg = "mbr enabled but /sbin/install-mbr not found" \
349 " - please install the mbr package."
350 raise cliapp.AppException(msg)
351
352 def setup_kpartx(self):
353 bootindex = None
354 out = self.runcmd(['kpartx', '-avs', self.settings['image']])
355 if self.settings['bootsize']:
356 bootindex = 0
357 rootindex = 1
358 parts = 2
359 else:
360 rootindex = 0
361 parts = 1
362 boot = None
363 devices = [line.split()[2]
364 for line in out.splitlines()
365 if line.startswith('add map ')]
366 if len(devices) != parts:
367 msg = 'Surprising number of partitions'
368 logging.debug("%s: devices=%s parts=%s", msg, devices, parts)
369 raise cliapp.AppException(msg)
370 root = '/dev/mapper/%s' % devices[rootindex]
371 if self.settings['bootsize']:
372 boot = '/dev/mapper/%s' % devices[bootindex]
373 return root, boot
374
375 def mkfs(self, device, fstype):
376 self.message('Creating filesystem %s' % fstype)
377 self.runcmd(['mkfs', '-t', fstype, device])
378
379 def debootstrap(self, rootdir):
380 msg = "(%s)" % self.settings['variant'] if self.settings['variant'] else ''
381 self.message('Debootstrapping %s %s' % (self.settings['distribution'], msg))
382
383 include = self.settings['package']
384
385 if not self.settings['foreign']:
386 include.append('acpid')
387
388 if self.settings['grub']:
389 include.append('grub2')
390
391 if not self.settings['no-kernel']:
392 if self.settings['arch'] == 'i386':
393 kernel_arch = '486'
394 else:
395 kernel_arch = self.settings['arch']
396 kernel_image = 'linux-image-%s' % kernel_arch
397 include.append(kernel_image)
398
399 if self.settings['sudo'] and 'sudo' not in include:
400 include.append('sudo')
401
402 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
403
404 if self.settings['package']:
405 args.append(
406 '--include=%s' % ','.join(include))
407 if self.settings['foreign']:
408 args.append('--foreign')
409 if self.settings['variant']:
410 args.append('--variant')
411 args.append(self.settings['variant'])
412 args += [self.settings['distribution'],
413 rootdir, self.settings['mirror']]
414 logging.debug(" ".join(args))
415 self.runcmd(args)
416 if self.settings['foreign']:
417 # set a noninteractive debconf environment for secondstage
418 env = {
419 "DEBIAN_FRONTEND": "noninteractive",
420 "DEBCONF_NONINTERACTIVE_SEEN": "true",
421 "LC_ALL": "C"
422 }
423 # add the mapping to the complete environment.
424 env.update(os.environ)
425 # First copy the binfmt handler over
426 self.message('Setting up binfmt handler')
427 shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
428 # Next, run the package install scripts etc.
429 self.message('Running debootstrap second stage')
430 self.runcmd(['chroot', rootdir,
431 '/debootstrap/debootstrap', '--second-stage'],
432 env=env)
433
434 def set_hostname(self, rootdir):
435 hostname = self.settings['hostname']
436 with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f:
437 f.write('%s\n' % hostname)
438
439 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
440 try:
441 with open(etc_hosts, 'r') as f:
442 data = f.read()
443 with open(etc_hosts, 'w') as f:
444 for line in data.splitlines():
445 if line.startswith('127.0.0.1'):
446 line += ' %s' % hostname
447 f.write('%s\n' % line)
448 except IOError:
449 pass
450
451 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments
452 def fsuuid(device):
453 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
454 '-s', 'UUID', device])
455 return out.splitlines()[0].strip()
456
457 if rootdev:
458 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
459 else:
460 rootdevstr = '/dev/sda1'
461
462 if bootdev:
463 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
464 else:
465 bootdevstr = None
466
467 fstab = os.path.join(rootdir, 'etc', 'fstab')
468 with open(fstab, 'w') as f:
469 f.write('proc /proc proc defaults 0 0\n')
470 f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype))
471 if bootdevstr:
472 f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype))
473
474 def install_debs(self, rootdir):
475 if not self.settings['custom-package']:
476 return
477 self.message('Installing custom packages')
478 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
479 os.mkdir(tmp)
480 for deb in self.settings['custom-package']:
481 shutil.copy(deb, tmp)
482 filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
483 for deb in self.settings['custom-package']]
484 out, err, _ = \
485 self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
486 logging.debug('stdout:\n%s', out)
487 logging.debug('stderr:\n%s', err)
488 out = self.runcmd(['chroot', rootdir,
489 'apt-get', '-f', '--no-remove', 'install'])
490 logging.debug('stdout:\n%s', out)
491 shutil.rmtree(tmp)
492
493 def cleanup_apt_cache(self, rootdir):
494 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
495 logging.debug('stdout:\n%s', out)
496
497 def set_root_password(self, rootdir):
498 if self.settings['root-password']:
499 self.message('Setting root password')
500 self.set_password(rootdir, 'root', self.settings['root-password'])
501 elif self.settings['lock-root-password']:
502 self.message('Locking root password')
503 self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root'])
504 else:
505 self.message('Give root an empty password')
506 self.delete_password(rootdir, 'root')
507
508 def create_users(self, rootdir):
509 def create_user(user):
510 self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user,
511 '--disabled-password', user])
512 if self.settings['sudo']:
513 self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo'])
514
515 for userpass in self.settings['user']:
516 if '/' in userpass:
517 user, password = userpass.split('/', 1)
518 create_user(user)
519 self.set_password(rootdir, user, password)
520 else:
521 create_user(userpass)
522 self.delete_password(rootdir, userpass)
523
524 def set_password(self, rootdir, user, password):
525 encrypted = crypt.crypt(password, '..')
526 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
527
528 def delete_password(self, rootdir, user):
529 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
530
531 def remove_udev_persistent_rules(self, rootdir):
532 self.message('Removing udev persistent cd and net rules')
533 for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
534 pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
535 if os.path.exists(pathname):
536 logging.debug('rm %s', pathname)
537 os.remove(pathname)
538 else:
539 logging.debug('not removing non-existent %s', pathname)
540
541 def setup_networking(self, rootdir):
542 self.message('Setting up networking')
543
544 f = open(os.path.join(rootdir, 'etc', 'network', 'interfaces'), 'w')
545 f.write('auto lo\n')
546 f.write('iface lo inet loopback\n')
547
548 if self.settings['enable-dhcp']:
549 f.write('\n')
550 f.write('auto eth0\n')
551 f.write('iface eth0 inet dhcp\n')
552
553 f.close()
554
555 def append_serial_console(self, rootdir):
556 if self.settings['serial-console']:
557 serial_command = self.settings['serial-console-command']
558 logging.debug('adding getty to serial console')
559 inittab = os.path.join(rootdir, 'etc/inittab')
560 # to autologin, serial_command can contain '-a root'
561 with open(inittab, 'a') as f:
562 f.write('\nS0:23:respawn:%s\n' % serial_command)
563
564 def install_grub2(self, rootdev, rootdir):
565 self.message("Configuring grub2")
566 # rely on kpartx using consistent naming to map loop0p1 to loop0
567 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
568 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
569 '%s' % os.path.join(rootdir, 'dev')])
570 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
571 '%s' % os.path.join(rootdir, 'proc')])
572 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
573 '%s' % os.path.join(rootdir, 'sys')])
574 try:
575 self.runcmd(['chroot', rootdir, 'update-grub'])
576 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
577 except cliapp.AppException:
578 self.message("Failed. Is grub2-common installed? Using extlinux.")
579 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
580 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
581 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
582 self.install_extlinux(rootdev, rootdir)
583
584 def install_extlinux(self, rootdev, rootdir):
585 if not os.path.exists("/usr/bin/extlinux"):
586 self.message("extlinux not installed, skipping.")
587 return
588 self.message('Installing extlinux')
589
590 def find(pattern):
591 dirname = os.path.join(rootdir, 'boot')
592 basenames = os.listdir(dirname)
593 logging.debug('find: %s', basenames)
594 for basename in basenames:
595 if re.search(pattern, basename):
596 return os.path.join('boot', basename)
597 raise cliapp.AppException('Cannot find match: %s' % pattern)
598
599 try:
600 kernel_image = find('vmlinuz-.*')
601 initrd_image = find('initrd.img-.*')
602 except cliapp.AppException as e:
603 self.message("Unable to find kernel. Not installing extlinux.")
604 logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
605 return
606
607 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
608 '-s', 'UUID', rootdev])
609 uuid = out.splitlines()[0].strip()
610
611 conf = os.path.join(rootdir, 'extlinux.conf')
612 logging.debug('configure extlinux %s', conf)
613 kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else ''
614 extserial = 'serial 0 115200' if self.settings['serial-console'] else ''
615 msg = '''
616 default linux
617 timeout 1
618
619 label linux
620 kernel %(kernel)s
621 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
622 %(extserial)s
623 ''' % {
624 'kernel': kernel_image, # pylint: disable=bad-continuation
625 'initrd': initrd_image, # pylint: disable=bad-continuation
626 'uuid': uuid, # pylint: disable=bad-continuation
627 'kserial': kserial, # pylint: disable=bad-continuation
628 'extserial': extserial, # pylint: disable=bad-continuation
629 } # pylint: disable=bad-continuation
630 logging.debug("extlinux config:\n%s", msg)
631
632 # python multiline string substitution is just ugly.
633 # use an external file or live with the mangling, no point in
634 # mangling the string to remove spaces just to keep it pretty in source.
635 f = open(conf, 'w')
636 f.write(msg)
637
638 self.runcmd(['extlinux', '--install', rootdir])
639 self.runcmd(['sync'])
640 time.sleep(2)
641
642 def optimize_image(self, rootdir):
643 """
644 Filing up the image with zeros will increase its compression rate
645 """
646 if not self.settings['sparse']:
647 zeros = os.path.join(rootdir, 'ZEROS')
648 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
649 self.runcmd(['rm', '-f', zeros])
650
651 def squash(self):
652 """
653 Run squashfs on the image.
654 """
655 if not os.path.exists('/usr/bin/mksquashfs'):
656 logging.warning("Squash selected but mksquashfs not found!")
657 return
658 self.message("Running mksquashfs")
659 suffixed = "%s.squashfs" % self.settings['image']
660 self.runcmd(['mksquashfs', self.settings['image'],
661 suffixed,
662 '-no-progress', '-comp', 'xz'], ignore_fail=False)
663 os.unlink(self.settings['image'])
664 self.settings['image'] = suffixed
665
666 def cleanup_system(self):
667 # Clean up after any errors.
668
669 self.message('Cleaning up')
670
671 # Umount in the reverse mount order
672 if self.settings['image']:
673 for i in range(len(self.mount_points) - 1, -1, -1):
674 mount_point = self.mount_points[i]
675 try:
676 self.runcmd(['umount', mount_point], ignore_fail=False)
677 except cliapp.AppException:
678 logging.debug("umount failed, sleeping and trying again")
679 time.sleep(5)
680 self.runcmd(['umount', mount_point], ignore_fail=False)
681
682 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
683
684 for dirname in self.remove_dirs:
685 shutil.rmtree(dirname)
686
687 def customize(self, rootdir):
688 script = self.settings['customize']
689 if not script:
690 return
691 if not os.path.exists(script):
692 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
693 if not os.path.exists(example):
694 self.message("Unable to find %s" % script)
695 return
696 script = example
697 self.message('Running customize script %s' % script)
698 logging.info("rootdir=%s image=%s", rootdir, self.settings['image'])
699 with open('/dev/tty', 'w') as tty:
700 cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty)
701
702 def create_tarball(self, rootdir):
703 # Create a tarball of the disk's contents
704 # shell out to runcmd since it more easily handles rootdir
705 self.message('Creating tarball of disk contents')
706 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
707
708 def chown(self):
709 # Change image owner after completed build
710 if self.settings['image']:
711 filename = self.settings['image']
712 elif self.settings['tarball']:
713 filename = self.settings['tarball']
714 else:
715 return
716 self.message("Changing owner to %s" % self.settings["owner"])
717 subprocess.call(["chown", self.settings["owner"], filename])
718
719 def list_installed_pkgs(self, rootdir):
720 # output the list of installed packages for sources identification
721 self.message("Creating a list of installed binary package names")
722 out = self.runcmd(['chroot', rootdir,
723 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
724 with open('dpkg.list', 'w') as dpkg:
725 dpkg.write(out)
726
727 def configure_apt(self, rootdir):
728 # use the distribution and mirror to create an apt source
729 self.message("Configuring apt to use distribution and mirror")
730 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
731 logging.debug('configure apt %s', conf)
732 mirror = self.settings['mirror']
733 if self.settings['apt-mirror']:
734 mirror = self.settings['apt-mirror']
735 self.message("Setting apt mirror to %s" % mirror)
736 os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
737 f = open(conf, 'w')
738 line = 'deb %s %s main\n' % (mirror, self.settings['distribution'])
739 f.write(line)
740 line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution'])
741 f.write(line)
742 f.close()
743 # ensure the apt sources have valid lists
744 self.runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update'])
745
746 if __name__ == '__main__':
747 VmDebootstrap(version=__version__).run()