#!/bin/bash

# create a VM image using debootstrap
#
# Copyright 2013, 2014, 2015 Peter Palfrader <peter@palfrader.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
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful, but
#   WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#   General Public License for more details.
#
#   You probably already have access to a copy of the GNU General Public
#   License or you can find one on the Internet; if not, write to the
#   copyright holder or to the Free Software Foundation, Inc., 59 Temple
#   Place, Suite 330, Boston, MA 02111-1307 USA.

set -e
set -u

SUITE=jessie
hostlistfile=/root/VMs/host-list

MIRROR=http://ftp.at.debian.org/debian
KEYRING=/usr/share/keyrings/debian-archive-keyring.gpg
prefixlen=26
gateway=213.235.231.193
ip6prefix=2001:858:10f:100::
ip6gateway=fe80::
basedir=/srv/vmstore

check_installed() {
  local p
  for p in "$@"; do
    if ! dpkg -l "$p" 2>/dev/null | grep -q '^ii'; then
      echo >&2 "Error: package $p not installed:"
      echo >&2 "  apt-get install $*"
      exit 1
    fi
  done
}
get_sshkey_fprs() {
  local f
  for f in etc/ssh/ssh_host*_key.pub; do
    echo -n "  "
    ssh-keygen -l -f "$f"
  done
  echo
  for f in etc/ssh/ssh_host*_key.pub; do
    echo -n "  "
    ssh-keygen -r '' -f "$f"
  done
}

do_cleanup() {
  #echo "Proposed cleanup:"
  local cnt
  cnt=$((${#cleanup[*]}-1))
  #for i in $(seq ${cnt} -1 0); do
  #  echo "  ${cleanup[$i]}"
  #done
  #echo "Press enter to commence cleanup."
  #read
  for i in $(seq ${cnt} -1 0); do
    echo "* ${cleanup[$i]}"
    eval "${cleanup[$i]}" || true
  done
  echo "done."
}

if [ -e /etc/debian_version ]; then
  check_installed debootstrap debian-archive-keyring kpartx
else
  test -e `which debootstrap >/dev/null 2>&1` || echo "W: could not find debootstrap binary"
  test -e `which kpartx >/dev/null 2>&1` || echo "W: could not find kpartx binary"
  test -e $KEYRING || echo "W: could not find keyring in $KEYRING"
fi

declare -a cleanup
cleanup+=(":")
trap do_cleanup EXIT

echo -n "New VM's FQDN: "
if [ -n "${1:-}" ]; then echo "$1"; fqdn="$1"; shift; else read fqdn; fi
echo
guest="${fqdn%%.*}"
domainname="${fqdn#*.}"

echo -n "Disk size: [10g]"
if [ -n "${1:-}" ]; then echo "$1"; disksize="$1"; shift; else read disksize; fi
disksize=${disksize:-10g}

echo -n "Swap size: [4g]"
if [ -n "${1:-}" ]; then echo "$1"; swapsize="$1"; shift; else read swapsize; fi
swapsize=${swapsize:-4g}

echo -n "Lvm size: [20g]"
if [ -n "${1:-}" ]; then echo "$1"; lvmsize="$1"; shift; else read lvmsize; fi
lvmsize=${lvmsize:-20g}
if [ "$lvmsize" = "0" ]; then lvmsize=""; fi

echo -n "ipaddr: ";
if [ -n "${1:-}" ]; then echo "$1"; ipaddr="$1"; shift; else read ipaddr; fi

echo -n "unique hostno (2-254): ";
if [ -n "${1:-}" ]; then echo "$1"; hostno="$1"; shift; else read hostno; fi

if awk -v v="$hostno" '$1 == v {print}' "$hostlistfile" | grep .; then
  echo >&2 "Host number $hostno already used in $hostlistfile."
  exit 1
fi
if awk -v v="$ipaddr" '$2 == v {print}' "$hostlistfile" | grep .; then
  echo >&2 "IP address $ipaddr already used in $hostlistfile."
  exit 1
fi
if awk -v v="$fqdn" '$3 == v {print}' "$hostlistfile" | grep .; then
  echo >&2 "FQDN $fqdn already used in $hostlistfile."
  exit 1
fi

diskfileprefix="$basedir/$fqdn/"
if [ -e "$diskfileprefix" ]; then
  echo >&2 "Error: Disk directory $diskfileprefix already exists."
  exit 1
fi

target="/mnt/target-$guest"
if [ -e "$target" ]; then
  echo >&2 "Error: Directory $target already exists."
  exit 1
fi


diskfileroot="${diskfileprefix}$guest-root"
diskfileswap="${diskfileprefix}$guest-swap"
diskfilelvm="${diskfileprefix}$guest-lvm"

mkdir "$diskfileprefix"
(umask 077 && qemu-img create -f qcow2 "$diskfileroot" "$disksize")
(umask 077 && qemu-img create -f raw   "$diskfileswap" "$swapsize")
if [ "$lvmsize" != "" ] ; then
  (umask 077 && qemu-img create -f qcow2 "$diskfilelvm"  "$lvmsize")
fi

modprobe nbd max_part=63

rootdev=/dev/nbd0
lvmdev=/dev/nbd1

qemu-nbd -c "$rootdev" "$diskfileroot"
cleanup+=("qemu-nbd -d '$rootdev'")

if [ "$lvmsize" != "" ] ; then
  qemu-nbd -c "$lvmdev" "$diskfilelvm"
  cleanup+=("qemu-nbd -d '$lvmdev'")
fi

if [ "$(head -c 65536 "$rootdev" | sha1sum | awk '{print $1}')" != "1adc95bebe9eea8c112d40cd04ab7a8d75c4f961" ]; then
  echo -n "Warning: Disk appears to be not be empty.  Continue anyway? [y/N] "
  read ans
  [ "$ans" = "y" ] || exit 0
fi

ip6addr="$ip6prefix$hostno:1"

echo '2048,,L,*' | sfdisk -u S --Linux "$rootdev"
kpartx -v -p "-p" -a "$rootdev" -s
cleanup+=("kpartx -d -p '-p' -v '$rootdev'")
part1="/dev/mapper/$(basename $rootdev)-p1"
mkfs.ext4 "$part1"

if [ "$lvmsize" != "" ] ; then
  pvcreate "$lvmdev"
  vgcreate "vg_$guest" "$lvmdev"
fi

mkdir "$target"
cleanup+=("rmdir '$target'")
mount "$part1" "$target"
cleanup+=("umount '$target'")
cd "$target"
cleanup+=("cd /")

debootstrap --variant=minbase --keyring="$KEYRING" "$SUITE" . "$MIRROR"

### Set up swap and fstab
mkswap "$diskfileswap"

uuidroot=$(blkid -s UUID -o value ${part1}) &&
uuidswap=$(blkid -s UUID -o value $diskfileswap) &&
cat > etc/fstab << EOF
UUID=$uuidroot / ext4 errors=remount-ro 0 1
UUID=$uuidswap none swap sw 0 0
tmpfs /tmp tmpfs defaults,size=512m 0 0
EOF

### Set up basic networking stuff
echo "$guest" > etc/hostname
cat > etc/hosts << EOF
127.0.0.1 localhost
$ipaddr $fqdn $guest

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
EOF
rm -fv etc/udev/rules.d/70-persistent-*
mkdir -p etc/udev/rules.d/
touch etc/udev/rules.d/75-persistent-net-generator.rules

cat > etc/network/interfaces << EOF
auto lo
iface lo inet loopback

allow-hotplug eth0
iface eth0 inet static
    pre-up echo 0 > /proc/sys/net/ipv6/conf/\$IFACE/accept_ra
    address $ipaddr/$prefixlen
    gateway $gateway

allow-hotplug eth1
iface eth1 inet static
    address 172.22.130.$hostno/23
iface eth1 inet6 static
    address $ip6addr/64
    gateway $ip6gateway
    accept_ra 0
EOF

cat > etc/resolv.conf << EOF
nameserver 172.22.130.1
search $domainname
EOF

### A couple packages
mv etc/apt/sources.list etc/apt/sources.list.d/debian.list
echo "deb http://security.debian.org/ jessie/updates main" > etc/apt/sources.list.d/security.list
echo "deb $MIRROR jessie-updates main" > etc/apt/sources.list.d/updates.list
chroot . apt-get update
echo "Apt::Install-Recommends 0;" > etc/apt/apt.conf.d/local-recommends
chroot . apt-get install -y locales-all net-tools iproute ifupdown dialog vim netbase udev psmisc usbutils pciutils
chroot . apt-get install -y iputils-ping telnet bind9-host lvm2
sed  -i -e 's/issue_discards = 0/issue_discards = 1/' etc/lvm/lvm.conf

### Set up kernel and bootloader
chroot . apt-get install -y linux-image-amd64
DEBIAN_FRONTEND=noninteractive chroot . apt-get install -y grub2

! [ -e dev/sda ]
! [ -e dev/sda1 ]
cp -av `readlink -f "$rootdev"` dev/new-root
cp -av `readlink -f "$part1"` dev/new-root1
chroot . grub-install --modules=part_msdos /dev/new-root
rm -v dev/new-root*

cp -av `readlink -f "$rootdev"` dev/sda
cp -av `readlink -f "$part1"` dev/sda1
rm -f boot/grub/device.map
sed -i -e 's/^GRUB_CMDLINE_LINUX=""$/GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8"/' etc/default/grub
cat >> etc/default/grub <<EOF
GRUB_TERMINAL="serial console"
GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"
EOF
chroot . update-grub
rm -v dev/sda*

rootpw="$(head -c 12 /dev/urandom | base64)"
echo "root:$rootpw" | chroot . chpasswd

### install ssh
chroot . apt-get install -y ssh
sed -i -e "s/`hostname`\$/$guest/" etc/ssh/ssh_host*_key.pub
sshkeys="$(get_sshkey_fprs)"
rsahostkey="$(cat etc/ssh/ssh_host_rsa_key.pub)"
[ -e etc/ssh/ssh_host_ed25519_key.pub ] && ed25519hostkey="$(cat etc/ssh/ssh_host_ed25519_key.pub)"
mkdir -p root/.ssh
cp /root/.ssh/authorized_keys root/.ssh

### unattended upgrades
chroot . apt-get install -y unattended-upgrades
cat > etc/apt/apt.conf.d/20auto-upgrades << EOF
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF

## trim
mkdir -p etc/cron.daily
tee > etc/cron.daily/local-trim-ext4 << 'EOF'
#!/bin/sh

# by weasel

for fs in `awk '$9 == "ext4" && $4 == "/" {print $5}' /proc/self/mountinfo`; do
  fstrim "$fs"
done
EOF
chmod +x etc/cron.daily/local-trim-ext4

### first boot
cat > first-boot.sh << EOF
#!/bin/bash
set -e
sleep 10
apt-get install -y acpi-support-base lldpd
apt-get install -y libpam-systemd dbus
apt-get install -y cron logrotate
apt-get install -y rsyslog
sed -i -e '/first-boot.sh/d' etc/rc.local
/etc/cron.daily/local-trim-ext4
rm /first-boot.sh
(sleep 10 ; /sbin/reboot) &
disown %1
EOF
chmod +x first-boot.sh
sed -i -e '$ i [ -x /first-boot.sh ] && /first-boot.sh' etc/rc.local


### clean up
chroot . apt-get clean

# prepare virsh file
( : #)
cat << EOF
<domain type='kvm'>
  <name>$guest</name>
  <uuid>$(uuidgen)</uuid>
  <memory unit='MiB'>2048</memory>
  <vcpu placement='static'>2</vcpu>
  <os>
    <type arch='x86_64' machine='pc-i440fx-2.1'>hvm</type>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <pae/>
  </features>
  <cpu mode='custom' match='exact'>
    <model fallback='allow'>SandyBridge</model>
  </cpu>
  <clock offset='utc'>
    <timer name='rtc' tickpolicy='catchup'/>
    <timer name='pit' tickpolicy='delay'/>
    <timer name='hpet' present='no'/>
  </clock>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>restart</on_crash>
  <pm>
    <suspend-to-mem enabled='no'/>
    <suspend-to-disk enabled='no'/>
  </pm>
  <devices>
    <emulator>/usr/bin/kvm</emulator>
    <controller type='scsi' index='0' model='virtio-scsi'/>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' discard='unmap'/>
      <source file='$diskfileroot'/>
      <target dev='sda' bus='scsi'/>
    </disk>
    <disk type='file' device='disk'>
      <driver name='qemu' type='raw' discard='unmap'/>
      <source file='$diskfileswap'/>
      <target dev='sdb' bus='scsi'/>
    </disk>
EOF
[ "$lvmsize" = "" ] || cat << EOF
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' discard='unmap'/>
      <source file='$diskfilelvm'/>
      <target dev='sdc' bus='scsi'/>
    </disk>
EOF
cat << EOF
    <interface type='bridge'>
      <mac address='CC:7F:AE:65:$(printf "%02x" $hostno):01'/>
      <source bridge='br-inet'/>
      <virtualport type='openvswitch'/>
      <model type='virtio'/>
    </interface>
    <interface type='bridge'>
      <mac address='CC:7F:AE:65:$(printf "%02x" $hostno):02'/>
      <source bridge='br-vmint'/>
      <virtualport type='openvswitch'/>
      <model type='virtio'/>
    </interface>
    <serial type='pty'>
      <target port='0'/>
    </serial>
    <console type='pty'>
      <target type='serial' port='0'/>
    </console>
    <input type='mouse' bus='ps2'/>
    <input type='keyboard' bus='ps2'/>
    <graphics type='vnc' port='-1' autoport='yes'/>
    <memballoon model='virtio'/>
  </devices>
</domain>
EOF
) > "$diskfileprefix/$guest.xml"

# and done
trap - EXIT
do_cleanup

echo "$guest's root password is $rootpw"
echo "SSH host key fingerprints are:"
echo "$sshkeys"
echo "IP addresses:"
echo "  $ipaddr"
echo "  $ip6addr"
echo
echo "Maybe run"
echo "  echo '$hostno' '$ipaddr' '$fqdn' >> '$hostlistfile'"
echo "  virsh define '$diskfileprefix/$guest.xml' && rm '$diskfileprefix/$guest.xml'"
echo "  virsh autostart '$guest'"
echo "  virsh start '$guest'"
echo "  virsh console '$guest'"
echo
echo '  for i in `/usr/local/sbin/vm_du_ suggest`; do ln -vsf /usr/local/sbin/vm_du_ /etc/munin/plugins/vm_du_$i; done'
echo '  service munin-node restart; sleep 3; service munin-async restart'