#!/bin/bash
# cobbler-ubuntu-import

VERBOSITY=0
TEMP_D=""
ISO_DIR="/var/lib/cobbler/isos"
KSDIR="/var/lib/cobbler/kickstarts"
DEFAULT_MIRROR="http://archive.ubuntu.com/ubuntu"
AUTO_KOPTS='log_host=@@server@@ log_port=514 priority=critical locale=en_US netcfg/choose_interface=auto'
AUTO_PRESEED="${KSDIR}/ubuntu-server.preseed"

[ -r /etc/cobbler/cobbler-import-ubuntu.conf ] &&
	. /etc/cobbler/cobbler-import-ubuntu.conf

GPG_KEYRING="${GPG_KEYRING:-/usr/share/keyrings/ubuntu-archive-keyring.gpg}"

error() { echo "$@" 1>&2; }
errorp() { printf "$@" 1>&2; }
fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
failp() { [ $# -eq 0 ] || errorp "$@"; exit 1; }
debug() { [ $VERBOSITY -gt 0 ] && echo "[DEBUG] $@" 1>&2; }

Usage() {
	cat <<EOF
Usage: ${0##*/} [ options ] [ release-arch [ release-arch [ ... ] ] ]

   import distro and profile for Ubuntu release-arch

   options:
      -D | --delete-iso   delete the mini iso after downloading.  Normally
                          they are kept in $ISO_DIR
      -m | --mirror M     use mirror rather than default for downloading iso
                          default: ${DEFAULT_MIRROR}
      -c | --update-check check saved ISOs against what is published on the
                          mirror.  exits 0 if an update is needed or iso does
                          not exist in $ISO_DIR
      -r | --remove       remove cobbler profile/distro and cached ISO
      -u | --update       update iso, import, replace outdated distro and profile
      -U | --update-existing
                          update all existing ubuntu distros in cobbler
      -p | --proxy        <host[:port> http proxy to use
      -v | --verbose      increase verbosity
   Note, arch can be i386, x86_64, or amd64.  However,
   the registered distro and profile will use 'x86_64' rather than 'amd64'.
   This is to keep consistent with cobbler naming.

   Example:
    - ${0##*/} natty-i386
EOF
}

bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; exit 1; }
cleanup() {
	[ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
}

loop_mount() {
	# Create more loop nodes, if necessary
	local mounts=$(grep -c /dev/loop /proc/mounts) || mounts=0
	local loops=$(ls /dev/loop* | wc -l) || loops=0
	if [ $mounts -ge $loops ]; then
		mknod -m 660 /dev/loop$loops b 7 $loops
		chown root:disk /dev/loop$loops
	fi
	# Do the loop mount
	debug "Creating loopback mount from $1 at $2"
	mount -o loop "$1" "$2"
}

get_iso() {
	# first check release's update pocket for an iso.
	# grab from release pocket if that does not exist.
	local md5url="" url="" out=""
	if wget --progress=dot:mega -O "$TEMP_D/$ISO" "$U_UPDATE"; then
		debug "Downloaded ISO from updates pocket: $U_UPDATE"
		md5url="$MD5_UPDATE"
		url="$U_UPDATE"
	elif wget --progress=dot:mega -O "$TEMP_D/$ISO" "$U"; then
		debug "Updating from release pocket: $U"
		md5url="$MD5"
		url="$U"
	else
		fail "failed download for $REL-$ARCH"
	fi

	wget -qO "$TEMP_D/MD5SUMS" "$md5url" &&
		wget -qO "$TEMP_D/MD5SUMS.gpg" "${md5url}.gpg" ||
		fail "unable to download MD5SUMS[.gpg] from $md5url"

	out=$(gpg "--keyring=${GPG_KEYRING}" \
		--verify "${TEMP_D}/MD5SUMS.gpg" "${TEMP_D}/MD5SUMS" 2>&1) ||
		fail "failed to verify MD5SUMS via $GPG_KEYRING ($md5url)"

	local md5exp="" md5found=""
	md5exp=$(awk '$2 == "./netboot/mini.iso" { print $1 }' "$TEMP_D/MD5SUMS") &&
		[ -n "$md5exp" ] ||
		fail "failed to parse checksum for ./netboot/mini.sio in ${md5url}"

	md5found=$(md5sum "${TEMP_D}/$ISO") && md5found=${md5found%% *} &&
		[ -n "$md5found" ] ||
		fail "failed to checksum ${TEMP_D}/$ISO: $md5found"

	[ "$md5exp" = "$md5found" ] ||
		fail "download of ${url} failed, checksum did not match from ${md5url}"

	{ $delete || mv "$TEMP_D/$ISO" "$ISO_DIR/$ISO"; } ||
		fail "failed to move iso $ISO_DIR/$ISO"
}

import_iso() {
	# import iso into cobbler as $DISTRO
	local mounted
	local rel="$1" arch="$2" name="$3"
	if cobbler_has distro "$name"; then
		error "distro '$name' already exists. cannot import."
		return 1
	fi
	{ [ -f "$TEMP_D/$ISO" ] || ln -sf "$ISO_DIR/$ISO" "$TEMP_D/$ISO"; } ||
		fail "failed to create symlink to existing $ISO_DIR/$ISO"
	mounted=0 && loop_mount "$TEMP_D/$ISO" "$TEMP_D/mnt" && mounted=1 &&
		cobbler import --name="$name" --path="$TEMP_D/mnt" \
			--breed=ubuntu --os-version="$rel" --arch="$arch" &&
		umount "${TEMP_D}/mnt" && mounted=0 || {
				[ $mounted -eq 0 ] || umount "$TEMP_D/mnt";
				fail "failed to import $REL-$ARCH";
			}
	echo "imported $name"
}

reassign_distro() {
	# expected to be called after get_iso() has imported a more up to date iso under
	# a temporary name. this will reassign all profiles associated with $old_distro
	# with $new_distro.  after all profiles have been reassigned, $old_distro is
	# removed.
	local new_distro="$1"
	local old_distro="$2"
	local descendents
	debug "Reassigning $old_distro to $new_distro"
	if ! cobbler_has distro "$old_distro"; then
		fail "distro matching profile '$old_distro' does not exist"
	fi
	debug "Renaming old distro '$old_distro' to 'last-$old_distro'"
	cobbler distro rename --name="$old_distro" --newname="last-$old_distro" ||
		fail "could not rename distro '$old_distro' to 'last-$old_distro'"
	descendants=`cobbler profile find --distro="last-$old_distro"`
	for profile in $descendants ; do
		debug "Assigning profile $profile to distro $new_distro"
		cobbler profile edit --name="$profile" --distro="$new_distro" ||
			fail "could not assign profile '$profile' to distro '$new_distro'"
	done
	debug "Removing distro last-$old_distro"
	cobbler distro remove --name="last-$old_distro" ||
		fail "could not remove stale distro 'last-$old_distro'"
	debug "Renaming distro $new_distro to $old_distro"
	cobbler distro rename --name="$new_distro" --newname="$old_distro" ||
		fail "could not rename distro '$new_distro' to '$old_distro'"
	debug "Cleaning up temporary profile '$new_distro'"
	cobbler profile remove --name="$new_distro" ||
		fail "could not remove temp profile '$new_distro'"
}

function update_available() {
	# check MD5SUM first from updates pocket if it exists, then release pocket.
	# compare netboot mini iso with what is local.
	local md5sum_local
	local md5sum_remote
	[ ! -f "$ISO_DIR/$ISO" ] && return 0
	md5sum_local=$(md5sum "$ISO_DIR/$ISO") && md5sum_local=${md5sum_local%% *} &&
		[ -n "$md5sum_local" ] || fail "failed to checksum $ISO_DIR/$ISO: $md5sum_local"
	debug "local md5sum: $md5sum_local ($ISO_DIR/$ISO)"
	wget -qO "$TEMP_D/MD5SUMS" "$MD5_UPDATE" || wget -qO "$TEMP_D/MD5SUMS" "$MD5" ||
		fail "failed to download MD5SUMS for $REL-$ARCH."
	md5sum_remote=$(awk '$2 == "./netboot/mini.iso" { print $1 }' $TEMP_D/MD5SUMS) &&
		[ -n "$md5sum_remote" ] || fail "failed to parse checksum from $TEMP_D/MD5SUMS: $md5sum_remote"
	debug "remote md5sum: $md5sum_remote"
	[ "$md5sum_local" != "$md5sum_remote" ]
}

# inargs(needle, haystack, ...)
# return 0 if 'needle' == one of args $2..$#
in_args() {
	local cur="" needle=$1
	shift;
	for cur in "$@"; do
		[ "$needle" = "$cur" ] && return 0;
	done
	return 1
}

cobbler_has() {
	local noun="$1" name="$2" out=""
	out=$(cobbler "$noun" find --name "$name") && [ "$out" = "$name" ]
}

setup_auto_profile() {
	local parent="$1" name="$2" ks="$3" kopts="$4"
	cobbler_has profile "$name" && return 0
	cobbler profile add --name="$name" --parent="$parent" \
		${ks:+"--kickstart=${ks}"} ${kopts:+"--kopts=${kopts}"}
}

distro_info() {
	if [ -n "${UBUNTU_DISTROS}" ]; then
		# if the user (in config) set ubuntu-distros
		local r
		for r in ${UBUNTU_DISTROS}; do
			echo "$r"
		done
		return
	fi
	distro-info --supported && return 0

	error "Warning: distro-info --supported failed.";
	error "  get distro-info or set UBUNTU_DISTROS in /etc/cobbler/cobbler-import-ubuntu.conf";
	return 1
}

short_opts="Dhm:o:p:vcruU"
long_opts="delete-iso,help,mirror:,proxy:,remove,update,update-existing,update-check,remove,verbose"
getopt_out=$(getopt --name "${0##*/}" \
	--options "${short_opts}" --long "${long_opts}" -- "$@") &&
	eval set -- "${getopt_out}" ||
	bad_Usage

delete=false
check_update=false
update_existing=0
remove=false
mirror=${DEFAULT_MIRROR}
do_update=false

while [ $# -ne 0 ]; do
	cur=${1}; next=${2};
	case "$cur" in
		-h|--help) Usage ; exit 0;;
		-m|--mirror) mirror=${2}; shift;;
		-D|--delete-iso) delete=true;;
		-p|--proxy) proxy=${2}; shift;;
		-v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
		-c|--update-check) check_update=true;;
		-r|--remove) remove=true;;
		-u|--update) do_update=true;;
		-U|--update-existing) do_update=true; update_existing=1;;
		--) shift; break;;
	esac
	shift;
done

if [ "${AUTO_KOPTS#*@@server@@}" != "$AUTO_KOPTS" ]; then
	cobbserv=$(awk '$1 == "server:" { print $2 }' /etc/cobbler/settings)
	[ -n "$cobbserv" ] ||
		fail "unable to get value for '@@server@@' from /etc/cobbler/settings"
	AUTO_KOPTS=${AUTO_KOPTS//@@server@@/${cobbserv}}
fi

## check arguments here
if [ $update_existing -eq 1 ]; then
	[ $# -eq 0 ] ||
		bad_Usage "do not provide arguments with update-existing"
	_existing=$(cobbler distro find --breed=ubuntu) ||
		fail "failed to get list of existing distros"
	[ -n "$_existing" ] ||
		fail "there were no existing distros of type ubuntu"
	supported=( $(distro_info) ) ||
		fail "failed to get list of supported distros"
	to_update=( )
	for item in $_existing; do
		in_args "${item%%-*}" "${supported[@]}" &&
			in_args "${item#*-}" "i386" "x86_64" &&
			to_update[${#to_update[@]}]=${item} ||
			echo "$item: skipping, not <codename>-<arch>"
	done
	set -- "${to_update[@]}"
fi
[ $# -ne 0 ] || bad_Usage "must provide arguments"

[ -f "$GPG_KEYRING" ] ||
	fail "gpg keyring $GPG_KEYRING is not a file"

TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
	fail "failed to make tempdir"
trap cleanup EXIT

if [ -n "$proxy" ] ; then
	debug "Using proxy: $proxy"
	export http_proxy="$proxy"
fi

# program starts here
if ! $delete; then
	{ [ -d "$ISO_DIR" ] || mkdir -p "$ISO_DIR"; } || fail "failed to create $ISO_DIR"
fi
mkdir "$TEMP_D/mnt" || fail "failed to make tempdir/mnt"

updates_needed=0

for tok in "$@"; do
	REL=${tok%-*}; ARCH=${tok#*-}
	[ "$ARCH" = "amd64" ] && 
		{ error "Warning: using x86_64 for arch rather than amd64"; ARCH="x86_64"; }
	ubuntu_arch=$ARCH; [ "$ARCH" = "x86_64" ] && ubuntu_arch=amd64
	ISO=$REL-$ARCH-mini.iso
	U=$mirror/dists/$REL/main/installer-$ubuntu_arch/current/images/netboot/mini.iso
	MD5=$mirror/dists/$REL/main/installer-$ubuntu_arch/current/images/MD5SUMS
	U_UPDATE=$mirror/dists/$REL-updates/main/installer-$ubuntu_arch/current/images/netboot/mini.iso
	MD5_UPDATE=$mirror/dists/$REL-updates/main/installer-$ubuntu_arch/current/images/MD5SUMS
	if $remove; then
		if [ -f "$ISO_DIR/$ISO" ]; then
			debug "Removing $ISO_DIR/$ISO"
			rm -rf "$ISO_DIR/$ISO" ||
				fail "Could not delete $ISO_DIR/$ISO"
		fi
		if cobbler_has distro "$REL-$ARCH"; then
			debug "Removing cobbler distro '$REL-$ARCH'"
			cobbler distro remove --name="$REL-$ARCH" ||
				fail "Could not remove cobbler profile for $REL-$ARCH"
		fi
		continue
	fi
	if $check_update; then
		if update_available; then
			echo "$REL-$ARCH: Update available"
			updates_needed=$(($updates_needed+1));
		else
			echo "$REL-$ARCH: Up to date"
		fi
		continue
	fi
	if $do_update; then
		update_available ||
			{ echo "$REL-$ARCH: no update available"; continue; }
		# get iso from either pocket and import it with a tmp name.
		get_iso && import_iso "$REL" "$ARCH" "tmp-$REL-$ARCH" ||
			fail "import-iso tmp-$REL-$ARCH failed"
		setup_auto_profile "$REL-$ARCH" "$REL-$ARCH-auto" \
			"$AUTO_PRESEED" "$AUTO_KOPTS" ||
			fail "failed to set up auto profile $REL-$ARCH-auto"
		reassign_distro "tmp-$REL-$ARCH" "$REL-$ARCH"
		continue
	fi
	[[ ! -f "$ISO_DIR/$ISO" ]] && get_iso
	import_iso "$REL" "$ARCH" "$REL-$ARCH" &&
		echo "$REL-$ARCH: imported" ||
		fail "failed to import iso for $REL-$ARCH"
	setup_auto_profile "$REL-$ARCH" "$REL-$ARCH-auto" \
		"$AUTO_PRESEED" "$AUTO_KOPTS" ||
		fail "failed to setup auto profile for $REL-$ARCH-auto"
done

if $check_update; then
	# if --update-check, but no updates needed, exit 3
	[ $updates_needed -eq 0 ] && exit 3
	# if updates are needed, exit 0
	exit 0
fi

# vi: ts=4 noexpandtab
