#!/bin/bash
# vim: set ts=4 sw=4 expandtab

# Exit on error and treat unset variables as an error.
set -eu

#
# LXC template for Sabayon OS, based of Alpine script.
#

# Authors:
# Geaaru <geaaru@gmail.com>

# 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


#===========================  Constants  ============================#

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

readonly LOCAL_STATE_DIR='/var/lib'
readonly LXC_TEMPLATE_CONFIG='/usr/share/lxc/config'


# Temporary static MIRROR LIST. I will get list from online path on the near future.
readonly MIRRORS_LIST="
http://mirror.it.sabayon.org/
http://dl.sabayon.org/
ftp://ftp.klid.dk/sabayonlinux/
http://ftp.fsn.hu/pub/linux/distributions/sabayon/
http://ftp.cc.uoc.gr/mirrors/linux/SabayonLinux/
http://ftp.rnl.ist.utl.pt/pub/sabayon/
ftp://ftp.nluug.nl/pub/os/Linux/distr/sabayonlinux/
http://mirror.internode.on.net/pub/sabayon/
http://mirror.yandex.ru/sabayon/
http://sabayon.c3sl.ufpr.br/
http://mirror.clarkson.edu/sabayon/
http://na.mirror.garr.it/mirrors/sabayonlinux/"

#========================  Global variables  ========================#

# Clean variables and set defaults.
arch="$(uname -m)"
debug='no'
flush_cache='no'
mirror_url=
name=
path=
release="DAILY"
rootfs=
unprivileged=false
mapped_uid=
mapped_gid=
flush_owner=false

#========================  Helper Functions  ========================#

usage() {
    cat <<-EOF
Template specific options can be passed to lxc-create after a '--' like this:

    lxc-create --name=NAME [lxc-create-options] -- [template-options]

Template options:
    -a ARCH, --arch=ARCH   The container architecture (e.g. x86_64, armv7); defaults
                           to the host arch.
    -d, --debug            Run this script in a debug mode (set -x and wget w/o -q).
    -m URL --mirror=URL    The Sabayon mirror to use; defaults to random mirror.
    -u, --unprivileged     Tuning of rootfs for unprivileged containers.
    -r, --release          Identify release to use. Default is DAILY.
    --mapped-gid           Group Id to use on unprivileged container
                           (based of value present on file /etc/subgid).
    --mapped-uid           User Id to use on unprivileged container
                           (based of value present on file /etc/subuid)
    --flush-owner          Only for directly creation of unprivileged containers
                           through lxc-create command. Execute fuidshift command.
                           Require --mapped-gid,--mapped-uid and --unprivileged
                           options.

Environment variables:
    RELEASE                Release version of Sabayon. Default is ${RELEASE}.
EOF
}

random_mirror_url() {
    local url=""

    if [ $arch == 'amd64' ] ; then
        url=$(echo $MIRRORS_LIST | sed -e 's/ /\n/g' |  sort -R --random-source=/dev/urandom | head -n 1)
    else
        if [ $arch == 'armv7l' ] ; then
            # Currently armv7l tarball is not on sabayon mirrored tree.
            url="https://dockerbuilder.sabayon.org/"
        fi
    fi

    [ -n "$url" ] && echo "$url"
}

die() {
    local retval=$1; shift

    echo -e "==> $@\n"
    exit $retval
}

einfo() {
    echo -e "==> $@\n"
}

fetch() {
    if [ "$debug" = 'yes' ]; then
        wget -T 10 -O - $@
    else
        wget -T 10 -O - -q $@
    fi
}

parse_arch() {
    case "$1" in
        x86_64 | amd64) echo 'amd64';;
        armv7 | armv7l) echo 'armv7l';;
        #arm*) echo 'armhf';;
        *) return 1;;
    esac
}

run_exclusively() {

    local lock_name="$1"
    local timeout=$2
    local method=$3
    shift 3

    mkdir -p "$LOCAL_STATE_DIR/lock/subsys"

    local retval
    {
        echo -n "Obtaining an exclusive lock..."
        if ! flock -x 9; then
            echo ' failed.'
            return 1
        fi
        echo ' done'

        ${method} $@
        retval=$?
    } 9> "$LOCAL_STATE_DIR/lock/subsys/lxc-sabayon-$lock_name"

    return $retval
}

create_url () {

    local url=""
    # Example of amd64 tarball url
    # http://mirror.yandex.ru/sabayon/iso/daily/Sabayon_Linux_DAILY_amd64_tarball.tar.gz

    if [ $arch == 'amd64' ] ; then

        if [ $release = 'DAILY' ] ; then
            url="${MIRROR_URL}iso/daily/Sabayon_Linux_DAILY_amd64_tarball.tar.gz"
        else
            url="${MIRROR_URL}iso/monthly/Sabayon_Linux_${release}_amd64_tarball.tar.gz"
        fi
    else
        # https://dockerbuilder.sabayon.org/Sabayon_Linux_16_armv7l.tar.bz2
        if [ $arch == 'armv7l' ] ; then

            # Currently $arch tarball is not on sabayon mirrored tree.
            url="${MIRROR_URL}Sabayon_Linux_16_armv7l.tar.bz2"

        fi
    fi

    echo $url
}


#===========================  Configure  ===========================#

unprivileged_rootfs() {

   pushd ${rootfs}/etc/systemd/system

   # Disable systemd-journald-audit.socket because it seems that doesn't
   # start correctly on unprivileged container
   ln -s /dev/null systemd-journald-audit.socket

   # Disable systemd-remount-fs.service because on unprivileged container
   # systemd can't remount filesystem
   ln -s /dev/null systemd-remount-fs.service

   # Remove mount of FUSE Control File system
   ln -s /dev/null sys-fs-fuse-connections.mount

   # Change execution of service systemd-sysctl to avoid errors.
   mkdir systemd-sysctl.service.d
   cat <<EOF > systemd-sysctl.service.d/00gentoo.conf
[Service]
ExecStart=
ExecStart=/usr/lib/systemd/systemd-sysctl  --prefix=/etc/sysctl.d/
EOF

   # Disable mount of hugepages
   ln -s /dev/null dev-hugepages.mount

   popd

   pushd ${rootfs}

   # Disable sabayon-anti-fork-bomb limits (already apply to lxc container manager)
   sed -i -e 's/^*/#*/g'   ./etc/security/limits.d/00-sabayon-anti-fork-bomb.conf || return 1
   sed -i -e 's/^root/#root/g'   ./etc/security/limits.d/00-sabayon-anti-fork-bomb.conf || return 1

   popd

   return 0
}

unprivileged_shift_owner () {

    # I use /usr/bin/fuidshift from LXD project.

    einfo "Executing: fuidshift ${rootfs} u:0:${mapped_uid}:65536 g:0:${mapped_gid}:65536 ..."

    fuidshift ${rootfs} u:0:${mapped_uid}:65536 g:0:${mapped_gid}:65536 ||
        die 1 "Error on change owners of ${rootfs} directory"

    einfo "Done."

    # Fix permission of container directory
    chmod a+rx ${path}

    return 0
}

systemd_container_tuning () {

    # To avoid error on start systemd-tmpfiles-setup service
    # it is needed clean journal directory
    rm -rf ${rootfs}/var/log/journal/

    # Remove LVM service. Normally not needed on container system.
    rm -rf ${rootfs}/etc/systemd/system/sysinit.target.wants/lvm2-lvmetad.service

    # Comment unneeded entry on /etc/fstab
    sed -e 's/\/dev/#\/dev/g' -i ${rootfs}/etc/fstab

    # Fix this stupid error until fix is available on sabayon image
    # /usr/lib/systemd/system-generators/gentoo-local-generator: line 4: cd: /etc/local.d: No such file or directory
    mkdir -p ${rootfs}/etc/local.d/

    mkdir -p ${rootfs}/etc/systemd/system/NetworkManager.service.d/
    cat <<EOF > ${rootfs}/etc/systemd/system/NetworkManager.service.d/override.conf
[Service]
ExecStartPre=-/bin/ip -4 link set dev eth0 down
EOF
    chmod 644 ${rootfs}/etc/systemd/system/NetworkManager.service.d/override.conf

    return 0
}

configure_container() {
    local config="$1"
    local hostname="$2"
    local arch="$3"
    local privileged_options=""
    local unprivileged_options=""

    if [[ $unprivileged && $unprivileged == true ]] ; then
        if [[ $flush_owner == true ]] ; then
            unprivileged_options="
lxc.idmap = u 0 ${mapped_uid} 65536
lxc.idmap = g 0 ${mapped_gid} 65536
"
        fi

        unprivileged_options="
$unprivileged_options

# Force use of cgroup v1. Currently systemd doesn't support
# correctly cgroup v2. See: https://github.com/lxc/lxc/issues/1669
# about discussion of default-hierarchy option.
lxc.init.cmd = /sbin/init systemd.legacy_systemd_cgroup_controller=yes

# Include common configuration.
lxc.include = $LXC_TEMPLATE_CONFIG/sabayon.userns.conf
"

    else
        privileged_options="
## Allow any mknod (but not reading/writing the node)
lxc.cgroup.devices.allow = b *:* m
lxc.cgroup.devices.allow = c *:* m

### /dev/pts/*
lxc.cgroup.devices.allow = c 136:* rwm
### /dev/tty
lxc.cgroup.devices.allow = c 5:0 rwm
### /dev/console
lxc.cgroup.devices.allow = c 5:1 rwm
### /dev/ptmx
lxc.cgroup.devices.allow = c 5:2 rwm
### fuse
lxc.cgroup.devices.allow = c 10:229 rwm

"
    fi

    cat <<-EOF >> "$config"
# Specify container architecture.
lxc.arch = $arch

# Set hostname.
lxc.uts.name = $hostname

# Include common configuration.
lxc.include = $LXC_TEMPLATE_CONFIG/sabayon.common.conf

$unprivileged_options
$privileged_options
EOF
}


#=============================  Main  ==============================#

parse_cmdline() {

    # Parse command options.
    local short_options="a:dm:n:p:r:hu"
    local long_options="arch:,debug,mirror:,name:,path:,release:,rootfs:,mapped-uid:,mapped-gid:,flush-owner,help"

    options=$(getopt -u -q -a -o "$short_options" -l "$long_options" -- "$@")

    eval set -- "$options"

    # Process command options.
    while [ $# -gt 0 ]; do
        case $1 in
            -a | --arch)
                arch=$2
                shift
                ;;
            -d | --debug)
                debug='yes'
                ;;
            -m | --mirror)
                mirror_url=$2
                shift
                ;;
            -n | --name)
                name=$2
                shift
                ;;
            -p | --path)
                path=$2
                shift
                ;;
            -r | --release)
                release=$2
                shift
                ;;
            --rootfs)
                rootfs=$2
                shift
                ;;
            -u | --unprivileged)
                unprivileged=true
                ;;
            -h | --help)
                usage
                exit 1
                ;;
            --mapped-uid)
                mapped_uid=$2
                shift
                ;;
            --mapped-gid)
                mapped_gid=$2
                shift
                ;;
            --flush-owner)
                flush_owner=true
                ;;
            --)
                break
                ;;
            *)
                einfo "Unknown option: $1"
                usage
                exit 1
                ;;
        esac
        shift
    done

    if [ "$(id -u)" != "0" ]; then
        die 1 "This script must be run as 'root'"
    fi

    # Validate options.
    [ -n "$name" ] || die 1 'Missing required option --name'
    [ -n "$path" ] || die 1 'Missing required option --path'

    if [ -z "$rootfs" ] && [ -f "$path/config" ]; then
        rootfs="$(sed -nE 's/^lxc.rootfs\s*=\s*(.*)$/\1/p' "$path/config")"
    fi
    if [ -z "$rootfs" ]; then
        rootfs="$path/rootfs"
    fi

    [ -z "$path" ] && die 1 "'path' parameter is required."

    arch=$(parse_arch "$arch") \
        || die 1 "Unsupported architecture: $arch"

    [[ $unprivileged == true && $flush_owner == true &&-z "$mapped_uid" ]] && \
        die 1 'Missing required option --mapped-uid with --unprivileged option'

    [[ $unprivileged == true && $flush_owner == true && -z "$mapped_gid" ]] && \
        die 1 'Missing required option --mapped-gid with --unprivileged option'

    [[ $flush_owner == true && $unprivileged == false ]] && \
        die 1 'flush-owner require --unprivileged option'

    return 0
}

main () {

    local tarball=""

    # Set global variables.
    RELEASE="${RELEASE:-"DAILY"}"
    ARCH="${ARCH:-`uname -m`}"
    OS="${OS:-"sabayon"}"

    einfo "Processing command line arguments: $@"

    # Parse command line options
    parse_cmdline "$@"

    DEBUG="$debug"
    MIRROR_URL="${mirror_url:-$(random_mirror_url)}"

    einfo "Use arch = $arch, mirror_url = $MIRROR_URL, path = $path, name = $name, release = $release, unprivileged = $unprivileged, rootfs = $rootfs, mapped_uid = $mapped_uid, mapped_gid = $mapped_gid, flush_owner = $flush_owner"

    [ "$debug" = 'yes' ] && set -x

    # Download sabayon tarball
    tarball=$(create_url)
    einfo "Fetching tarball $tarball..."

    # TODO: use only a compression mode
    if [ $arch == 'amd64' ] ; then
        fetch "${tarball}" | tar -xpz -C "${rootfs}"
    else
        if [ $arch == 'armv7l' ] ; then
            fetch "${tarball}" | tar -xpj -C "${rootfs}"
        fi
    fi

    einfo "Tarball ${tarball} Extracted."

    systemd_container_tuning

    # Fix container for unprivileged mode.
    if [[ $unprivileged == true ]] ; then
        unprivileged_rootfs
        if [[ $flush_owner == true ]] ; then
            unprivileged_shift_owner
        fi
    fi

    return 0
}


einfo "Prepare creation of sabayon container with params: $@ ($#)"

# Here we go!
run_exclusively 'main' 10 main "$@"
configure_container "$path/config" "$name" "$arch"

einfo "Container's rootfs and config have been created"
cat <<-EOF
    Edit the config file $path/config to check/enable networking setup.
    The installed system is preconfigured for a loopback and single network
    interface configured via DHCP.

    To start the container, run "lxc-start -n $name".
    The root password is not set; to enter the container run "lxc-attach -n $name".

    Note: From kenel >= 4.6 for use unprivileged containers it is needed this mount on host:

    mkdir /sys/fs/cgroup/systemd
    mount -t cgroup -o none,name=systemd systemd /sys/fs/cgroup/systemd
EOF
