]> git.siccegge.de Git - forks/vmdebootstrap.git/blobdiff - vmdebootstrap
Add support for apt mirror and package list.
[forks/vmdebootstrap.git] / vmdebootstrap
index 80089a513c8961c70d74a9e8c9c00457088c3f1d..d5c1067aa84144f942d20fdc7abf783f0de6a35c 100755 (executable)
@@ -1,6 +1,7 @@
-#!/usr/bin/python
+#! /usr/bin/python
 # Copyright 2011-2013  Lars Wirzenius
 # Copyright 2012  Codethink Limited
+# Copyright 2014 Neil Williams <codehelp@debian.org>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -20,22 +21,20 @@ import crypt
 import logging
 import os
 import re
-import time
 import shutil
 import subprocess
 import tempfile
 import time
 
 
-__version__ = '0.3'
+__version__ = '0.6'
 
 
 class VmDebootstrap(cliapp.Application):
 
     def add_settings(self):
-        default_arch = self.runcmd(
-            ["dpkg", "--print-architecture"],
-            ignore_fail=False).strip()
+        default_arch = subprocess.check_output(
+            ["dpkg", "--print-architecture"]).strip()
 
         self.settings.boolean(['verbose'], 'report what is going on')
         self.settings.string(['image'], 'put created disk image in FILE',
@@ -55,16 +54,16 @@ class VmDebootstrap(cliapp.Application):
                              'set up foreign debootstrap environment using provided program (ie binfmt handler)')
         self.settings.string(['variant'],
                              'select debootstrap variant it not using the default')
-        self.settings.boolean(
-            ['extlinux'],
-            'install extlinux?',
-            default=True)
+        self.settings.boolean(['extlinux'], 'install extlinux?', default=True)
         self.settings.string(['tarball'], "tar up the disk's contents in FILE",
                              metavar='FILE')
+        self.settings.string(['apt-mirror'],
+                             'configure apt to use MIRROR',
+                             metavar='URL')
         self.settings.string(['mirror'],
                              'use MIRROR as package source (%default)',
                              metavar='URL',
-                             default='http://cdn.debian.net/debian/')
+                             default='http://http.debian.net/debian/')
         self.settings.string(['arch'], 'architecture to use (%default)',
                              metavar='ARCH',
                              default=default_arch)
@@ -116,6 +115,14 @@ class VmDebootstrap(cliapp.Application):
         self.settings.boolean(['grub'],
                               'Install and configure grub2 - disables '
                               'extlinux.')
+        self.settings.boolean(['sparse'],
+                              'Dont fill the image with zeros to keep a sparse disk image',
+                              default=False)
+        self.settings.boolean(['pkglist'],
+                              'Create a list of package names included in '
+                              'the image.')
+        self.remove_dirs = []
+        self.mount_points = []
 
     def process_args(self, args):
         if not self.settings['image'] and not self.settings['tarball']:
@@ -140,14 +147,14 @@ class VmDebootstrap(cliapp.Application):
                 if self.settings['mbr']:
                     self.install_mbr()
                 (rootdev, bootdev) = self.setup_kpartx()
-                self.mkfs(rootdev, type=roottype)
+                self.mkfs(rootdev, fstype=roottype)
                 rootdir = self.mount(rootdev)
                 if bootdev:
                     if self.settings['boottype']:
                         boottype = self.settings['boottype']
                     else:
                         boottype = 'ext2'
-                    self.mkfs(bootdev, type=boottype)
+                    self.mkfs(bootdev, fstype=boottype)
                     bootdir = '%s/%s' % (rootdir, 'boot/')
                     os.mkdir(bootdir)
                     bootdir = self.mount(bootdev, bootdir)
@@ -162,9 +169,11 @@ class VmDebootstrap(cliapp.Application):
             self.create_users(rootdir)
             self.remove_udev_persistent_rules(rootdir)
             self.setup_networking(rootdir)
-            if self.settings['configure-apt']:
+            if self.settings['configure-apt'] or self.settings['apt-mirror']:
                 self.configure_apt(rootdir)
             self.customize(rootdir)
+            self.update_initramfs(rootdir)
+
             if self.settings['image']:
                 if self.settings['grub']:
                     self.install_grub2(rootdev, rootdir)
@@ -174,6 +183,8 @@ class VmDebootstrap(cliapp.Application):
                 self.optimize_image(rootdir)
                 if self.settings['squash']:
                     self.squash()
+            if self.settings['pkglist']:
+                self.list_installed_pkgs(rootdir)
 
             if self.settings['foreign']:
                 os.unlink('%s/usr/bin/%s' %
@@ -201,11 +212,11 @@ class VmDebootstrap(cliapp.Application):
         if self.settings['verbose']:
             print msg
 
-    def runcmd(self, argv, stdin='', ignore_fail=False, **kwargs):
-        logging.debug('runcmd: %s %s' % (argv, kwargs))
+    def runcmd(self, argv, stdin='', ignore_fail=False, env=None, **kwargs):
+        logging.debug('runcmd: %s %s %s', argv, env, kwargs)
         p = subprocess.Popen(argv, stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
-                             **kwargs)
+                             env=env, **kwargs)
         out, err = p.communicate(stdin)
         if p.returncode != 0:
             msg = 'command failed: %s\n%s\n%s' % (argv, out, err)
@@ -217,7 +228,7 @@ class VmDebootstrap(cliapp.Application):
     def mkdtemp(self):
         dirname = tempfile.mkdtemp()
         self.remove_dirs.append(dirname)
-        logging.debug('mkdir %s' % dirname)
+        logging.debug('mkdir %s', dirname)
         return dirname
 
     def mount(self, device, path=None):
@@ -228,7 +239,7 @@ class VmDebootstrap(cliapp.Application):
         self.message('Mounting %s on %s' % (device, mount_point))
         self.runcmd(['mount', device, mount_point])
         self.mount_points.append(mount_point)
-        logging.debug('mounted %s on %s' % (device, mount_point))
+        logging.debug('mounted %s on %s', device, mount_point)
         return mount_point
 
     def create_empty_image(self):
@@ -244,7 +255,7 @@ class VmDebootstrap(cliapp.Application):
         if self.settings['bootsize'] and self.settings['bootsize'] is not '0%':
             bootsize = str(self.settings['bootsize'] / (1024 * 1024))
             self.runcmd(['parted', '-s', self.settings['image'],
-                         'mkpart', 'primary', 'fat16', '0', bootsize])
+                         'mkpart', 'primary', 'fat16', '0%', bootsize])
         else:
             bootsize = '0%'
         self.runcmd(['parted', '-s', self.settings['image'],
@@ -252,6 +263,12 @@ class VmDebootstrap(cliapp.Application):
         self.runcmd(['parted', '-s', self.settings['image'],
                      'set', '1', 'boot', 'on'])
 
+    def update_initramfs(self, rootdir):
+        cmd = os.path.join('usr', 'sbin', 'update-initramfs')
+        if os.path.exists(os.path.join(rootdir, cmd)):
+            self.message("Updating the initramfs")
+            self.runcmd(['chroot', rootdir, cmd, '-u'])
+
     def install_mbr(self):
         if os.path.exists("/sbin/install-mbr"):
             self.message('Installing MBR')
@@ -277,9 +294,9 @@ class VmDebootstrap(cliapp.Application):
             boot = '/dev/mapper/%s' % devices[bootindex]
         return (root, boot)
 
-    def mkfs(self, device, type):
-        self.message('Creating filesystem %s' % type)
-        self.runcmd(['mkfs', '-t', type, device])
+    def mkfs(self, device, fstype):
+        self.message('Creating filesystem %s' % fstype)
+        self.runcmd(['mkfs', '-t', fstype, device])
 
     def debootstrap(self, rootdir):
         self.message('Debootstrapping')
@@ -306,8 +323,9 @@ class VmDebootstrap(cliapp.Application):
             include.append('sudo')
 
         args = ['debootstrap', '--arch=%s' % self.settings['arch']]
-        args.append(
-            '--include=%s' % ','.join(necessary_packages + include))
+        if self.settings['package'] and len(necessary_packages) > 0:
+            args.append(
+                '--include=%s' % ','.join(necessary_packages + include))
         if self.settings['foreign']:
             args.append('--foreign')
         if self.settings['variant']:
@@ -318,12 +336,22 @@ class VmDebootstrap(cliapp.Application):
         logging.debug(" ".join(args))
         self.runcmd(args)
         if self.settings['foreign']:
+            # set a noninteractive debconf environment for secondstage
+            env = {
+                "DEBIAN_FRONTEND": "noninteractive",
+                "DEBCONF_NONINTERACTIVE_SEEN": "true",
+                "LC_ALL": "C"
+            }
+            # add the mapping to the complete environment.
+            env.update(os.environ)
             # First copy the binfmt handler over
+            self.message('Setting up binfmt handler')
             shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
             # Next, run the package install scripts etc.
             self.message('Running debootstrap second stage')
             self.runcmd(['chroot', rootdir,
-                         '/debootstrap/debootstrap', '--second-stage'])
+                         '/debootstrap/debootstrap', '--second-stage'],
+                        env=env)
 
     def set_hostname(self, rootdir):
         hostname = self.settings['hostname']
@@ -339,7 +367,7 @@ class VmDebootstrap(cliapp.Application):
                     if line.startswith('127.0.0.1'):
                         line += ' %s' % hostname
                     f.write('%s\n' % line)
-        except IOError, e:
+        except IOError:
             pass
 
     def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype):
@@ -375,18 +403,18 @@ class VmDebootstrap(cliapp.Application):
             shutil.copy(deb, tmp)
         filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
                      for deb in self.settings['custom-package']]
-        out, err, exit = \
+        out, err, exitcode = \
             self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
-        logging.debug('stdout:\n%s' % out)
-        logging.debug('stderr:\n%s' % err)
+        logging.debug('stdout:\n%s', out)
+        logging.debug('stderr:\n%s', err)
         out = self.runcmd(['chroot', rootdir,
-                          'apt-get', '-f', '--no-remove', 'install'])
-        logging.debug('stdout:\n%s' % out)
+                           'apt-get', '-f', '--no-remove', 'install'])
+        logging.debug('stdout:\n%s', out)
         shutil.rmtree(tmp)
 
     def cleanup_apt_cache(self, rootdir):
         out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
-        logging.debug('stdout:\n%s' % out)
+        logging.debug('stdout:\n%s', out)
 
     def set_root_password(self, rootdir):
         if self.settings['root-password']:
@@ -427,10 +455,10 @@ class VmDebootstrap(cliapp.Application):
         for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
             pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
             if os.path.exists(pathname):
-                logging.debug('rm %s' % pathname)
+                logging.debug('rm %s', pathname)
                 os.remove(pathname)
             else:
-                logging.debug('not removing non-existent %s' % pathname)
+                logging.debug('not removing non-existent %s', pathname)
 
     def setup_networking(self, rootdir):
         self.message('Setting up networking')
@@ -451,6 +479,7 @@ class VmDebootstrap(cliapp.Application):
             serial_command = self.settings['serial-console-command']
             logging.debug('adding getty to serial console')
             inittab = os.path.join(rootdir, 'etc/inittab')
+            # to autologin, serial_command can contain '-a root'
             with open(inittab, 'a') as f:
                 f.write('\nS0:23:respawn:%s\n' % serial_command)
 
@@ -459,16 +488,16 @@ class VmDebootstrap(cliapp.Application):
         # rely on kpartx using consistent naming to map loop0p1 to loop0
         install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
         self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
-                    '%s' % os.path.join(rootdir, 'dev')])
+                     '%s' % os.path.join(rootdir, 'dev')])
         self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
-                    '%s' % os.path.join(rootdir, 'proc')])
+                     '%s' % os.path.join(rootdir, 'proc')])
         self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
-                    '%s' % os.path.join(rootdir, 'sys')])
+                     '%s' % os.path.join(rootdir, 'sys')])
         try:
             self.runcmd(['chroot', rootdir, 'update-grub'])
             self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
         except cliapp.AppException as e:
-            self.message("Failed to configure grub2. Using extlinux.")
+            self.message("Failed. Is grub2-common installed? Using extlinux.")
         self.runcmd(['umount', os.path.join(rootdir, 'sys')])
         self.runcmd(['umount', os.path.join(rootdir, 'proc')])
         self.runcmd(['umount', os.path.join(rootdir, 'dev')])
@@ -483,21 +512,26 @@ class VmDebootstrap(cliapp.Application):
         def find(pattern):
             dirname = os.path.join(rootdir, 'boot')
             basenames = os.listdir(dirname)
-            logging.debug('find: %s' % basenames)
+            logging.debug('find: %s', basenames)
             for basename in basenames:
                 if re.search(pattern, basename):
                     return os.path.join('boot', basename)
             raise cliapp.AppException('Cannot find match: %s' % pattern)
 
-        kernel_image = find('vmlinuz-.*')
-        initrd_image = find('initrd.img-.*')
+        try:
+            kernel_image = find('vmlinuz-.*')
+            initrd_image = find('initrd.img-.*')
+        except cliapp.AppException as e:
+            self.message("Unable to find kernel. Not installing extlinux.")
+            logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
+            return
 
         out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
                            '-s', 'UUID', rootdev])
         uuid = out.splitlines()[0].strip()
 
         conf = os.path.join(rootdir, 'extlinux.conf')
-        logging.debug('configure extlinux %s' % conf)
+        logging.debug('configure extlinux %s', conf)
         f = open(conf, 'w')
         f.write('''
 default linux
@@ -525,9 +559,10 @@ append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
         """
         Filing up the image with zeros will increase its compression rate
         """
-        zeros = os.path.join(rootdir, 'ZEROS')
-        self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
-        self.runcmd(['rm', '-f', zeros])
+        if not self.settings['sparse']:
+            zeros = os.path.join(rootdir, 'ZEROS')
+            self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
+            self.runcmd(['rm', '-f', zeros])
 
     def squash(self):
         """
@@ -567,10 +602,17 @@ append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
 
     def customize(self, rootdir):
         script = self.settings['customize']
-        if script:
-            self.message('Running customize script %s' % script)
-            with open('/dev/tty', 'w') as tty:
-                cliapp.runcmd([script, rootdir], stdout=tty, stderr=tty)
+        if not script:
+            return
+        if not os.path.exists(script):
+            example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
+            if not os.path.exists(example):
+                self.message("Unable to find %s" % script)
+                return
+            script = example
+        self.message('Running customize script %s' % script)
+        with open('/dev/tty', 'w') as tty:
+            cliapp.runcmd([script, rootdir], stdout=tty, stderr=tty)
 
     def create_tarball(self, rootdir):
         # Create a tarball of the disk's contents
@@ -585,17 +627,30 @@ append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
                          self.settings["owner"],
                          self.settings["image"]])
 
+    def list_installed_pkgs(self, rootdir):
+        # output the list of installed packages for sources identification
+        self.message("Creating a list of installed binary package names")
+        out = self.runcmd(['chroot', rootdir,
+                           'dpkg-query', '-W' "-f='${Package}.deb\n'"])
+        with open('dpkg.list', 'w') as dpkg:
+            dpkg.write(out)
+
     def configure_apt(self, rootdir):
         # use the distribution and mirror to create an apt source
         self.message("Configuring apt to use distribution and mirror")
         conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
-        logging.debug('configure apt %s' % conf)
+        logging.debug('configure apt %s', conf)
+        mirror = self.settings['mirror']
+        if self.settings['apt-mirror']:
+            mirror = self.settings['apt-mirror']
+            self.message("Setting apt mirror to %s" % mirror)
+        os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
         f = open(conf, 'w')
         f.write('''
 deb %(mirror)s %(distribution)s main
 #deb-src %(mirror)s %(distribution)s main
 ''' % {
-            'mirror': self.settings['mirror'],
+            'mirror': mirror,
             'distribution': self.settings['distribution']
         })
         f.close()