#!/bin/bash

# lxc template for debootstrapping in userns

# Authors:
# Brett Parker <iDunno@sommitrealweird.co.uk>

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library 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
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

MAPPED=no

# Only support usage in userns.
for arg in "$@"; do
    [ "$arg" = "--" ] && break
    if [ "$arg" = "--mapped-uid" -o "$arg" = "--mapped-gid" ]; then
        MAPPED=yes
    fi
done

if [ "$MAPPED" == "no" ]; then
    echo "This template can only be used for unprivileged containers." 1>&2
    echo "You might want the \"debian\" template instead." 1>&2
    exit 1
fi

set -e
set -u

# Make sure the usual locations are in PATH
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:$PATH
export GREP_OPTIONS=""

usage() {
    cat <<EOF
LXC debootstrap in user namespace for unprivileged containers

Special arguments:
[ -h | --help ]: Print this help message and exit.

Required arguments:
[ -r | --release <release> ]: The debian release, e.g. jessie or stretch.

Optional arguments:
[ -m | --mirror <mirrorurl> ]: The debian mirror to user
[ -n | --network <networkspec> ]: How to configure networking

Network spec:
  <type>[,options]

  type is one of:
    dhcp4: v4 dhcp will be enabled.
    dhcp6: v6 dhcp will be enabled.
    dhcp: v4 and v6 dhcp will be enabled.
    static: no dhcp will be enabled

  options:
    this is a , seperated list, and sets up static assignments for v4 or v6,
    regardless of type.
    The order of arguments is
      staticv4/staticnetmask
      staticv6/staticv6netmask
      gateway
      v6gateway

  examples:

    --network dhcp4 (default)
    --network dhcp6 (v6 dhcp)
    --network dhcp  (v4 and v6 dhcp)
    --network dhcp4,,2001:db8:1234:5678::1/64 (dhcp4 and static v6 address)
    --network static,,2001:db8:1234:5678::5/64,,fe80::1 (static v6)
    --network static,192.0.2.15/24,,192.0.2.1 (static v4)

EOF
    return 0
}

options=$(getopt -o r:m:n:h -l release:,mirror:,network:,help,mapped-uid:,mapped-gid:,name:,path:,rootfs: -- "$@")

if [ $? -ne 0 ]; then
    usage
    exit 1
fi

eval set -- "$options"

DEBIAN_MIRROR="http://mirror.mythic-beasts.com/debian/"
DEBIAN_RELEASE="jessie"
NETWORK_CONFIG=""

disable_initscripts() {
    cat <<EOF > "${LXC_ROOTFS}/usr/sbin/policy-rc.d"
#!/bin/sh

exit 101
EOF
    chmod 755 "${LXC_ROOTFS}/usr/sbin/policy-rc.d"
}

enable_initscripts() {
    if [ -e "${LXC_ROOTFS}/usr/sbin/policy-rc.d" ]; then
        rm "${LXC_ROOTFS}/usr/sbin/policy-rc.d"
    fi
}

while :; do
    case "$1" in
        -h|--help)      usage && exit 1;;
        -r|--release)   DEBIAN_RELEASE="$2"; shift 2;;
        -m|--mirror)    DEBIAN_MIRROR="$2"; shift 2;;
        -n|--network)   NETWORK_CONFIG="$2"; shift 2;;
        --mapped-uid)   MAPPED_UID="$2"; shift 2;;
        --mapped-gid)   MAPPED_GID="$2"; shift 2;;
        --name)         LXC_NAME="$2"; shift 2;;
        --path)         LXC_PATH="$2"; shift 2;;
        --rootfs)       LXC_ROOTFS="$2"; shift 2;;
        *)              break;;
    esac
done

INTERFACE_DEFAULTS="auto eth0
iface eth0 inet dhcp"

generate_network_config() {
    if [ "$NETWORK_CONFIG" == "" ]; then
        echo "$INTERFACE_DEFAULTS"
        return 0
    fi

    ETH0_HEADER="auto eth0"
    ETH0_IPV4=""
    ETH0_IPV6=""

    echo "auto eth0"
    # see if there's a type
    network_type=${NETWORK_CONFIG/,*}
    other_params=${NETWORK_CONFIG#*,}

    v4_configured=no
    v6_configured=no

    if [ "$network_type" == "dhcp" ]; then
        ETH0_IPV4="iface eth0 inet dhcp"
        ETH0_IPV6="iface eth0 inet6 dhcp"
        v4_configured=yes
        v6_configured=yes
    elif [ "$network_type" == "dhcp4" ]; then
        ETH0_IPV4="iface eth0 inet dhcp"
        v4_configured=yes
    elif [ "$network_type" == "dhcp6" ]; then
        ETH0_IPV6="iface eth0 inet6 dhcp"
        v6_configured=yes
    elif [ "$network_type" != "static" ]; then
        echo "Unknown network type: $network_type" 1>&2
        echo 1>&2
        usage 1>&2
        exit 1
    fi

    [ "$network_type" == "$other_params" ] && return 0

    v4_static=${other_params/,*}
    other_params=${other_params#*,}

    if [ "$v4_static" != "" ]; then
        if [ "$v4_configured" == "yes" ]; then
            echo "Both v4 DHCP and Static - giving up." 1>&2
            echo 1>&2
            usage 1>&2
            exit 1
        fi
    fi

    if [ "$v4_static" == "$other_params" ]; then
        if [ "$v4_static" != "" ]; then
            echo "iface eth0 inet static"
            echo "  address $v4_static"
        fi
    fi

    v6_static=${other_params/,*}
    other_params=${other_params#*,}

    if [ "$v6_static" != "" ]; then
        if [ "$v6_configured" == "yes" ]; then
            echo "Both v6 DHCP and Static - giving up." 1>&2
            echo 1>&2
            usage 1>&2
            exit 1
        fi
    fi

    if [ "$v6_static" == "$other_params" ]; then
        if [ "$v4_static" ]; then
            echo "iface eth0 inet static"
            echo "  address $v4_static"
            echo
        fi

        echo "iface eth0 inet6 static"
        echo "  address $v6_static"

        return 0
    fi

    v4_gateway=${other_params/,*}
    other_params=${other_params#*,}

    if [ "$v4_gateway" == "$other_params" ]; then
        if [ "$v4_static" != "" ]; then
            echo "iface eth0 inet static"
            echo "  address $v4_static"
            [ "$v4_gateway" != "" ] && echo "  gateway $v4_gateway"

            if [ "$v6_static" != "" ]; then
                echo "iface eth0 inet6 static"
                echo "  address $v6_static"
            fi

            return 0
        fi

        if [ "$v4_configured" == "yes" ]; then
            echo "DHCP and static gateway not supported, giving up." 1>&2
            echo 1>&2
            usage 1>&2
            exit 1
        fi
    fi

    v6_gateway=${other_params/,*}

    if [ "$v4_static" != "" ]; then
        echo "iface eth0 inet static"
        echo "  address $v4_static"
        [ "$v4_gateway" != "" ] && echo "  gateway $v4_gateway"
        echo
    fi

    if [ "$v6_static" != "" ]; then
        echo "iface eth0 inet6 static"
        echo "  address $v6_static"
        [ "$v6_gateway" != "" ] && echo "  gateway $v6_gateway"
    fi

    return 0
}

INTERFACE_DETAILS="$(generate_network_config)"

# rewrite the default config file

sed -i -e "/lxc./{w ${LXC_PATH}/config-auto" -e "d}" "${LXC_PATH}/config"
sed -i -e '4,$d' "${LXC_PATH}/config"

cat <<EOF >> "${LXC_PATH}/config"

# Useful includes
lxc.include = /usr/share/lxc/config/debian.common.conf
lxc.include = /usr/share/lxc/config/debian.userns.conf

# Set our hostname
lxc.utsname = $LXC_NAME

# Automatic configuration
EOF

# add back in the auto foo
cat "${LXC_PATH}/config-auto" >> "${LXC_PATH}/config"
rm "${LXC_PATH}/config-auto"

mkdir "${LXC_PATH}/bin"
cat <<EOF > "${LXC_PATH}/bin/mknod"
#!/bin/sh

# look for the first argument that looks like a path
for i do
    case \$i in
        /*)
            exec touch "\$i"
            ;;
    esac
done

EOF

chmod 755 "${LXC_PATH}/bin/mknod"

export PATH="${LXC_PATH}/bin:$PATH"

debootstrap --foreign --include debian-archive-keyring,ifupdown,isc-dhcp-client,locales,openssh-server $DEBIAN_RELEASE "${LXC_ROOTFS}" $DEBIAN_MIRROR

echo "DEBOOTSTRAP STAGE 1 COMPLETE"

# now totally skip that check in the new root, because it sucks.
sed -i -e 's#check_sane_mount () {#check_sane_mount () {\n\treturn 0#;' "${LXC_ROOTFS}/debootstrap/functions"

# and stop it from bothering to try to setup proc
sed -i -e 's#setup_proc () {#setup_proc () {\n\treturn 0#;' "${LXC_ROOTFS}/debootstrap/functions"

keyring_dpkg=$(sed -ne "/^debian-archive-keyring/ { s#.* ##; p; }" "${LXC_ROOTFS}/debootstrap/debpaths")
# and unpack debian-archive-keyring, because we'll need that
(cd "${LXC_ROOTFS}" && dpkg-deb -x ".$keyring_dpkg" .)

# replace the tar containing devices with something that doesn't contain any
(cd "$LXC_ROOTFS/debootstrap" && rm devices.tar.gz && tar czvf devices.tar.gz --files-from=/dev/null)

# and mount a shitload of things for fun and profit...
for file in /var/lib/lxcfs/proc/*; do
    fname="$(basename $file)"
    touch "${LXC_ROOTFS}/proc/$fname"
    mount -n -o bind "$file" "${LXC_ROOTFS}/proc/$fname"
done

for dev in null random urandom; do
    touch "${LXC_ROOTFS}/dev/$dev"
    mount -n -o bind /dev/$dev "${LXC_ROOTFS}/dev/$dev"
done

# set /proc/cmdline to something
echo "debootstrapping" > "${LXC_ROOTFS}/proc/cmdline"

# and disable initscripts
disable_initscripts

# and run the second stage
chroot "${LXC_ROOTFS}" /debootstrap/debootstrap --second-stage

# make sure that initscripts are still disabled
disable_initscripts

# configure locales
lang=en_GB.UTF-8
enc=UTF-8
if [ ! -z "$LANG" ]; then
    lang=${LANG}
    enc=${LANG#*.}
fi

cat >> "${LXC_ROOTFS}/etc/locale.gen" <<EOF
$lang $enc
EOF

chroot "${LXC_ROOTFS}" /usr/sbin/locale-gen $lang $enc
chroot "${LXC_ROOTFS}" /usr/sbin/update-locale LANG=$LANG

# configure timezone
if [ -f /etc/timezone ]; then
    cat /etc/timezone > "${LXC_ROOTFS}/etc/timezone"
elif [ -f /etc/sysconfig/clock ]; then
    . /etc/sysconfig/clock
    echo $ZONE > "${LXC_ROOTFS}/etc/timezone"
fi
chroot "${LXC_ROOTFS}" dpkg-reconfigure -f noninteractive tzdata

# "setup" networking
NETWORK_FILE=/etc/network/interfaces
if [ -e "${LXC_ROOTFS}/etc/network/interfaces.d" ]; then
    NETWORK_FILE=/etc/network/interfaces.d/eth0
fi

# remove some interesting breakages in pam for unpriv foo
sed -i -e 's#^\(session.*required.*pam_loginuid.so\)#\#\1#;' "${LXC_ROOTFS}"/etc/pam.d/*

# set the hostname
echo $LXC_NAME > "${LXC_ROOTFS}/etc/hostname"

SECURITY=""
if [ "$DEBIAN_RELEASE" != "sid" ] && [ "$DEBIAN_RELEASE" != "unstable" ]; then
    SECURITY="deb http://security.debian.org/ $DEBIAN_RELEASE/updates main"
fi

# setup sources.list
cat <<EOF > "${LXC_ROOTFS}/etc/apt/sources.list"
deb $DEBIAN_MIRROR $DEBIAN_RELEASE main
$SECURITY
EOF

# disable bits of systemd / initrd that break things
chroot "${LXC_ROOTFS}" /usr/sbin/update-rc.d -f checkroot.sh disable > /dev/null 2>&1 || true
chroot "${LXC_ROOTFS}" /usr/sbin/update-rc.d -f umountfs disable > /dev/null 2>&1 || true
chroot "${LXC_ROOTFS}" /usr/sbin/update-rc.d -f hwclock.sh disable > /dev/null 2>&1 || true
chroot "${LXC_ROOTFS}" /usr/sbin/update-rc.d -f hwclockfirst.sh disable > /dev/null 2>&1 || true

if [ -e "${LXC_ROOTFS}/etc/systemd/system/" ]; then
    touch "${LXC_ROOTFS}/etc/systemd/system/systemd-setup-dgram-qlen.service"
    touch "${LXC_ROOTFS}/etc/systemd/system/dev-hugepages.mount"
    touch "${LXC_ROOTFS}/etc/systemd/system/udev.service"
    touch "${LXC_ROOTFS}/etc/systemd/system/systemd-udevd.service"
    chroot "${LXC_ROOTFS}" systemctl set-default multi-user.target
    chroot "${LXC_ROOTFS}" ln -s /lib/systemd/system/halt.target /etc/systemd/system/sigpwr.target
fi

if [ -e "${LXC_ROOTFS}/lib/systemd/system/systemd-journald-audit.socket" ]; then
    touch "${LXC_ROOTFS}/etc/systemd/system/systemd-journald-audit.socket"
fi

echo "$INTERFACE_DETAILS" >> "${LXC_ROOTFS}${NETWORK_FILE}"

# and update to the latest security
chroot "${LXC_ROOTFS}" apt-get update
chroot "${LXC_ROOTFS}" apt-get -y upgrade

# if we're all good here, unmount things and clean up
[ -e "${LXC_ROOTFS}/usr/sbin/policy-rc.d" ] && rm "${LXC_ROOTFS}/usr/sbin/policy-rc.d"
rm "${LXC_ROOTFS}/proc/cmdline"

for dev in null random urandom; do
    umount "${LXC_ROOTFS}/dev/$dev"
    rm "${LXC_ROOTFS}/dev/$dev"
done

for file in /var/lib/lxcfs/proc/*; do
    fname="$(basename $file)"
    umount "${LXC_ROOTFS}/proc/$fname"
    rm "${LXC_ROOTFS}/proc/$fname"
done

enable_initscripts

rm -r "${LXC_PATH}/bin"

cat <<EOF

You have successfully created a new debian container, ${LXC_NAME} running ${DEBIAN_RELEASE}.

You should start the new container, and use:

  lxc-attach -n ${LXC_NAME} -- su -

To create a user account / set the root password.

Note, by default, it's likely only to be the console that can login as root, so that'd be:

  lxc-console -n ${LXC_NAME} -t 0

EOF

exit 0