#!/bin/sh

# Copyright (C) 2014-2022 Daniel Baumann <daniel.baumann@open-infrastructure.net>
#
# SPDX-License-Identifier: GPL-3.0+
#
# 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 3 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 should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

set -e

PROJECT="open-infrastructure"
SOFTWARE="compute-tools"
PROGRAM="container"
VERSION="$(container version)"

SCRIPT="${0}"
export SCRIPT

CACHE="/var/cache/${PROGRAM}/build-$(basename ${SCRIPT})"
CONFIG="/etc/${SOFTWARE}/config"
HOOKS="/etc/${SOFTWARE}/hooks"
MACHINES="/var/lib/machines"

Parameters ()
{
	GETOPT_LONGOPTIONS="bind:,bind-ro:,script:,name:,preseed-file:"
	GETOPT_OPTIONS="b:,s:,n:,p:"

	PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${SCRIPT} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"

	if [ "${?}" != "0" ]
	then
		echo "'${SCRIPT}': getopt exit" >&2
		exit 1
	fi

	eval set -- "${PARAMETERS}"

	while true
	do
		case "${1}" in
			-b|--bind)
				# ignore
				shift 2
				;;

			--bind-ro)
				# ignore
				shift 2
				;;

			--cnt.auto)
				# ignore
				shift 2
				;;

			--cnt.container-server)
				# ignore
				shift 2
				;;

			-s|--script)
				# ignore
				shift 2
				;;

			-n|--name)
				NAME="${2}"
				shift 2
				;;

			--preseed-file)
				PRESEED_FILE="${2}"
				shift 2
				;;

			--)
				shift 1
				break
				;;

			*)
				echo "'${SCRIPT}': getopt error" >&2
				exit 1
				;;
		esac
	done
}

Usage ()
{
	echo "Usage: container build -n|--name NAME -s|--script ${SCRIPT} -- [-p|--preseed-file FILE]" >&2
	exit 1
}

Parameters "${@}"

if [ -z "${NAME}" ]
then
	Usage
fi

if [ -e "${MACHINES}/${NAME}" ]
then
	echo "'${NAME}': container already exists" >&2
	exit 1
fi

if [ "$(id -u)" -ne 0 ]
then
	echo "'${NAME}': need root privileges" >&2
	exit 1
fi

HOST="$(echo ${NAME} | cut -d. -f1)"

Mount ()
{
	# Mounting rw bind mounts
	if [ -n "${BIND}" ]
	then
		BINDS="$(echo ${BIND} | sed -e 's|;| |g')"

		for ENTRY in ${BINDS}
		do
			SOURCE="$(echo ${ENTRY} | awk -F: '{ print $1 }')"
			TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')"

			mkdir -p "${SOURCE}"
			mkdir -p "${MACHINES}/${NAME}/${TARGET}"

			mount -o bind "${SOURCE}" "${MACHINES}/${NAME}/${TARGET}"
		done
	fi

	# Mounting ro bind mounts
	if [ -n "${BIND_RO}" ]
	then
		BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"

		for ENTRY in ${BINDS_RO}
		do
			SOURCE="$(echo ${ENTRY} | awk -F: '{ print $1 }')"
			TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')"

			mkdir -p "${SOURCE}"
			mkdir -p "${MACHINES}/${NAME}/${TARGET}"

			mount -o rbind "${SOURCE}" "${MACHINES}/${NAME}/${TARGET}"
		done
	fi

	# Mounting overlay mounts
	if [ -n "${CNT_OVERLAY}" ]
	then
		CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')"

		COUNT="0"
		for CNT_OVERLAY in ${CNT_OVERLAYS}
		do
			DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')"
			DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')"
			DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')"
			DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')"

			COUNT="$((${COUNT} + 1))"
			CNT_OVERLAY_OPTION="$(echo ${CNT_OVERLAY_OPTIONS} | awk -F ';' "{ print \$${COUNT} }")"

			for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
			do
				mkdir -p "${DIRECTORY}"
			done

			if ! findmnt -n -o SOURCE "${DIRECTORY_MERGED}" | grep -qs '^overlay-'
			then
				if [ -n "${CNT_OVERLAY_OPTION}" ]
				then
					CNT_OVERLAY_OPTION="-o ${CNT_OVERLAY_OPTION}"
				fi

				mount -t overlay overlay-${NAME} ${CNT_OVERLAY_OPTION} -olowerdir="${DIRECTORY_LOWER}",upperdir="${DIRECTORY_UPPER}",workdir="${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
			fi
		done
	fi
}

Umount ()
{
	# Unmounting overlay mounts
	if [ -n "${CNT_OVERLAY}" ]
	then
		CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')"

		for CNT_OVERLAY in ${CNT_OVERLAYS}
		do
			DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')"
			DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')"
			DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')"
			DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')"

			umount -f "${DIRECTORY_MERGED}" > /dev/null 2>&1 || true

			for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
			do
				rmdir --ignore-fail-on-non-empty --parents ${DIRECTORY} > /dev/null 2>&1 || true
			done
		done

		# empty workdir otherwise there might happen stale file handles
		if [ -d "${DIRECTORY_WORK}" ]
		then
			rm --preserve-root --one-file-system -rf "${DIRECTORY_WORK}"/*
		fi
	fi

	# Unmounting ro bind mounts
	if [ -n "${BIND_RO}" ]
	then
		# unmount in reverse order to allow nested bind mounts
		BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g' | awk '{ for (i=NF; i>=1; i--) printf "%s ", $i; print ""}')"

		for ENTRY in ${BINDS_RO}
		do
			TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')"

			umount -f "${MACHINES}/${NAME}/${TARGET}" > /dev/null 2>&1 || true
		done
	fi

	# Unmounting rw bind mounts
	if [ -n "${BIND}" ]
	then
		# unmount in reverse order to allow nested bind mounts
		BINDS="$(echo ${BIND} | sed -e 's|;| |g' | awk '{ for (i=NF; i>=1; i--) printf "%s ", $i; print ""}')"

		for ENTRY in ${BINDS}
		do
			TARGET="$(echo ${ENTRY} | awk -F: '{ print $2 }')"

			umount -f "${MACHINES}/${NAME}/${TARGET}" > /dev/null 2>&1 || true
		done
	fi

	# Unmounting pseudo-filesystems
	umount -f "${DIRECTORY}/dev/pts" > /dev/null 2>&1 || true
	umount -f "${DIRECTORY}/proc" > /dev/null 2>&1 || true
	umount -f "${DIRECTORY}/sys" > /dev/null 2>&1 || true
}

Chroot ()
{
	CHROOT="${1}"
	shift

	chroot "${CHROOT}" /usr/bin/env -i \
		LC_ALL="C" PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games" TERM="${TERM}" \
		DEBIAN_FRONTEND="${DEBCONF_FRONTEND}" DEBIAN_PRIORITY="${DEBCONF_PRIORITY}" \
		DEBCONF_NONINTERACTIVE_SEEN="true" DEBCONF_NOWARNINGS="true" \
		ftp_proxy="${ftp_proxy}" http_proxy="${http_proxy}" \
		${@}
}

Upgrade_system ()
{
	DIRECTORY="${1}"

	# Mount pseudo-filesystems
	mount -o bind /dev/pts "${DIRECTORY}/dev/pts"
	mount -o bind /proc "${DIRECTORY}/proc"
	mount -o bind /sys "${DIRECTORY}/sys"

	# Disable dpkg syncing

cat > "${DIRECTORY}/etc/dpkg/dpkg.cfg.d/${SOFTWARE}" << EOF
force-unsafe-io
EOF

	# Create policy-rc.d file

cat > "${DIRECTORY}/usr/sbin/policy-rc.d" << EOF
#!/bin/sh
echo "All runlevel operations denied by policy" >&2
exit 101
EOF

	chmod 0755 "${DIRECTORY}/usr/sbin/policy-rc.d"

	# Upgrade system
	Chroot "${DIRECTORY}" "apt update"
	Chroot "${DIRECTORY}" "apt --yes --option Dpkg::Options::=--force-confnew upgrade"
	Chroot "${DIRECTORY}" "apt --yes --option Dpkg::Options::=--force-confnew dist-upgrade"

	# Install systemd support packages
	Chroot "${DIRECTORY}" "apt --yes install dbus libpam-systemd systemd-sysv"

	Chroot "${DIRECTORY}" "apt clean"
}

Cleanup_system ()
{
	DIRECTORY="${1}"

	Chroot "${DIRECTORY}" "apt --yes --purge autoremove"
	Chroot "${DIRECTORY}" "apt clean"

	# Cleanup
	rm -f "${DIRECTORY}/etc/dpkg/dpkg.cfg.d/${SOFTWARE}"
	rm -f "${DIRECTORY}/usr/sbin/policy-rc.d"

	# Unmount pseudo-filesystems
	umount -f "${DIRECTORY}/dev/pts" > /dev/null 2>&1 || true
	umount -f "${DIRECTORY}/proc" > /dev/null 2>&1 || true
	umount -f "${DIRECTORY}/sys" > /dev/null 2>&1 || true
}

Debconf ()
{
	# Configure local debconf
	mkdir -p "/tmp/${SOFTWARE}"
	DEBCONF_TMPDIR="$(mktemp -d -p "/tmp/${SOFTWARE}" -t $(basename ${0}).XXXX)"
	export DEBCONF_TMPDIR

	mkdir -p "${DEBCONF_TMPDIR}/debconf"

cat > "${DEBCONF_TMPDIR}/debconf.systemrc" << EOF
Config: configdb
Templates: templatedb

Name: config
Driver: File
Mode: 644
Reject-Type: password
Filename: ${DEBCONF_TMPDIR}/debconf/config.dat

Name: passwords
Driver: File
Mode: 600
Backup: false
Required: false
Accept-Type: password
Filename: ${DEBCONF_TMPDIR}/debconf/passwords.dat

Name: configdb
Driver: Stack
Stack: config, passwords

Name: templatedb
Driver: File
Mode: 644
Filename: ${DEBCONF_TMPDIR}/debconf/templates.dat
EOF

	DEBCONF_SYSTEMRC="${DEBCONF_TMPDIR}/debconf.systemrc"
	export DEBCONF_SYSTEMRC
}

Bootstrap ()
{
	DIRECTORY="${1}"

	EXCLUDE="ifupdown"
	INCLUDE="dbus"

	# apt repositories
	INCLUDE="${INCLUDE},gnupg"

	if ( echo "${MIRROR}" | grep -qs '^https' ) || \
	   ( echo "${PARENT_MIRROR}" | grep -qs '^https' )
	then
		INCLUDE="${INCLUDE},apt-transport-https,ca-certificates"
	fi

	case "${MODE}" in
		progress-linux)
			INCLUDE="${INCLUDE},progress-linux,gnupg"
			;;
	esac

	mkdir -p "$(dirname ${DIRECTORY})"

	case "${BOOTSTRAP}" in
		debootstrap)
			debootstrap --verbose --arch=${ARCHITECTURE} --components=${PARENT_ARCHIVE_AREAS} \
				--exclude=${EXCLUDE} --include=${INCLUDE} ${PARENT_DISTRIBUTION} "${DIRECTORY}" ${PARENT_MIRROR}
			;;

		mmdebstrap)
			mmdebstrap --arch=${ARCHITECTURE} --components=${PARENT_ARCHIVE_AREAS} \
				--format=directory --mode=root --aptopt='APT::Sandbox::User "root"' \
				--include=${INCLUDE} ${PARENT_DISTRIBUTION} "${DIRECTORY}" ${PARENT_MIRROR}
			;;

		*)
			echo "'${NAME}': ${BOOTSTRAP} - not supported" >&2
			exit 1
			;;
	esac
}

Image ()
{
	DIRECTORY="${1}"

	FILES="${IMAGE}"

	for NUMBER in $(seq 1 ${IMAGE_NUMBER})
	do
		eval FILES="${FILES} $`echo IMAGE${NUMBER}`"
	done

	for FILE in ${FILES}
	do
		case "${FILE}" in
			*.gz)
				TAR_OPTIONS="--gzip"

				if [ ! -e /bin/gzip ]
				then
					echo -en "\n"
					echo "'${NAME}': /bin/lzip - no such file." >&2
					exit 1
				fi
				;;

			*.lz)
				TAR_OPTIONS="--lzip"

				if [ ! -e /usr/bin/lzip ]
				then
					echo -en "\n"
					echo "'${NAME}': /usr/bin/lzip - no such file." >&2
					exit 1
				fi
			;;

			*.xz)
				TAR_OPTIONS="--xz"

				if [ ! -e /usr/bin/xz ]
				then
					echo -en "\n"
					echo "'${NAME}': /usr/bin/xz - no such file." >&2
					exit 1
				fi
				;;

			*)
				TAR_OPTIONS=""
				;;
		esac

		mkdir -p "${DIRECTORY}"

		echo "Using ${FILE}"

		if [ -e /usr/bin/pv ]
		then
			curl	--fail --location --progress-bar --user-agent ${SOFTWARE}/${VERSION} --http2 ${CURL_TIME_COND} \
				"${FILE}" -o - | \
				pv --format '%p' --width 77 | \
				tar -C "${DIRECTORY}" --strip 1 ${TAR_OPTIONS} -xf -
				#pv --format '%p' --width 77 "${CACHE}/${FILE}" | tar xf - ${TAR_OPTIONS} -C "${DIRECTORY}" --strip 1
		else
			curl	--fail --location --progress-bar --user-agent ${SOFTWARE}/${VERSION} --http2 ${CURL_TIME_COND} \
				"${FILE}" -o - | \
				tar -C "${DIRECTORY}" --strip 1 ${TAR_OPTIONS} -xf -
		fi

		echo " ok."
	done

	# Writing resolv.conf
	rm -f "${DIRECTORY}/etc/resolv.conf"
	cp /etc/resolv.conf "${DIRECTORY}/etc"
}

Configure_apt ()
{
	DIRECTORY="${1}"

	# Configure apt
	rm -f "${DIRECTORY}/etc/apt/sources.list" "${DIRECTORY}/etc/apt/sources.list.d/debian.sources"

	PARENT_AREA="$(echo ${PARENT_ARCHIVE_AREAS} | sed -e 's|,| |g')"
	PARENT_DIST="$(echo ${PARENT_DISTRIBUTION} | sed -e 's|-backports||')"

cat > "${DIRECTORY}/etc/apt/sources.list.d/debian.sources" << EOF
# /etc/apt/sources.list.d/debian.sources

Types: deb
URIs: ${PARENT_MIRROR}
Suites: ${PARENT_DIST}
Components: ${PARENT_AREA}
PDiffs: no
EOF

	case "${MODE}" in
		progress-linux)

cat > "${DIRECTORY}/progress-linux.cfg" << EOF
progress-linux progress-linux/archives multiselect $(echo ${ARCHIVES} | sed -e 's| |, |g')
progress-linux progress-linux/archive-areas multiselect $(echo ${ARCHIVE_AREAS} | sed -e 's|,| |g')
EOF

			Chroot "${DIRECTORY}" "debconf-set-selections progress-linux.cfg"
			Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure progress-linux"

			rm -f "${DIRECTORY}/progress-linux.cfg"

			case "${INSTALLER}" in
				bootstrap)
					Chroot "${DIRECTORY}" "apt update"
					;;
			esac
			;;
	esac
}

Deconfigure_system ()
{
	DIRECTORY="${1}"

	# Configure fstab

cat > "${DIRECTORY}/etc/fstab" << EOF
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>

EOF

	# Fix /etc/mtab
	rm -f "${DIRECTORY}/etc/mtab"
	ln -s /proc/self/mounts "${DIRECTORY}/etc/mtab"

	# Removing machine-id
	rm -f "${DIRECTORY}/etc/machine-id"

	# Removing resolv.conf
	rm -f "${DIRECTORY}/etc/resolv.conf"
	cp /etc/resolv.conf "${DIRECTORY}/etc"

	# Removing hosts/hostname
	rm -f "${DIRECTORY}"/etc/hosts
	rm -f "${DIRECTORY}"/etc/hostname

	# Removing openssh-server host keys
	rm -f "${DIRECTORY}"/etc/ssh/ssh_host_*_key
	rm -f "${DIRECTORY}"/etc/ssh/ssh_host_*_key.pub
}

Configure_system ()
{
	DIRECTORY="${1}"

	# Overwrite resolv.conf from cache with hosts resolv.conf
	rm -f "${DIRECTORY}/etc/resolv.conf"
	cp /etc/resolv.conf "${DIRECTORY}/etc"

	echo "${NAME}" > "${DIRECTORY}/etc/hostname"

	# Configure apt
	rm -f "${DIRECTORY}/etc/apt/sources.list" "${DIRECTORY}/etc/apt/sources.list.d/debian.sources"

	PARENT_AREA="$(echo ${PARENT_ARCHIVE_AREAS} | sed -e 's|,| |g')"
	PARENT_DIST="$(echo ${PARENT_DISTRIBUTION} | sed -e 's|-backports||')"

cat > "${DIRECTORY}/etc/apt/sources.list.d/debian.sources" << EOF
# /etc/apt/sources.list.d/debian.sources

Types: deb
URIs: ${PARENT_MIRROR}
Suites: ${PARENT_DIST}
Components: ${PARENT_AREA}
PDiffs: no
EOF

	AUTOMATIC_SUITES=""

	for PARENT_REPO in ${PARENT_ARCHIVES}
	do
		case "${PARENT_REPO}" in
			${PARENT_DIST}-updates)
				AUTOMATIC_SUITES="${AUTOMATIC_SUITES} ${PARENT_DIST}-updates"
				;;

			${PARENT_DIST}-proposed-updates)
				AUTOMATIC_SUITES="${AUTOMATIC_SUITES} ${PARENT_DIST}-proposed-updates"
				;;

			${PARENT_DIST}-backports)
				AUTOMATIC_SUITES="${AUTOMATIC_SUITES} ${PARENT_DIST}-backports"
				;;

			${PARENT_DIST}-experimental)
				AUTOMATIC_SUITES="${AUTOMATIC_SUITES} experimental"
				;;
		esac
	done

	if [ -n "${AUTOMATIC_SUITES}" ]
	then
		AUTOMATIC_SUITES="$(echo ${AUTOMATIC_SUITES} | sed -e 's|^ ||')"

cat >> "${DIRECTORY}/etc/apt/sources.list.d/debian.sources" << EOF

Types: deb
URIs: ${PARENT_MIRROR}
Suites: ${AUTOMATIC_SUITES}
Components: ${PARENT_AREA}
PDiffs: no
EOF

	fi

	SECURITY_SUITES=""

	for PARENT_REPO in ${PARENT_ARCHIVES}
	do
		case "${PARENT_REPO}" in
			buster-security)
				SECURITY_SUITES="${SECURITY_SUITES} ${PARENT_DIST}/updates"
				;;

			${PARENT_DIST}-security)
				SECURITY_SUITES="${SECURITY_SUITES} ${PARENT_DIST}-security"
				;;
		esac
	done

	if [ -n "${SECURITY_SUITES}" ]
	then
		SECURITY_SUITES="$(echo ${SECURITY_SUITES} | sed -e 's|^ ||')"

cat >> "${DIRECTORY}/etc/apt/sources.list.d/debian.sources" << EOF

Types: deb
URIs: ${PARENT_MIRROR_SECURITY}
Suites: ${PARENT_DIST}-security
Components: ${PARENT_AREA}
PDiffs: no
EOF

	fi

	case "${MODE}" in
		progress-linux)

cat > "${DIRECTORY}/progress-linux.cfg" << EOF
progress-linux progress-linux/archives multiselect $(echo ${ARCHIVES} | sed -e 's| |, |g')
progress-linux progress-linux/archive-areas multiselect $(echo ${ARCHIVE_AREAS} | sed -e 's|,| |g')
EOF

			Chroot "${DIRECTORY}" "debconf-set-selections progress-linux.cfg"
			Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure progress-linux"

			rm -f "${DIRECTORY}/progress-linux.cfg"
			;;
	esac

	if [ "${APT_RECOMMENDS}" = "false" ]
	then

cat > "${DIRECTORY}/etc/apt/apt.conf.d/recommends.conf" << EOF
APT::Install-Recommends "false";
EOF

	fi

	# Add local archives configured from preseed file
	if ls "${DEBCONF_TMPDIR}/apt"/*.sources > /dev/null 2>&1
	then
		cp "${DEBCONF_TMPDIR}/apt"/*.sources "${DIRECTORY}/etc/apt/sources.list.d"

		if ls "${DEBCONF_TMPDIR}/apt"/*.key > /dev/null 2>&1
		then
			for KEY in "${DEBCONF_TMPDIR}/apt"/*.key
			do
				cp "${KEY}" "${DIRECTORY}"
				Chroot "${DIRECTORY}" "apt-key add $(basename ${KEY})"
				rm -f "${DIRECTORY}/$(basename ${KEY})"
			done
		fi

		if ls "${DEBCONF_TMPDIR}/apt"/*.pref > /dev/null 2>&1
		then
			cp "${DEBCONF_TMPDIR}/apt"/*.pref "${DIRECTORY}/etc/apt/preferences.d"
		fi
	fi

	Upgrade_system "${DIRECTORY}"

	# Preseed system
	if [ -n "${PRESEED_FILE}" ]
	then
		for FILE in ${PRESEED_FILE}
		do
			if [ -e /usr/bin/kdig ]
			then
				DIG="/usr/bin/kdig"
			elif [ -e /usr/bin/dig ]
			then
				DIG="/usr/bin/dig"
			fi

			if [ -n "${DIG}" ]
			then
				IPV4_ADDRESS1="$(${DIG} A +short ${NAME} | tail -n1)"
				IPV4_ADDRESS1_PART1="$(echo ${IPV4_ADDRESS1} | cut -d. -f1)"
				IPV4_ADDRESS1_PART2="$(echo ${IPV4_ADDRESS1} | cut -d. -f2)"
				IPV4_ADDRESS1_PART3="$(echo ${IPV4_ADDRESS1} | cut -d. -f3)"
				IPV4_ADDRESS1_PART4="$(echo ${IPV4_ADDRESS1} | cut -d. -f4)"

				IPV6_ADDRESS1="$(${DIG} AAAA +short ${NAME} | tail -n1)"
				# FIXME: address parts

				export IPV4_ADDRESS1 IPV4_ADDRESS1_PART1 IPV4_ADDRESS1_PART2 IPV4_ADDRESS1_PART3 IPV4_ADDRESS1_PART4
				export IPV6_ADDRESS1
			fi

			sed -e "s|@FILE@|${FILE}|g" \
			    -e "s|@NAME@|${NAME}|g" \
			    -e "s|@HOST@|${HOST}|g" \
			    -e "s|@IPV4_ADDRESS1@|${IPV4_ADDRESS1}|g" \
			    -e "s|@IPV4_ADDRESS1_PART1@|${IPV4_ADDRESS1_PART1}|g" \
			    -e "s|@IPV4_ADDRESS1_PART2@|${IPV4_ADDRESS1_PART2}|g" \
			    -e "s|@IPV4_ADDRESS1_PART3@|${IPV4_ADDRESS1_PART3}|g" \
			    -e "s|@IPV4_ADDRESS1_PART4@|${IPV4_ADDRESS1_PART4}|g" \
			    -e "s|@IPV6_ADDRESS1@|${IPV6_ADDRESS1}|g" \
			"${FILE}" >> "${DIRECTORY}/preseed.cfg"
		done

		Chroot "${DIRECTORY}" "debconf-set-selections preseed.cfg"

		rm -f "${DIRECTORY}/preseed.cfg"
	fi

	# Manual hack to workaround broken preseeding in locales package
	if [ -n "${PRESEED_FILE}" ]
	then
		for FILE in ${PRESEED_FILE}
		do
			if grep -qs locales "${FILE}"
			then
				if Chroot "${DIRECTORY}" dpkg --get-selections | awk '{ print $1 }' | grep -qs '^locales$'
				then
					rm -f "${DIRECTORY}/etc/default/locale" "${DIRECTORY}/etc/locale.gen"
					Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure locales"

					break
				fi
			fi
		done
	fi

	# Manual hack to create conffiles when using locales-all instead of locales
	if [ ! -e "${DIRECTORY}/etc/environment" ]
	then
		echo "LANG=C.UTF-8" >> "${DIRECTORY}/etc/environment"
	fi

	if [ ! -e "${DIRECTORY}/etc/default/locale" ]
	then
		echo "LANG=C.UTF-8" >> "${DIRECTORY}/etc/default/locale"
	fi

	# Manual hack to workaround broken preseeding in tzdata package
	if [ -n "${PRESEED_FILE}" ]
	then
		for FILE in ${PRESEED_FILE}
		do
			if grep -qs tzdata "${FILE}"
			then
				rm -f "${DIRECTORY}/etc/localtime" "${DIRECTORY}/etc/timezone"
				Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure tzdata"

				break
			fi
		done
	fi

	if [ -n "${PACKAGES}" ]
	then
		Chroot "${DIRECTORY}" "apt --option Dpkg::Options::=--force-confnew --yes install ${PACKAGES}"
	fi

	# Manual hack to regenerate ssh keys
	if Chroot "${DIRECTORY}" dpkg --get-selections | awk '{ print $1 }' | grep -qs '^openssh-server$' && \
	   ! ls "${DIRECTORY}"/etc/ssh/ssh_host_*_key > /dev/null 2>&1
	then
		Chroot "${DIRECTORY}" "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=criticial dpkg-reconfigure openssh-server"
	fi

	# container command
	if [ -n "${CONTAINER_COMMAND}" ]
	then
		echo "${CONTAINER_COMMAND}" > "${DIRECTORY}/.container-command"

		chmod 0755 "${DIRECTORY}/.container-command"
		Chroot "${DIRECTORY}" "sh /.container-command"

		rm -f "${DIRECTORY}/.container-command"
	fi

	for NUMBER in $(seq 1 ${CONTAINER_COMMAND_NUMBER})
	do
		eval COMMAND="$`echo CONTAINER_COMMAND${NUMBER}`"

		echo "${COMMAND}" > "${DIRECTORY}/.container-command"

		chmod 0755 "${DIRECTORY}/.container-command"
		Chroot "${DIRECTORY}" "sh /.container-command"

		rm -f "${DIRECTORY}/.container-command"
	done
}

Configure_network ()
{
	DIRECTORY="${1}"

	# Create /etc/resolv.conf
	rm -f "${DIRECTORY}/etc/resolv.conf.tmp"

	if [ -n "${NAMESERVER_DOMAIN}" ]
	then
		echo "domain ${NAMESERVER_DOMAIN}" >> "${DIRECTORY}/etc/resolv.conf.tmp"
	fi

	if [ -n "${NAMESERVER_SEARCH}" ]
	then
		echo "search ${NAMESERVER_SEARCH}" >> "${DIRECTORY}/etc/resolv.conf.tmp"
	fi

	if [ -n "${NAMESERVER_SERVER}" ]
	then
		if [ -e "${DIRECTORY}/etc/resolv.conf.tmp" ]
		then
			echo "" >> "${DIRECTORY}/etc/resolv.conf.tmp"
		fi

		for NAMESERVER in $(echo ${NAMESERVER_SERVER} | sed -e 's|,| |g')
		do
			echo "nameserver ${NAMESERVER}" >> "${DIRECTORY}/etc/resolv.conf.tmp"
		done
	fi

	if [ -n "${NAMESERVER_OPTIONS}" ]
	then
		if [ -e "${DIRECTORY}/etc/resolv.conf.tmp" ]
		then
			echo "" >> "${DIRECTORY}/etc/resolv.conf.tmp"
		fi

		echo "options ${NAMESERVER_OPTIONS}" >> "${DIRECTORY}/etc/resolv.conf.tmp"
	fi

	if [ -e "${DIRECTORY}/etc/resolv.conf.tmp" ]
	then
		mv "${DIRECTORY}/etc/resolv.conf.tmp" "${DIRECTORY}/etc/resolv.conf"
	fi

	# Create /etc/hosts
	rm -f "${DIRECTORY}/etc/hosts.tmp"

	if [ -n "${NETWORK1_IPV4_ADDRESS}" ]
	then

cat >> "${DIRECTORY}/etc/hosts.tmp" << EOF
${NETWORK1_IPV4_ADDRESS}	${NAME}
EOF

	fi

	if [ -n "${NETWORK1_IPV6_ADDRESS}" ]
	then

cat >> "${DIRECTORY}/etc/hosts.tmp" << EOF
${NETWORK1_IPV6_ADDRESS}	${NAME}
EOF

	fi

	if [ -n "${NETWORK1_IPV4_ADDRESS}" ] || [ -n "${NETWORK1_IPV6_ADDRESS}" ]
	then
		echo >> "${DIRECTORY}/etc/hosts.tmp"
	fi

	if [ -z "${NETWORK1_IPV4_ADDRESS}" ] && [ -z "${NETWORK1_IPV6_ADDRESS}" ]
	then
		# localhost only

cat > "${DIRECTORY}/etc/hosts.tmp" << EOF
127.0.0.1	localhost	${NAME}
::1		localhost	${NAME}
EOF

	else

cat > "${DIRECTORY}/etc/hosts.tmp" << EOF
127.0.0.1	localhost
::1		localhost
EOF

	fi

cat >> "${DIRECTORY}/etc/hosts.tmp" << EOF

# The following lines are desirable for IPv6 capable hosts
::1	ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
EOF

	mv "${DIRECTORY}/etc/hosts.tmp" "${DIRECTORY}/etc/hosts"
}

Configure_systemd_networkd ()
{
	DIRECTORY="${1}"

	# Enable systemd-networkd
	chroot "${DIRECTORY}" systemctl enable systemd-networkd

	for NUMBER in $(seq 1 ${NETWORK_NUMBER})
	do
		eval IPV4_COMMENT="$`echo NETWORK${NUMBER}_IPV4_COMMENT`"
		eval IPV4_METHOD="$`echo NETWORK${NUMBER}_IPV4_METHOD`"
		eval IPV4_ADDRESS="$`echo NETWORK${NUMBER}_IPV4_ADDRESS`"
		eval IPV4_GATEWAY="$`echo NETWORK${NUMBER}_IPV4_GATEWAY`"
		eval IPV4_NETMASK="$`echo NETWORK${NUMBER}_IPV4_NETMASK`"
		eval IPV4_POST_UP="$`echo NETWORK${NUMBER}_IPV4_POST_UP`"
		eval IPV4_POST_DOWN="$`echo NETWORK${NUMBER}_IPV4_POST_DOWN`"

		eval IPV6_COMMENT="$`echo NETWORK${NUMBER}_IPV6_COMMENT`"
		eval IPV6_METHOD="$`echo NETWORK${NUMBER}_IPV6_METHOD`"
		eval IPV6_ADDRESS="$`echo NETWORK${NUMBER}_IPV6_ADDRESS`"
		eval IPV6_GATEWAY="$`echo NETWORK${NUMBER}_IPV6_GATEWAY`"
		eval IPV6_NETMASK="$`echo NETWORK${NUMBER}_IPV6_NETMASK`"
		eval IPV6_POST_UP="$`echo NETWORK${NUMBER}_IPV6_POST_UP`"
		eval IPV6_POST_DOWN="$`echo NETWORK${NUMBER}_IPV6_POST_DOWN`"

		if [ "${IPV4_METHOD}" != "none" ] || [ "${IPV6_METHOD}" != "none" ]
		then

cat > "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
[Match]
Name=eno${NUMBER}
EOF

		fi

		if [ -n "${IPV4_METHOD}" ] && [ "${IPV4_METHOD}" != "none" ]
		then

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF

[Network]
EOF

			if [ -n "${IPV4_COMMENT}" ]
			then
				echo "Description=${IPV4_COMMENT}" >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network"
			fi

			case "${IPV4_METHOD}" in
				dhcp)

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
DHCP=ipv4
EOF

					;;

				static)

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
DHCP=no
Address=${IPV4_ADDRESS}/${IPV4_NETMASK}
EOF

					if [ -n "${IPV4_GATEWAY}" ]
					then

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
Gateway=${IPV4_GATEWAY}
EOF

					fi
					;;

				stub)

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
DHCP=no
EOF

					;;
			esac

			if [ -n "${IPV4_POST_UP}" ]
			then

cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv4-post-up-eno${NUMBER}.service" << EOF
[Unit]
Description=${SOFTWARE} IPV4_POST_UP
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c "${IPV4_POST_UP}"

[Install]
WantedBy=multi-user.target
EOF

				chroot "${DIRECTORY}" systemctl enable cnt-ipv4-post-up-eno${NUMBER}.service
			fi

			if [ -n "${IPV4_POST_DOWN}" ]
			then

cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv4-post-down-eno${NUMBER}.service" << EOF
[Unit]
Description=${SOFTWARE} IPV4_POST_DOWN
After=network.target
Wants=network.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c "${IPV4_POST_DOWN}"

[Install]
WantedBy=multi-user.target
EOF

				chroot "${DIRECTORY}" systemctl enable cnt-ipv4-post-down-eno${NUMBER}.service
			fi
		fi

		if [ -n "${IPV6_METHOD}" ] && [ "${IPV6_METHOD}" != "none" ]
		then

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF

[Network]
EOF

			if [ -n "${IPV6_COMMENT}" ]
			then
				echo "Description=${IPV6_COMMENT}" >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network"
			fi

			case "${IPV6_METHOD}" in
				dhcp)

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
DHCP=ipv6
EOF

					;;

				static)

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
DHCP=no
IPv6AcceptRA=no
Address=${IPV6_ADDRESS}/${IPV6_NETMASK}
EOF

					if [ -n "${IPV6_GATEWAY}" ]
					then

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
Gateway=${IPV6_GATEWAY}
EOF

					fi
					;;

				stub)

cat >> "${DIRECTORY}/etc/systemd/network/eno${NUMBER}.network" << EOF
DHCP=no
IPv6AcceptRA=no
EOF

					;;
			esac

			if [ -n "${IPV6_POST_UP}" ]
			then

cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv6-post-up-eno${NUMBER}.service" << EOF
[Unit]
Description=${SOFTWARE} IPV6_POST_UP
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c "${IPV6_POST_UP}"

[Install]
WantedBy=multi-user.target
EOF

				chroot "${DIRECTORY}" systemctl enable cnt-ipv6-post-up-eno${NUMBER}.service
			fi

			if [ -n "${IPV6_POST_DOWN}" ]
			then

cat > "${DIRECTORY}/etc/systemd/system/cnt-ipv6-post-down-eno${NUMBER}.service" << EOF
[Unit]
Description=${SOFTWARE} IPV6_POST_DOWN
After=network.target
Wants=network.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c "${IPV6_POST_DOWN}"

[Install]
WantedBy=multi-user.target
EOF

				chroot "${DIRECTORY}" systemctl enable cnt-ipv6-post-down-eno${NUMBER}.service
			fi
		fi

		NUMBER="$((${NUMBER} + 1))"
	done
}

Commands ()
{
	DIRECTORY="${1}"

	# maximum of 15 characters, prefix is 'veth-'
	HOSTNAME_SHORT="$(echo ${NAME} | cut -c-8)"
	HOST_INTERFACE_NAME="$(echo ${NETWORK1_VETH:-veth-${HOSTNAME_SHORT}-0})"

	sed -i	-e "s|^cnt.auto=.*|cnt.auto=${CNT_AUTO}|g" \
		-e "s|^cnt.container-server=.*|cnt.container-server=${CNT_CONTAINER_SERVER}|g" \
		-e "s|^cnt.network-bridge=.*|cnt.network-bridge=${HOST_INTERFACE_NAME}:${NETWORK1_BRIDGE:-bridge0}|g" \
		-e "s|^cnt.overlay=.*|cnt.overlay=${CNT_OVERLAY}|g" \
		-e "s|^cnt.overlay-options=.*|cnt.overlay-options=${CNT_OVERLAY_OPTIONS}|g" \
		-e "s|^bind=.*|bind=${BIND}|g" \
		-e "s|^bind-ro=.*|bind-ro=${BIND_RO}|g" \
		-e "s|^network-veth-extra=.*|network-veth-extra=${HOST_INTERFACE_NAME}:eno1|g" \
	"${CONFIG}/${NAME}.conf"

	if [ "${NETWORK_NUMBER}" -ge 2 ]
	then
		for NUMBER in $(seq 2 ${NETWORK_NUMBER})
		do
			eval IPV4_METHOD="$`echo NETWORK${NUMBER}_IPV4_METHOD`"
			eval IPV6_METHOD="$`echo NETWORK${NUMBER}_IPV6_METHOD`"

			if [ -z "${IPV4_METHOD}" ] && [ -z "${IPV6_METHOD}" ]
			then
				continue
			fi

			eval HOST_INTERFACE_NAME="$`echo NETWORK${NUMBER}_VETH`"

			HOST_INTERFACE_NAME="$(echo ${HOST_INTERFACE_NAME:-veth-${HOSTNAME_SHORT}-${NUMBER}})"
			CONTAINER_INTERFACE_NAME="eno${NUMBER}"

			sed -i -e "/^register=.*/ a network-veth-extra=${HOST_INTERFACE_NAME}:${CONTAINER_INTERFACE_NAME}" "${CONFIG}/${NAME}.conf"

			eval BRIDGE="$`echo NETWORK${NUMBER}_BRIDGE`"
			sed -i -e "/^register=.*/ a cnt.network-bridge=${HOST_INTERFACE_NAME}:${BRIDGE:-bridge${NUMBER}}" "${CONFIG}/${NAME}.conf"
		done
	fi

	# Setting root password
	echo root:${ROOT_PASSWORD} | chroot "${DIRECTORY}" chpasswd

	# Host command
	if [ -n "${HOST_COMMAND}" ]
	then
		echo "${HOST_COMMAND}" > "${DIRECTORY}/.host-command"

		cd "${DIRECTORY}"
		sh "${DIRECTORY}/.host-command"
		cd "${OLDPWD}"

		rm -f "${DIRECTORY}/.host-command"
	fi

	for NUMBER in $(seq 1 ${HOST_COMMAND_NUMBER})
	do
		eval COMMAND="$`echo HOST_COMMAND${NUMBER}`"

		echo "${COMMAND}" > "${DIRECTORY}/.host-command"

		cd "${DIRECTORY}"
		sh "${DIRECTORY}/.host-command"
		cd "${OLDPWD}"

		rm -f "${DIRECTORY}/.host-command"
	done

	# Show root password in case its automatically set
	case "${ROOT_RANDOM_PASSWORD}" in
		true)
			echo "${NAME}: root password set to '${ROOT_PASSWORD}'."
			;;
	esac
}

# Trap function
trap 'Umount' EXIT HUP INT QUIT TERM

umask 0022

export NAME
export HOST

Debconf

# Pre hooks
for FILE in "${HOOKS}/pre-${SCRIPT}".* "${HOOKS}/${NAME}.pre-${SCRIPT}"
do
	if [ -x "${FILE}" ]
	then
		"${FILE}"
	fi
done

# Run debconf parts
for DEBCONF_SCRIPT in "/usr/share/${SOFTWARE}/build-scripts/debconf.d"/*
do
	if [ -x "${DEBCONF_SCRIPT}" ]
	then
		"${DEBCONF_SCRIPT}"
	fi
done

# Read-in configuration from debconf
. "${DEBCONF_TMPDIR}/debconf.default"

SYSTEM="${MACHINES}/${NAME}"

if [ -z "${IMAGE}" ] && [ -z "${IMAGE1}" ]
then
	INSTALLER="bootstrap"
else
	INSTALLER="image"
fi

case "${INSTALLER}" in
	bootstrap)
		## Dependencies
		if [ -x /usr/bin/mmdebstrap ]
		then
			BOOTSTRAP="mmdebstrap"
		elif [ -x /usr/sbin/debootstrap ]
		then
			BOOTSTRAP="debootstrap"
		else
			echo "'${NAME}': /usr/bin/mmdebstrap or /usr/sbin/debootstrap - no such file." >&2
			exit 1
		fi

		## Generic parts
		if [ ! -e "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" ]
		then
			Bootstrap "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp"
			Configure_apt "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp"
			Deconfigure_system "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp"

			mv "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}.tmp" "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}"
		fi

		Upgrade_system "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" || echo "W: If upgrading the system failed, try removing the cache for your distribution in /var/cache/${PROGRAM}"
		Cleanup_system "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}"

		## Specific parts
		mkdir -p "${MACHINES}"
		cp -a "${CACHE}/${DISTRIBUTION}_${ARCHITECTURE}" "${MACHINES}/${NAME}"
		;;

	image)
		## Dependencies
		if [ -x /usr/bin/curl ]
		then
			GET="curl"
		elif [ -x /usr/bin/wget ]
		then
			GET="wget"
		else
			echo "'${NAME}': /usr/bin/curl or /usr/bin/wget - no such file." >&2
			exit 1
		fi

		COMPRESSIONS=""

		if [ -x /usr/bin/lzip ]
		then
			COMPRESSIONS="${COMPRESSIONS} lz"
		fi

		if [ -x /usr/bin/xz ]
		then
			COMPRESSIONS="${COMPRESSIONS} xz"
		fi

		if [ -x /bin/gzip ]
		then
			COMPRESSIONS="${COMPRESSIONS} gz"
		fi

		if [ -z "${COMPRESSIONS}" ]
		then
			echo "'${NAME}': no supported compressor available (lz, xz, gz)."
			exit 1
		fi

		## Parts
		mkdir -p "${MACHINES}"
		Image "${MACHINES}/${NAME}"

		Configure_apt "${MACHINES}/${NAME}"
		Deconfigure_system "${MACHINES}/${NAME}"
		;;
esac

Mount

Configure_system "${MACHINES}/${NAME}"
Configure_network "${MACHINES}/${NAME}"
Configure_systemd_networkd "${MACHINES}/${NAME}"
Cleanup_system "${MACHINES}/${NAME}"

Commands "${MACHINES}/${NAME}"

# remove debconf temporary files
rm --preserve-root --one-file-system -rf "${DEBCONF_TMPDIR}"
rmdir --ignore-fail-on-non-empty "/tmp/${SOFTWARE}" 2>&1 || true

# Post hooks
for FILE in "${HOOKS}/post-${SCRIPT}".* "${HOOKS}/${NAME}.post-${SCRIPT}"
do
	if [ -x "${FILE}" ]
	then
		"${FILE}"
	fi
done