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