#!/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( '''\ # 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()