#!/usr/bin/python3 import os import sys import glob import subprocess import tempfile import shutil import re import functools parameters = dict( release='unstable', #mirror='http://fr.archive.ubuntu.com/ubuntu/', mirror='http://apt:9999/debian/', arch='amd64', image = 'unstable-amd64.qcow2', packages=''' apt aptitude bash bash-completion bind9-host bmon busybox bzip2 curl ed htop iftop ifupdown iotop iperf iproute2 iptables iputils-ping less lftp 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(), kernel_package = 'linux-image-amd64', serial_console = 'ttyS0', ) 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(device, rootdir, 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 '''.format(rid=rootpartuuid))) # activate serial console on parameter['serial_console'] console = parameters['serial_console'] run([ 'ln', '-s', '/lib/systemd/system/serial-getty@.service', rootdir + '/etc/systemd/system/getty.target.wants/serial-getty@%s.service' % console, ], check=True) os.makedirs(rootdir + '/etc/systemd/system/serial-getty@%s.service.d' % console, mode=0o755) open8(rootdir + '/etc/systemd/system/serial-getty@%s.service.d/autologin.conf' % console, 'w').write(mlstrip( '''\ [Service] ExecStart= ExecStart=-/sbin/agetty --autologin root --login-pause --noclear %I 115200,38400,9600 vt102 ''')) 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 ''')) def make_image(): device = None rootdirobj = tempfile.TemporaryDirectory() rootdir = rootdirobj.name try: run(['qemu-img', 'create', '-f', 'qcow2', parameters['image'], '20G'], 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, parameters['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', '--largest-new=1', '--change-name=1:root', '--typecode=1:8300', device, ], check=True) run(['partprobe', device]) run(['mkfs.ext4', '-q', '-L', 'root', device+'p1'], check=True) run(['mount', device+'p1', rootdir], check=True) run([ 'qemu-debootstrap', '--arch=' + parameters['arch'], '--include=' + ','.join(parameters['packages'] + [parameters['kernel_package']]), '--components=main,universe', parameters['release'], rootdir, parameters['mirror'], ], check=True) rootpartuuid = (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+'p1'], check=True, stdout=subprocess.PIPE) .stdout .decode('utf8') .splitlines()) if "filesystem uuid" in line.lower()][0] system_customization(device, rootdir, rootpartuuid, rootfsuuid) finally: if rootdir: run(['umount', rootdir]) if device: run(['qemu-nbd', '-d', device]) def main(): reexec_root() make_image() if __name__ == '__main__': main()