]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
Allow space for a bootloader installed with dd
[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 if self.settings['foreign']:
374 necessary_packages = []
375 else:
376 necessary_packages = ['acpid']
377
378 if self.settings['grub']:
379 necessary_packages.append('grub2')
380
381 include = self.settings['package']
382
383 if not self.settings['no-kernel']:
384 if self.settings['arch'] == 'i386':
385 kernel_arch = '486'
386 else:
387 kernel_arch = self.settings['arch']
388 kernel_image = 'linux-image-%s' % kernel_arch
389 include.append(kernel_image)
390
391 if self.settings['sudo'] and 'sudo' not in include:
392 include.append('sudo')
393
394 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
395 if self.settings['package']:
396 args.append(
397 '--include=%s' % ','.join(include))
398 if len(necessary_packages) > 0:
399 args.append(
400 '--include=%s' % ','.join(necessary_packages))
401 if self.settings['foreign']:
402 args.append('--foreign')
403 if self.settings['variant']:
404 args.append('--variant')
405 args.append(self.settings['variant'])
406 args += [self.settings['distribution'],
407 rootdir, self.settings['mirror']]
408 logging.debug(" ".join(args))
409 self.runcmd(args)
410 if self.settings['foreign']:
411 # set a noninteractive debconf environment for secondstage
412 env = {
413 "DEBIAN_FRONTEND": "noninteractive",
414 "DEBCONF_NONINTERACTIVE_SEEN": "true",
415 "LC_ALL": "C"
416 }
417 # add the mapping to the complete environment.
418 env.update(os.environ)
419 # First copy the binfmt handler over
420 self.message('Setting up binfmt handler')
421 shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
422 # Next, run the package install scripts etc.
423 self.message('Running debootstrap second stage')
424 self.runcmd(['chroot', rootdir,
425 '/debootstrap/debootstrap', '--second-stage'],
426 env=env)
427
428 def set_hostname(self, rootdir):
429 hostname = self.settings['hostname']
430 with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f:
431 f.write('%s\n' % hostname)
432
433 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
434 try:
435 with open(etc_hosts, 'r') as f:
436 data = f.read()
437 with open(etc_hosts, 'w') as f:
438 for line in data.splitlines():
439 if line.startswith('127.0.0.1'):
440 line += ' %s' % hostname
441 f.write('%s\n' % line)
442 except IOError:
443 pass
444
445 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments
446 def fsuuid(device):
447 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
448 '-s', 'UUID', device])
449 return out.splitlines()[0].strip()
450
451 if rootdev:
452 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
453 else:
454 rootdevstr = '/dev/sda1'
455
456 if bootdev:
457 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
458 else:
459 bootdevstr = None
460
461 fstab = os.path.join(rootdir, 'etc', 'fstab')
462 with open(fstab, 'w') as f:
463 f.write('proc /proc proc defaults 0 0\n')
464 f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype))
465 if bootdevstr:
466 f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype))
467
468 def install_debs(self, rootdir):
469 if not self.settings['custom-package']:
470 return
471 self.message('Installing custom packages')
472 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
473 os.mkdir(tmp)
474 for deb in self.settings['custom-package']:
475 shutil.copy(deb, tmp)
476 filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
477 for deb in self.settings['custom-package']]
478 out, err, _ = \
479 self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
480 logging.debug('stdout:\n%s', out)
481 logging.debug('stderr:\n%s', err)
482 out = self.runcmd(['chroot', rootdir,
483 'apt-get', '-f', '--no-remove', 'install'])
484 logging.debug('stdout:\n%s', out)
485 shutil.rmtree(tmp)
486
487 def cleanup_apt_cache(self, rootdir):
488 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
489 logging.debug('stdout:\n%s', out)
490
491 def set_root_password(self, rootdir):
492 if self.settings['root-password']:
493 self.message('Setting root password')
494 self.set_password(rootdir, 'root', self.settings['root-password'])
495 elif self.settings['lock-root-password']:
496 self.message('Locking root password')
497 self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root'])
498 else:
499 self.message('Give root an empty password')
500 self.delete_password(rootdir, 'root')
501
502 def create_users(self, rootdir):
503 def create_user(user):
504 self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user,
505 '--disabled-password', user])
506 if self.settings['sudo']:
507 self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo'])
508
509 for userpass in self.settings['user']:
510 if '/' in userpass:
511 user, password = userpass.split('/', 1)
512 create_user(user)
513 self.set_password(rootdir, user, password)
514 else:
515 create_user(userpass)
516 self.delete_password(rootdir, userpass)
517
518 def set_password(self, rootdir, user, password):
519 encrypted = crypt.crypt(password, '..')
520 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
521
522 def delete_password(self, rootdir, user):
523 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
524
525 def remove_udev_persistent_rules(self, rootdir):
526 self.message('Removing udev persistent cd and net rules')
527 for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
528 pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
529 if os.path.exists(pathname):
530 logging.debug('rm %s', pathname)
531 os.remove(pathname)
532 else:
533 logging.debug('not removing non-existent %s', pathname)
534
535 def setup_networking(self, rootdir):
536 self.message('Setting up networking')
537
538 f = open(os.path.join(rootdir, 'etc', 'network', 'interfaces'), 'w')
539 f.write('auto lo\n')
540 f.write('iface lo inet loopback\n')
541
542 if self.settings['enable-dhcp']:
543 f.write('\n')
544 f.write('auto eth0\n')
545 f.write('iface eth0 inet dhcp\n')
546
547 f.close()
548
549 def append_serial_console(self, rootdir):
550 if self.settings['serial-console']:
551 serial_command = self.settings['serial-console-command']
552 logging.debug('adding getty to serial console')
553 inittab = os.path.join(rootdir, 'etc/inittab')
554 # to autologin, serial_command can contain '-a root'
555 with open(inittab, 'a') as f:
556 f.write('\nS0:23:respawn:%s\n' % serial_command)
557
558 def install_grub2(self, rootdev, rootdir):
559 self.message("Configuring grub2")
560 # rely on kpartx using consistent naming to map loop0p1 to loop0
561 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
562 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
563 '%s' % os.path.join(rootdir, 'dev')])
564 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
565 '%s' % os.path.join(rootdir, 'proc')])
566 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
567 '%s' % os.path.join(rootdir, 'sys')])
568 try:
569 self.runcmd(['chroot', rootdir, 'update-grub'])
570 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
571 except cliapp.AppException:
572 self.message("Failed. Is grub2-common installed? Using extlinux.")
573 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
574 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
575 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
576 self.install_extlinux(rootdev, rootdir)
577
578 def install_extlinux(self, rootdev, rootdir):
579 if not os.path.exists("/usr/bin/extlinux"):
580 self.message("extlinux not installed, skipping.")
581 return
582 self.message('Installing extlinux')
583
584 def find(pattern):
585 dirname = os.path.join(rootdir, 'boot')
586 basenames = os.listdir(dirname)
587 logging.debug('find: %s', basenames)
588 for basename in basenames:
589 if re.search(pattern, basename):
590 return os.path.join('boot', basename)
591 raise cliapp.AppException('Cannot find match: %s' % pattern)
592
593 try:
594 kernel_image = find('vmlinuz-.*')
595 initrd_image = find('initrd.img-.*')
596 except cliapp.AppException as e:
597 self.message("Unable to find kernel. Not installing extlinux.")
598 logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
599 return
600
601 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
602 '-s', 'UUID', rootdev])
603 uuid = out.splitlines()[0].strip()
604
605 conf = os.path.join(rootdir, 'extlinux.conf')
606 logging.debug('configure extlinux %s', conf)
607 # python multiline string substitution is just ugly.
608 # use an external file or live with the mangling, no point in
609 # mangling the string to remove spaces just to keep it pretty in source.
610 f = open(conf, 'w')
611 f.write('''
612 default linux
613 timeout 1
614
615 label linux
616 kernel %(kernel)s
617 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
618 %(extserial)s
619 ''' % {
620 'kernel': kernel_image, # pylint: disable=bad-continuation
621 'initrd': initrd_image, # pylint: disable=bad-continuation
622 'uuid': uuid, # pylint: disable=bad-continuation
623 'kserial': # pylint: disable=bad-continuation
624 'console=ttyS0,115200' if self.settings['serial-console'] else '', # pylint: disable=bad-continuation
625 'extserial': 'serial 0 115200' if self.settings['serial-console'] else '', # pylint: disable=bad-continuation
626 }) # pylint: disable=bad-continuation
627 f.close() # pylint: disable=bad-continuation
628
629 self.runcmd(['extlinux', '--install', rootdir])
630 self.runcmd(['sync'])
631 time.sleep(2)
632
633 def optimize_image(self, rootdir):
634 """
635 Filing up the image with zeros will increase its compression rate
636 """
637 if not self.settings['sparse']:
638 zeros = os.path.join(rootdir, 'ZEROS')
639 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
640 self.runcmd(['rm', '-f', zeros])
641
642 def squash(self):
643 """
644 Run squashfs on the image.
645 """
646 if not os.path.exists('/usr/bin/mksquashfs'):
647 logging.warning("Squash selected but mksquashfs not found!")
648 return
649 self.message("Running mksquashfs")
650 suffixed = "%s.squashfs" % self.settings['image']
651 self.runcmd(['mksquashfs', self.settings['image'],
652 suffixed,
653 '-no-progress', '-comp', 'xz'], ignore_fail=False)
654 os.unlink(self.settings['image'])
655 self.settings['image'] = suffixed
656
657 def cleanup_system(self):
658 # Clean up after any errors.
659
660 self.message('Cleaning up')
661
662 # Umount in the reverse mount order
663 if self.settings['image']:
664 for i in range(len(self.mount_points) - 1, -1, -1):
665 mount_point = self.mount_points[i]
666 try:
667 self.runcmd(['umount', mount_point], ignore_fail=False)
668 except cliapp.AppException:
669 logging.debug("umount failed, sleeping and trying again")
670 time.sleep(5)
671 self.runcmd(['umount', mount_point], ignore_fail=False)
672
673 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
674
675 for dirname in self.remove_dirs:
676 shutil.rmtree(dirname)
677
678 def customize(self, rootdir):
679 script = self.settings['customize']
680 if not script:
681 return
682 if not os.path.exists(script):
683 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
684 if not os.path.exists(example):
685 self.message("Unable to find %s" % script)
686 return
687 script = example
688 self.message('Running customize script %s' % script)
689 logging.info("rootdir=%s image=%s", rootdir, self.settings['image'])
690 with open('/dev/tty', 'w') as tty:
691 cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty)
692
693 def create_tarball(self, rootdir):
694 # Create a tarball of the disk's contents
695 # shell out to runcmd since it more easily handles rootdir
696 self.message('Creating tarball of disk contents')
697 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
698
699 def chown(self):
700 # Change image owner after completed build
701 self.message("Changing owner to %s" % self.settings["owner"])
702 subprocess.call(["chown",
703 self.settings["owner"],
704 self.settings["image"]])
705
706 def list_installed_pkgs(self, rootdir):
707 # output the list of installed packages for sources identification
708 self.message("Creating a list of installed binary package names")
709 out = self.runcmd(['chroot', rootdir,
710 'dpkg-query', '-W' "-f='${Package}.deb\n'"])
711 with open('dpkg.list', 'w') as dpkg:
712 dpkg.write(out)
713
714 def configure_apt(self, rootdir):
715 # use the distribution and mirror to create an apt source
716 self.message("Configuring apt to use distribution and mirror")
717 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
718 logging.debug('configure apt %s', conf)
719 mirror = self.settings['mirror']
720 if self.settings['apt-mirror']:
721 mirror = self.settings['apt-mirror']
722 self.message("Setting apt mirror to %s" % mirror)
723 os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
724 f = open(conf, 'w')
725 line = 'deb %s %s main\n' % (mirror, self.settings['distribution'])
726 f.write(line)
727 line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution'])
728 f.write(line)
729 f.close()
730
731 if __name__ == '__main__':
732 VmDebootstrap(version=__version__).run()