aboutsummaryrefslogtreecommitdiffstats
path: root/qemu-headless/make-vm-debootstrap-efi.py
diff options
context:
space:
mode:
Diffstat (limited to 'qemu-headless/make-vm-debootstrap-efi.py')
-rwxr-xr-xqemu-headless/make-vm-debootstrap-efi.py365
1 files changed, 365 insertions, 0 deletions
diff --git a/qemu-headless/make-vm-debootstrap-efi.py b/qemu-headless/make-vm-debootstrap-efi.py
new file mode 100755
index 0000000..15654f4
--- /dev/null
+++ b/qemu-headless/make-vm-debootstrap-efi.py
@@ -0,0 +1,365 @@
+#!/usr/bin/python3
+
+import os
+import sys
+import glob
+import subprocess
+import tempfile
+import shutil
+import re
+import functools
+
+
+image = 'unstable.qcow2'
+
+
+parameters = dict(
+ release='unstable',
+ #mirror='http://fr.archive.ubuntu.com/ubuntu/',
+ mirror='http://apt:9999/debian/',
+ arch='amd64',
+ packages='''
+ apt
+ aptitude
+ bash
+ bash-completion
+ bind9-host
+ bmon
+ busybox
+ bzip2
+ curl
+ ed
+ grub2
+ htop
+ iftop
+ ifupdown
+ iotop
+ iperf
+ iproute2
+ iptables
+ iputils-ping
+ less
+ lftp
+ linux-image-amd64
+ locales
+ ncdu
+ ncurses-base
+ ncurses-term
+ net-tools
+ netbase
+ netcat
+ nload
+ openssh-client
+ openssh-server
+ psmisc
+ python3
+ ranger
+ rsync
+ screen
+ sed
+ socat
+ strace
+ tar
+ tcpdump
+ telnet
+ tmux
+ tree
+ tzdata
+ vim
+ vim-nox
+ vim-runtime
+ w3m
+ wget
+ zsh
+ '''.split(),
+)
+
+
+open8 = functools.partial(open, encoding='utf8')
+numerical_sort = lambda y: [int(x) if x.isdigit() else x
+ for x in re.split('(\d+)', y)]
+
+
+def mlstrip(s):
+ return re.sub(r'^\s*', '', s, flags=re.MULTILINE)
+
+
+def run(*l, **kw):
+ print('run:', *l)
+ return subprocess.run(*l, **kw)
+
+
+def reexec_root():
+ # run as root
+ if os.geteuid() != 0:
+ os.execvp("sudo", ["sudo"] + sys.argv)
+
+
+def system_customization(rootdir, bootpartuuid, rootpartuuid, rootfsuuid):
+ os.unlink(rootdir + '/etc/localtime')
+ open8(rootdir + '/etc/zoneinfo', 'w').write('Europe/Paris\n')
+ shutil.copy(rootdir + '/usr/share/zoneinfo/Europe/Paris',
+ rootdir + '/etc/localtime')
+
+ open8(rootdir + '/etc/network/interfaces', 'w').write(mlstrip(
+ '''\
+ auto lo
+ iface lo inet loopback
+
+ auto eth0
+ iface eth0 inet dhcp
+ '''))
+ open8(rootdir + '/etc/hosts', 'w').write(mlstrip(
+ '''\
+ 127.0.0.1 localhost localhost.localdomain debian
+
+ # the following lines are desirable for IPv6 capable hosts
+ ::1 localhost ip6-localhost ip6-loopback
+ ff02::1 ip6-allnodes
+ ff02::2 ip6-allrouters
+ '''))
+ open8(rootdir + '/etc/hostname', 'w').write('debian\n')
+ open8(rootdir + '/etc/fstab', 'w').write(mlstrip(
+ '''\
+ # <device> <mount point> <type> <options> <dump> <pass>
+ PARTUUID={rid} / ext4 errors=remount-ro,relatime 0 1
+ PARTUUID={bid} /boot/firmware vfat errors=remount-ro,relatime 0 2
+ '''.format(bid=bootpartuuid,
+ rid=rootpartuuid)))
+
+ # activate serial console
+ run([
+ 'ln',
+ '-s',
+ '/lib/systemd/system/serial-getty@.service',
+ rootdir + '/etc/systemd/system/getty.target.wants/serial-getty@ttyS0.service',
+ ], check=True)
+ os.makedirs(rootdir + '/etc/systemd/system/serial-getty@ttyS0.service.d',
+ mode=0o755)
+ open8(rootdir +
+ '/etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf',
+ 'w').write(mlstrip(
+ '''\
+ [Service]
+ ExecStart=
+ ExecStart=-/sbin/agetty --autologin root --login-pause --noclear %I 115200,38400,9600 vt102
+ '''))
+
+ script_dir = os.path.dirname(os.path.realpath(__file__))
+ os.makedirs(rootdir + '/boot/firmware/efi/boot', mode=0o755)
+ with tempfile.NamedTemporaryFile(mode='w', encoding='utf8') as f:
+ f.write(mlstrip(
+ '''\
+ search --set=root --no-floppy --fs-uuid {rid} --hint hd0,gpt2
+ configfile /boot/grub/grub.cfg
+ '''.format(rid=rootfsuuid)))
+ f.flush()
+ run([
+ script_dir + '/grub-2.02~beta3/grub-mkimage',
+ '-d', script_dir + '/grub-2.02~beta3/grub-core',
+ '-O', 'x86_64-efi',
+ '--output={}/boot/firmware/efi/boot/bootx64.efi'.format(rootdir),
+ '--prefix=/efi/boot',
+ '--config=' + f.name,
+ ] + list(glob.glob(script_dir + '/grub-2.02~beta3/grub-core/*.mod')),
+ check=True)
+
+ open8(rootdir + '/etc/default/keyboard', 'w').write(mlstrip(
+ '''\
+ XKBMODEL="pc105"
+ XKBLAYOUT="fr"
+ XKBVARIANT="bepo"
+ XKBOPTIONS=""
+ '''))
+
+ open8(rootdir + '/etc/default/locale', 'w').write(mlstrip(
+ '''\
+ LANG="en_US.UTF-8"
+ LC_TIME="en_DK.UTF-8"
+ LC_PAPER="en_GB.UTF-8"
+ LC_MEASUREMENT="en_GB.UTF-8"
+ '''))
+
+ for locale in ['fr_FR', 'en_US', 'en_GB', 'en_DK', 'de_DE']:
+ run([
+ 'localedef',
+ '--prefix={}'.format(rootdir),
+ '-f', 'UTF-8',
+ '-i', locale,
+ '{}.UTF-8'.format(locale)
+ ], check=True)
+
+ open8(rootdir + '/etc/bash.bashrc', 'a').write(mlstrip(
+ '''\
+
+ # enable bash completion in interactive shells
+ if ! shopt -oq posix; then
+ if [ -f /usr/share/bash-completion/bash_completion ]; then
+ . /usr/share/bash-completion/bash_completion
+ elif [ -f /etc/bash_completion ]; then
+ . /etc/bash_completion
+ fi
+ fi
+ alias ls="ls --color=aut"
+ alias l="ls -CF"
+ alias ll="l -lh"
+ alias la="l -a"
+ alias e="vim"
+ alias rm='rm -i'
+ alias cp='cp -i'
+ alias mv='mv -i'
+ export PAGER=less
+ export EDITOR=vim
+ export VISUAL=vim
+ '''))
+
+ open8(rootdir + '/etc/vim/vimrc', 'w').write(mlstrip(
+ '''\
+ set nocompatible
+ filetype plugin indent on
+ set autoindent
+ set background=dark
+ set backspace=2
+ set hidden
+ set hlsearch
+ set ignorecase
+ set incsearch
+ set laststatus=2
+ set modelines=0
+ set nobackup
+ set nowritebackup
+ set ruler
+ set scrolloff=3
+ set shiftwidth=4
+ set showcmd
+ set showmatch
+ set statusline=%<%f%h%m%r%=%l,%c\ %P
+ set ts=4
+ set whichwrap=<,>,[,]
+ set wildmode=list:full
+ syntax on
+ '''))
+
+ open8(rootdir + '/etc/default/grub', 'w').write(mlstrip(
+ '''\
+ GRUB_DEFAULT=0
+ GRUB_TIMEOUT=10
+ GRUB_DISTRIBUTOR=Debian
+ GRUB_TERMINAL=console
+ GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"
+ GRUB_CMDLINE_LINUX_DEFAULT=""
+ GRUB_CMDLINE_LINUX="text console=ttyS0,115200n8 console=tty0 net.ifnames=0"
+ '''))
+
+ kernel_params = 'ro text console=ttyS0,115200n8'
+ kernel_params += ' console=tty0 net.ifnames=0'
+ open8(rootdir + '/boot/grub/grub.cfg', 'w').write(mlstrip(
+ '''\
+ terminal_output console
+ serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
+ set default=0
+ set timeout=3
+ menuentry 'default' {{
+ search --set=root --file /boot/grub/grub.cfg --hint hd0,gpt2
+ linux /vmlinuz root=PARTUUID={rid} {kp}
+ initrd /initrd.img
+ }}
+ '''.format(rid=rootpartuuid, kp=kernel_params)))
+
+
+def make_image():
+ device = None
+ rootdirobj = tempfile.TemporaryDirectory()
+ rootdir = rootdirobj.name
+ try:
+ run(['qemu-img', 'create', '-f', 'qcow2', image, '10G'], check=True)
+
+ run(['modprobe', 'nbd', 'max_part=16'])
+
+ for d in sorted(glob.glob('/dev/nbd*'), key=numerical_sort):
+ if run(['qemu-nbd', '-c', d, image]).returncode == 0:
+ device = d
+ break
+ else:
+ print('No free nbd device found for qemu-nbd')
+ raise SystemExit(1)
+
+ run([
+ 'sgdisk',
+ '--set-alignment=8192',
+ '--zap-all',
+ '--new=1:4M:+300M',
+ '--change-name=1:boot',
+ '--typecode=1:EF00',
+ '--largest-new=2',
+ '--change-name=2:root',
+ '--typecode=2:8300',
+ device,
+ ], check=True)
+
+ run(['partprobe', device])
+ run(['mkfs.fat', '-n', 'boot', device+'p1'], check=True)
+ run(['mkfs.ext4', '-q', '-L', 'root', device+'p2'], check=True)
+ run(['mount', device+'p2', rootdir], check=True)
+ os.makedirs(rootdir + '/boot/firmware', mode=0o755)
+ run(['mount', device+'p1', rootdir+'/boot/firmware'], check=True)
+
+ run([
+ 'qemu-debootstrap',
+ '--arch=' + parameters['arch'],
+ '--include=' + ','.join(parameters['packages']),
+ '--components=main,universe',
+ parameters['release'],
+ rootdir,
+ parameters['mirror'],
+ ], check=True)
+
+ rootpartuuid = (run([
+ 'partx',
+ '--noheadings',
+ '--show',
+ '--output',
+ 'UUID',
+ device+'p2'], check=True, stdout=subprocess.PIPE)
+ .stdout
+ .decode('utf8')
+ .strip())
+ bootpartuuid = (run([
+ 'partx',
+ '--noheadings',
+ '--show',
+ '--output',
+ 'UUID',
+ device+'p1'], check=True, stdout=subprocess.PIPE)
+ .stdout
+ .decode('utf8')
+ .strip())
+
+ rootfsuuid = [line.split()[-1]
+ for line in (run([ 'tune2fs', '-l', device+'p2'],
+ check=True, stdout=subprocess.PIPE)
+ .stdout
+ .decode('utf8')
+ .splitlines())
+ if "filesystem uuid" in line.lower()][0]
+
+ system_customization(rootdir, bootpartuuid, rootpartuuid, rootfsuuid)
+
+ finally:
+ if rootdir:
+ cmd = ['umount'] + \
+ ['{rootdir}/{}'.format(x, rootdir=rootdir)
+ for x in 'proc sys dev boot/firmware'.split()]
+ run(cmd)
+ run(['umount', rootdir])
+ if device:
+ run(['qemu-nbd', '-d', device])
+
+
+def main():
+ reexec_root()
+ make_image()
+
+
+if __name__ == '__main__':
+ main()