#!/bin/bash
PATH=/bin:/usr/bin:/sbin:/usr/sbin

set -e

PKGDIR="/opt/isu"
RUNDIR="/run/isu"
ISUCFG="isu.cfg"
LOGTAG="ISUGENERATOR"
BOOT_STATUS_PATH="/opt/isu/.boot_status"

# Public key will be checked only if below variable is set
PUBKEY="/etc/isu_public_key.pem"

MY_NAME=$(basename "$0")

if [ "$MY_NAME" = "isu-system-generator" ]; then
	ISU_SERVICES_DIR=system-services
	SERVICES_DIR=/usr/lib/systemd/system
	INSTANCE=system
elif [ "$MY_NAME" = "isu-user-generator" ]; then
	ISU_SERVICES_DIR=user-services
	SERVICES_DIR=/usr/lib/systemd/user
	INSTANCE=user
else
	echo "This generator must be named isu-system-generator or isu-user-generator"
	exit 1
fi

log()
{
	local priority="$1"
	shift
	# Keep echo for immediate log messages to the terminal.
	# Useful for manual script debugging.
	echo "$MY_NAME: $*" >&2
	# Skip sending logs to dlog when the priority is "I" (information).
	# This is done to reduce overhead and improve performance by avoiding
	# time consuming logging operations for informational messages.
	if [[ "$priority" != "I" ]]; then
		# Send logs to dlog with the specified priority.
		dlogsend -p "$priority" -t "$LOGTAG" "$MY_NAME: $*"
	fi
}

install_units()
{
	local srv_path="$1"
	local isu_pkg_name="$2"
	local srv_fname=$(basename $srv_path)
	local new_srv_path="$UNITDIR/$srv_fname"

	rm -f "${new_srv_path}" "${new_srv_path}.tmp"

	cat "$srv_path" > "$new_srv_path".tmp || return 1
	local mount_unit="run-isu-$(systemd-escape ${isu_pkg_name})-rootfs.mount"
	local confd="${new_srv_path}.d"
	mkdir -p "${confd}"

	if [ "$INSTANCE" = "system" ]; then
		cat <<EOF >> "$confd/isu.conf.tmp" || return 1
# Automatically generated by $0
[Unit]
After=${mount_unit}
BindsTo=${mount_unit}
EOF
	else
# User session unit can not depend on system session units, thus we can't really use After=/etc. here.
# However, given that we guarantee that isu .mount units are mounted in local-fs.target, which is part
# of basic.target, it should be enough to depend on the DefaultDependencies logic.
		cat <<EOF >> "$confd/isu.conf.tmp" || return 1
# Automatically generated by $0
[Unit]
DefaultDependencies=yes
EOF
	fi

	# make new unit visible
	mv "${confd}/isu.conf.tmp" "${confd}/isu.conf" || return 1
	if ! mv "${new_srv_path}.tmp" "${new_srv_path}"; then
		mv "${confd}/isu.conf" "${confd}/isu.conf.revert" || :
	fi
	log "I" "Installed $INSTANCE service $srv_path for package $isu_pkg_name"

	return 0
}

setup_isu_run_dir()
{
	local isu_pkg_dir="$1"
	local isu_pkg_name="$2"
	local isu_pkg_run="$RUNDIR/$isu_pkg_name"

	if ! mkdir -p "$isu_pkg_run/rootfs"; then
		log "E" "Unable to create needed directory hierarchy at $isu_pkg_run/rootfs - skipping ISU package"
		return 1
	fi

	if ! install -m0644 -o root -g root --context=_ "$isu_pkg_dir/$ISUCFG" "$isu_pkg_run/$ISUCFG"; then
		log "E" "Unable to setup essential ISU configuration - skipping ISU package"
		return 1
	fi
}

install_mount_unit()
{
	local isu_pkg_name="$1"

	if ! test -d "$RUNDIR/$isu_pkg_name/rootfs"; then return 1; fi

	local mount_unit="run-isu-$(systemd-escape ${isu_pkg_name})-rootfs.mount"
	if [ ! -r "$UNITDIR/$mount_unit" ]; then

		if ! test -r "$i"/rootfs.img; then
			log "E" "Cannot access rootfs.img. Skipping $isu_pkg_name"
			return 1
		fi

		# generate mount unit for ISU image and extend the service file to use it
		# if mount unit already exists, it means it's been generated by previous
		# install_mount_unit() invocation - for the same ISU package, but different
		# .service file
		cat <<EOF >> "$UNITDIR/$mount_unit" || return 1
# Automatically generated by $0
[Unit]
DefaultDependencies=no
Before=local-fs.target

[Mount]
What=${PKGDIR}/${isu_pkg_name}/rootfs.img
Where=${RUNDIR}/${isu_pkg_name}/rootfs
EOF
		mkdir -p "$UNITDIR/local-fs.target.wants"
		ln -s "../$mount_unit" "$UNITDIR/local-fs.target.wants/"
		log "I" "Installed $UNITDIR/$mount_unit for package $isu_pkg_name"

	fi

	return 0
}

isu_prepare_system()
{
	if [ "$INSTANCE" != "system" ]; then return 0; fi

	local isu_pkg_dir="$1"
	local isu_pkg_name="$2"

	# verify signature and checksum before considering ISU package for application on the system
	cksum_sign_path="$isu_pkg_dir/checksum.sha256.sign"
	cksum_path="${cksum_sign_path%.sign}"

	if [ "$PUBKEY" -a -r "$PUBKEY" ]; then
		if ! openssl dgst -sha256 -verify "$PUBKEY" -signature "$cksum_sign_path" "$cksum_path"; then
			log "E" "Public key verification failed for $cksum_path"
			return 1
		fi
		log "I" "Public key verification succeeded for $isu_pkg_dir"
	fi

	if [ -s "$cksum_path" ]; then
		pushd "$isu_pkg_dir"
		if ! sha256sum -c --status "$cksum_path"; then
			popd
			log "E" "Checksum verification failed for $isu_pkg_dir - skipping ISU package"
			return 1
		fi
		popd
	else
		log "E" "Missing or broken checksum file: $cksum_path - skipping ISU package"
		return 1
	fi

	setup_isu_run_dir "$isu_pkg_dir" "$isu_pkg_name"
	install_mount_unit "$isu_pkg_name"
}

isu_check_type()
{
	local isu_dir="$1"
	if grep -q -i -e "^type[ \t]*=[ \t]*library" "${isu_dir}/$ISUCFG"; then
		echo "library"
	else
		echo "service"
	fi
	return 0
}

isu_install_library_pkg()
{
	if [ "$INSTANCE" != "system" ]; then return 0; fi

	local isu_pkg_dir="$1"
	local isu_pkg_name="$2"

	# This piece of code can be optimized by creating a link to the latest
	# version of the package, but this requires a change in glibc
	setup_isu_run_dir "$isu_pkg_dir" "$isu_pkg_name"
	install_mount_unit "$isu_pkg_name"

	for dir in $(compgen -G "${PKGDIR}/${isu_pkg_name}@*"); do
		if [ ! -d "$dir" ]; then
			continue
		fi

		local isu_pkg_version_name=$(basename "$dir")

		setup_isu_run_dir "$isu_pkg_dir" "$isu_pkg_version_name"
		install_mount_unit "$isu_pkg_version_name"
	done

	return 0
}

isu_prepare()
{
	local isu_pkg_dir="$1"
	local isu_pkg_name=$(basename "$isu_pkg_dir")

	local isu_pkg_type=$(isu_check_type "$isu_pkg_dir")

	if [ "$isu_pkg_type" = "library" ]; then
		isu_install_library_pkg "$isu_pkg_dir" "$isu_pkg_name"
		return 0
	fi

	if ! isu_prepare_system "$isu_pkg_dir" "$isu_pkg_name"; then
		return 1
	fi

	for srv_path in $(compgen -G "${isu_pkg_dir}/${ISU_SERVICES_DIR}/*.service"); do

		if ! test -r "$srv_path"; then
			log "E" "Service file $srv_path not readable. Skipping"
			continue
		fi

		install_units "$srv_path" "$isu_pkg_name"
	done

	for unit_path in $(compgen -G "${isu_pkg_dir}/${ISU_SERVICES_DIR}/*.mount"); do

		if ! test -r "$unit_path"; then
			log "E" "Unit file $unit_path not readable. Skipping"
			continue
		fi
		cp -a "$unit_path" "$UNITDIR/"
	done
}

# Entry point

set_boot_status()
{
    local new_status="$1"

    local tmp_file="/opt/isu/.boot_status.tmp"
    > "$tmp_file"

    local status_set=false

    if [ ! -f "$BOOT_STATUS_PATH" ]; then
        echo "boot_status: $new_status" > "$BOOT_STATUS_PATH"
        chown system:system "$BOOT_STATUS_PATH"
        return
    fi

    while IFS= read -r line; do
        if [[ "$line" == boot_status:* ]]; then
            echo "boot_status: $new_status" >> "$tmp_file"
            status_set=true
        else
            echo "$line" >> "$tmp_file"
        fi
    done < "$BOOT_STATUS_PATH"

    if [ "$status_set" = false ]; then
        echo "boot_status: $new_status" >> "$tmp_file"
    fi

    mv "$tmp_file" "$BOOT_STATUS_PATH" 2>/dev/null
    if [ $? -ne 0 ]; then
        timestamp=$(date +%s)
        echo "boot_status: $new_status" > "/opt/isu/error_$timestamp"
    fi
    chown system:system "$BOOT_STATUS_PATH"
}

if [ -z "$1" ]; then
    echo "Please specify unitdir(s) as decribed in systemd.generator (1 or 3 arguments)"
    exit 1
fi

UNITDIR="$1"

if [ ! -f "$BOOT_STATUS_PATH" ]; then
    log "I" "BOOT_STATUS_PATH does not exist, skipping generation - file should be created during ablc startup"
    exit 0
fi

BOOT_STATUS=$(grep '^boot_status:' "$BOOT_STATUS_PATH" | awk '{print $2}')
SKIP_SERVICES=""

if [ "$BOOT_STATUS" = "critical" ]; then
    log "I" "BOOT_STATUS is critical - skipping all generation"
    set_boot_status "critical"
    exit 0
fi

if [ "$BOOT_STATUS" = "failed" ]; then
    in_target_section=""
    while IFS= read -r line; do
        if [[ "$line" =~ ^new_packages: ]]; then
            in_target_section="new"
            continue
        elif [[ "$line" =~ ^[a-z_]+: ]]; then
            in_target_section=""
            continue
        fi

        if [[ "$in_target_section" == "new" ]] && [[ "$line" =~ ^[[:space:]]*-[[:space:]]*(.+)$ ]]; then
            pkg=$(echo "${BASH_REMATCH[1]}" | xargs)
            SKIP_SERVICES+="$(echo "$pkg" | xargs) "
        fi
    done < "$BOOT_STATUS_PATH"
fi

for i in $(compgen -G "$PKGDIR/*"); do
    if [ "$BOOT_STATUS" = "success" ] || [ "$BOOT_STATUS" = "failed" ]; then
        if [ ! -L "$i" ]; then
            continue
        fi
    fi

    if ! test -d "$i" -a -r "$i/isu.cfg"; then
        log "E" "Cannot access essential ISU package data. Skipping $i"
        continue
    fi

    real_path=$(readlink -f "$i")
    pkg_name=$(basename "$real_path")

    if echo "$SKIP_SERVICES" | grep -qw "$pkg_name"; then
        continue
    fi

    isu_prepare "$i" || :
done

if [ "$BOOT_STATUS" = "success" ]; then
    set_boot_status "failed"
elif [ "$BOOT_STATUS" = "failed" ]; then
    set_boot_status "critical"
fi
