#!/bin/sh
set -ex
MYSELF="${0##*/}"

bindir="${0%/*}"
datadir="${bindir%/*}/share/fuel-bootstrap-image"

global_conf="/etc/fuel-bootstrap-image.conf"
[ -r "$global_conf" ] && . "$global_conf"

[ -z "$MOS_VERSION" ] && MOS_VERSION="7.0"
[ -z "$DISTRO_RELEASE" ] && DISTRO_RELEASE="trusty"
[ -z "$MIRROR_DISTRO" ] && MIRROR_DISTRO="http://archive.ubuntu.com/ubuntu"
[ -z "$MIRROR_MOS" ] && MIRROR_MOS="http://mirror.fuel-infra.org/mos-repos/ubuntu/$MOS_VERSION"
[ -z "$KERNEL_FLAVOR" ] && KERNEL_FLAVOR="-generic-lts-trusty"
[ -z "$ARCH" ] && ARCH="amd64"
[ -z "$DESTDIR" ] && DESTDIR="/var/www/nailgun/bootstrap/ubuntu"

# Packages required for the master node to discover a bootstrap node
BOOTSTRAP_FUEL_PKGS_DFLT="openssh-client openssh-server ntp mcollective nailgun-agent nailgun-mcagents nailgun-net-check fuel-agent"
[ -z "$BOOTSTRAP_FUEL_PKGS" ] && BOOTSTRAP_FUEL_PKGS="$BOOTSTRAP_FUEL_PKGS_DFLT"

if [ -n "$http_proxy" ]; then
	export HTTP_PROXY="$http_proxy"
elif [ -n "$HTTP_PROXY" ]; then
	export http_proxy="$HTTP_PROXY"
fi

# Kernel, firmware, live boot
BOOTSTRAP_PKGS="ubuntu-minimal live-boot live-boot-initramfs-tools linux-image${KERNEL_FLAVOR} linux-firmware linux-firmware-nonfree"
# compress initramfs with xz, make squashfs root filesystem image
BOOTSTRAP_PKGS="$BOOTSTRAP_PKGS xz-utils squashfs-tools"
# Smaller tools providing the standard ones.
# - mdadm depends on mail-transport-agent, default one is postfix => use msmtp instead
BOOTSTRAP_PKGS="$BOOTSTRAP_PKGS msmtp-mta"

apt_setup ()
{
	local root="$1"
	local sources_list="${root}/etc/apt/sources.list"
	local apt_prefs="${root}/etc/apt/preferences"
	local mos_codename="mos${MOS_VERSION}"
	local release_file="$MIRROR_MOS/dists/$mos_codename/Release"
	if ! wget -q -O /dev/null "$release_file" 2>/dev/null; then
		cat >&2 <<-EOF
		$MYSELF: broken MOS repo: no $release_file"
		EOF
		exit 2
	fi
	mkdir -p "${sources_list%/*}"

	cat > "$sources_list" <<-EOF
	deb $MIRROR_DISTRO ${DISTRO_RELEASE}         main universe multiverse restricted
	deb $MIRROR_DISTRO ${DISTRO_RELEASE}-security main universe multiverse restricted
	deb $MIRROR_DISTRO ${DISTRO_RELEASE}-updates  main universe multiverse restricted
	deb $MIRROR_MOS ${mos_codename}          main
	deb $MIRROR_MOS ${mos_codename}-security main
	deb $MIRROR_MOS ${mos_codename}-updates  main
	deb $MIRROR_MOS ${mos_codename}-holdback main
	EOF
	if [ -n "$EXTRA_DEB_REPOS" ]; then
		l="$EXTRA_DEB_REPOS"
		IFS='|'
		set -- $l
		unset IFS
		for repo; do
			echo "$repo"
		done >> "$sources_list"
	fi

	cat > "$apt_prefs" <<-EOF
	Package: *
	Pin: release o=Mirantis, n=mos${MOS_VERSION}
	Pin-Priority: 1101
	Package: *
	Pin: release o=Mirantis, n=${DISTRO_RELEASE}
	Pin-Priority: 1101
	EOF

	if [ -n "$HTTP_PROXY" ]; then
		cat > "$root/etc/apt/apt.conf.d/01mirantis-use-proxy" <<-EOF
		Acquire::http::Proxy "$HTTP_PROXY";
		EOF
	fi
}

run_apt_get ()
{
	local root="$1"
	shift
	chroot "$root" env \
		LC_ALL=C \
		DEBIAN_FRONTEND=noninteractive \
		DEBCONF_NONINTERACTIVE_SEEN=true \
		TMPDIR=/tmp \
		TMP=/tmp \
		apt-get $@
}

dpkg_is_too_old ()
{
	# XXX: dpkg-deb versions older than 1.15.6 can't handle data.tar.xz
	# (which is the default payload of Ubuntu packages)
	# Such an ancient version of dpkg is shipped with CentOS 6.[56]
	local dpkg_version
	local dpkg_major_version
	local dpkg_minor_version
	local dpkg_patch_version
	if ! dpkg-deb --help >/dev/null 2>&1; then
		return 0
	fi
	dpkg_version=`dpkg-deb --version | sed -rne '1 s/^.*\s+version\s+([0-9]+)\.([0-9]+)\.([0-9]+).*/\1.\2.\3/p'`
	[ -z "$dpkg_version" ] && return 0

	IFS='.'
	set -- $dpkg_version
	unset IFS
	dpkg_major_version="$1"
	dpkg_minor_version="$2"
	dpkg_patch_version="$3"

	if [ $dpkg_major_version -le 1 ] && [ $dpkg_minor_version -le 15 ] && [ $dpkg_patch_version -lt 6 ]; then
		echo "DEBUG: $MYSELF: dpkg is too old, using ar to unpack debian packages" >&2
		return 0
	fi
	return 1
}

run_debootstrap ()
{
	local root="$1"
	[ -z "$root" ] && exit 1
	local insecure="--no-check-gpg"
	local extractor=''
	if dpkg_is_too_old; then
		# Ubuntu packages use data.tar.xz payload. Ancient versions of
		# dpkg (in particular the ones shipped with CentOS 6.x) can't
		# handle such packages. Tell debootstrap to use ar instead to
		# avoid the failure.
		extractor='--extractor=ar'
	fi
	env \
		LC_ALL=C \
		DEBIAN_FRONTEND=noninteractive \
		DEBCONF_NONINTERACTIVE_SEEN=true \
		debootstrap $insecure $extractor --arch=${ARCH} ${DISTRO_RELEASE} "$root" $MIRROR_DISTRO
}

install_packages ()
{
	local root="$1"
	shift
	echo "INFO: $MYSELF: installing pkgs: $*" >&2
	run_apt_get "$root" install --yes $@
}

upgrade_chroot ()
{
	local root="$1"
	run_apt_get "$root" update
	if ! mountpoint -q "$root/proc"; then
		mount -t proc bootstrapproc "$root/proc"
	fi
	run_apt_get "$root" dist-upgrade --yes
}

add_local_mos_repo ()
{
	# we need the local APT repo (/var/www/nailgun/ubuntu/x86_64)
	# before web server is up and running => use bind mount
	local root="$1"
	# TODO(asheplyakov): use proper arch name (amd64)
	local local_repo="/var/www/nailgun/ubuntu/x86_64"
	local path_in_chroot="/tmp/local-apt"
	local source_parts_d="${root}/etc/apt/sources.list.d"

	# TODO(asheplyakov): update the codename after repo get fixed
	local mos_codename="mos${MOS_VERSION}"

	mkdir -p "${root}${path_in_chroot}" "${source_parts_d}"
	mount -o bind "$local_repo" "${root}${path_in_chroot}"
	mount -o remount,ro,bind "${root}${path_in_chroot}"
	cat > "${source_parts_d}/nailgun-local.list" <<-EOF
	deb file://${path_in_chroot} ${mos_codename} main
	EOF
}

allow_insecure_apt ()
{
	local root="$1"
	local conflet="${root}/etc/apt/apt.conf.d/02mirantis-insecure-apt"
	mkdir -p "${conflet%/*}"
	echo 'APT::Get::AllowUnauthenticated 1;' > "$conflet"
}

suppress_services_start ()
{
	local root="$1"
	local policy_rc="$root/usr/sbin/policy-rc.d"
	mkdir -p "${policy_rc%/*}"
	cat > "$policy_rc" <<-EOF
	#!/bin/sh
	# suppress services start in the staging chroot
	exit 101
	EOF
	chmod 755 "$policy_rc"
}

propagate_host_resolv_conf ()
{
	local root="$1"
	mkdir -p "$root/etc"
	for conf in "/etc/resolv.conf" "/etc/hosts"; do
		if [ -e "${root}${conf}" ]; then
			cp -a "${root}${conf}" "${root}${conf}.bak"
		fi
	done
}

restore_resolv_conf ()
{
	local root="$1"
	for conf in "/etc/resolv.conf" "/etc/hosts"; do
		if [ -e "${root}${conf}.bak" ]; then
			rm -f "${root}${conf}"
			cp -a "${root}${conf}.bak" "${root}${conf}"
		fi
	done
}

make_utf8_locale ()
{
	local root="$1"
	chroot "$root" /bin/sh -c "locale-gen en_US.UTF-8 && dpkg-reconfigure locales"
}

# XXX: CentOS version of debootstrap produces a broken /dev:
# /dev/fd is a directory instead of a symlink to /proc/self/fd
dev_fixup ()
{
	local root="$1"
	if [ -z "$root" ]; then
		echo "*** Error: $MYSELF: dev_fixup: \$root is not specified" >&2
		exit 1
	fi
	mkdir -p -m755 "$root/dev"
	if [ ! -L "$root/dev/fd" ]; then
		rm -rf "$root/dev/fd"
		# Ask MAKEDEV to re-create /dev/fd, /dev/stdin, etc
		chroot "$root" /bin/sh -c "cd /dev && MAKEDEV fd"
	fi
	if [ ! -c "$root/dev/null" ]; then
		# basic device nodes are missing => create them
		chroot "$root" /bin/sh -c "cd /dev && MAKEDEV std"
	fi
}

copy_conf_files ()
{
	local root="$1"
	rsync -rlptDK "$datadir/ubuntu/files/" "${root%/}"
	sed -i $root/etc/shadow -e '/^root/c\root:$6$oC7haQNQ$LtVf6AI.QKn9Jb89r83PtQN9fBqpHT9bAFLzy.YVxTLiFgsoqlPY3awKvbuSgtxYHx4RUcpUqMotp.WZ0Hwoj.:15441:0:99999:7:::'
}

install_ssh_keys ()
{
	local root="$1"
	shift
	if [ -z "$*" ]; then
		echo "*** Error: $MYSELF: no ssh keys specified" >&2
		exit 1
	fi
	local authorized_keys="$root/root/.ssh/authorized_keys"
	local dot_ssh_dir="${authorized_keys%/*}"
	if [ ! -d "${dot_ssh_dir}" ]; then
		mkdir -p -m700 "${dot_ssh_dir}"
	fi
	for key; do
		if [ ! -r "$key" ]; then
			echo "*** Error: $MYSELF: no such file: $key" >&2
			exit 1
		fi
	done
	cat $@ > "$authorized_keys"
	chmod 640 "$authorized_keys"
}

cleanup_chroot ()
{
	local root="$1"
	[ -z "$root" ] && exit 1
	signal_chrooted_processes "$root" SIGTERM
	signal_chrooted_processes "$root" SIGKILL
	umount "${root}/tmp/local-apt" 2>/dev/null || umount -l "${root}/tmp/local-apt"
	rm -f "${root}/etc/apt/sources.list.d/nailgun-local.list"
	rm -rf $root/var/cache/apt/archives/*.deb
	rm -f $root/etc/apt/apt.conf.d/01mirantis-use-proxy.conf
	rm -f $root/var/log/bootstrap.log
	rm -rf $root/tmp/*
	rm -rf $root/run/*
}

recompress_initramfs ()
{
	local root="$1"
	local initramfs_conf="$root/etc/initramfs-tools/initramfs.conf"
	sed -i $initramfs_conf -re 's/COMPRESS\s*=\s*gzip/COMPRESS=xz/'
	rm -f $root/boot/initrd*
	chroot "$root" \
		env \
		LC_ALL=C \
		DEBIAN_FRONTEND=noninteractive \
		DEBCONF_NONINTERACTIVE_SEEN=true \
		TMPDIR=/tmp \
		TMP=/tmp \
		update-initramfs -c -k all
}

mk_squashfs_image ()
{
	local root="$1"
	local tmp="$$"
	[ -d "$DESTDIR" ] && mkdir -p "$DESTDIR"
	cp -a $root/boot/initrd* $DESTDIR/initramfs.img.${tmp}
	cp -a $root/boot/vmlinuz* $DESTDIR/linux.${tmp}
	rm -f $root/boot/initrd*
	rm -f $root/boot/vmlinuz*

	# run mksquashfs inside a chroot (Ubuntu kernel will be able to
	# mount an image produced by Ubuntu squashfs-tools)
	mount -t tmpfs -o rw,nodev,nosuid,noatime,mode=0755,size=4M mnt${tmp} "$root/mnt"
	mkdir -p "$root/mnt/src" "$root/mnt/dst"
	mount -o bind "$root" "$root/mnt/src"
	mount -o remount,bind,ro "$root/mnt/src"
	mount -o bind "$DESTDIR" "$root/mnt/dst"

	if ! mountpoint -q "$root/proc"; then
		mount -t proc sandboxproc "$root/proc"
	fi
	chroot "$root" mksquashfs /mnt/src /mnt/dst/root.squashfs.${tmp} -comp xz -no-progress -noappend
	mv $DESTDIR/initramfs.img.${tmp} $DESTDIR/initramfs.img
	mv $DESTDIR/linux.${tmp} $DESTDIR/linux
	mv $DESTDIR/root.squashfs.${tmp} $DESTDIR/root.squashfs

	umount "$root/mnt/dst"
	umount "$root/mnt/src"
	umount "$root/mnt"
}

build_image ()
{
	local root="$1"
	chmod 755 "$root"
	suppress_services_start "$root"
	run_debootstrap "$root"
	dev_fixup "$root"
	suppress_services_start "$root"
	propagate_host_resolv_conf "$root"
	make_utf8_locale "$root"
	apt_setup "$root"
	add_local_mos_repo "$root"
	allow_insecure_apt "$root"
	upgrade_chroot "$root"
	install_packages "$root" $BOOTSTRAP_PKGS $BOOTSTRAP_FUEL_PKGS
	recompress_initramfs "$root"
	copy_conf_files "$root"
	if [ -n "$BOOTSTRAP_SSH_KEYS" ]; then
		install_ssh_keys "$root" $BOOTSTRAP_SSH_KEYS
	else
		cat >&2 <<-EOF
		$MYSELF: Warning: no ssh keys specified
		$MYSELF: bootstrap nodes won't be available via ssh
		EOF
	fi
	restore_resolv_conf "$root"
	cleanup_chroot "$root"
	mk_squashfs_image "$root"
}

root=`mktemp -d --tmpdir fuel-bootstrap-image.XXXXXXXXX`

main ()
{
	build_image "$root"
}

signal_chrooted_processes ()
{
	local root="$1"
	local signal="${2:-SIGTERM}"
	local max_attempts=10
	local timeout=2
	local count=0
	local found_processes
	[ ! -d "$root" ] && return 0
	while [ $count -lt $max_attempts ]; do
		found_processes=''
		for pid in `fuser $root 2>/dev/null`; do
			[ "$pid" = "kernel" ] && continue
			if [ "`readlink /proc/$pid/root`" = "$root" ]; then
				found_processes='yes'
				kill "-${signal}" $pid
			fi
		done
		[ -z "$found_processes" ] && break
		count=$((count+1))
		sleep $timeout
	done
}

final_cleanup ()
{
	signal_chrooted_processes "$root" SIGTERM
	signal_chrooted_processes "$root" SIGKILL
	for mnt in /tmp/local-apt /mnt/dst /mnt/src /mnt /proc; do
		if mountpoint -q "${root}${mnt}"; then
			umount "${root}${mnt}" || umount -l "${root}${mnt}" || true
		fi
	done
	if [ -z "$SAVE_TEMPS" ]; then
		rm -rf "$root"
	fi
}

trap final_cleanup 0
trap final_cleanup HUP TERM INT QUIT
main
