# OpenStack Nova Instance Metadata
# vim:set filetype=sh:
# shellcheck shell=sh

: "${OPENSTACK_IMDS_FORMAT:=auto}"
unset OPENSTACK_SELECTED_FORMAT OPENSTACK_SELECTED_SOURCE IMDS_DOCUMENT_DIR

_openstack_select_format() (
	local format lock="$TINY_CLOUD_RUN/openstack.lock" attempts=0 tmpdir
	local marker="$TINY_CLOUD_RUN/openstack-format"

	case "$OPENSTACK_IMDS_FORMAT" in
		auto|native|ec2) ;;
		*) echo "imds: invalid OpenStack format '$OPENSTACK_IMDS_FORMAT'" >&2
			return 1;;
	esac

	exec 9>"$lock" || return
	while ! flock -n 9; do
		attempts=$((attempts + 1))
		if [ "$attempts" -ge 300 ]; then
			echo 'imds: timed out waiting for OpenStack selection' >&2
			return 1
		fi
		sleep 0.1
	done
	if [ -f "$marker" ]; then
		format=$(cat "$marker") || return
	else
		tmpdir=$(mktemp -d "$TINY_CLOUD_RUN/openstack-select.XXXXXX") || return
		trap 'rm -rf "$tmpdir"' 0
		trap 'exit 1' HUP INT TERM
		format="$OPENSTACK_IMDS_FORMAT"
		if [ "$format" = auto ]; then
			IMDS_URI=openstack
			if _imds '' /dev/null 2>"$tmpdir/error"; then
				format=native
			elif [ "$WGET_STATUS" = 404 ]; then
				format=ec2
			else
				cat "$tmpdir/error" >&2
				return 1
			fi
			if [ "$format" = ec2 ]; then
				IMDS_URI=latest
				_imds '' /dev/null || return
			fi
		fi
		printf '%s\n' "$format" > "$tmpdir/format" &&
			mv "$tmpdir/format" "$marker" || return
	fi
	case "$format" in
		native|ec2) printf '%s\n' "$format";;
		*) echo 'imds: invalid saved OpenStack format' >&2; return 1;;
	esac
)

_imds_init() {
	[ -z "$OPENSTACK_SELECTED_FORMAT" ] || return 0

	case "$OPENSTACK_IMDS_FORMAT" in
		auto|native|ec2) ;;
		*) echo "imds: invalid OpenStack format '$OPENSTACK_IMDS_FORMAT'" >&2; return 1;;
	esac
	OPENSTACK_SELECTED_SOURCE=$(_openstack_select_source) || return
	if [ "$OPENSTACK_SELECTED_SOURCE" = configdrive ]; then
		IMDS_DOCUMENT_DIR="$TINY_CLOUD_RUN/openstack-configdrive"
		OPENSTACK_SELECTED_FORMAT=$(cat "$IMDS_DOCUMENT_DIR/format") || return
		# Resolve disk paths locally; never fill missing files from HTTP.
		_imds() {
			local path="$1"
			if [ "$OPENSTACK_SELECTED_FORMAT" = ec2 ]; then
				case "$path" in
					meta-data) path=meta-data.json/;;
					meta-data/*) path="meta-data.json/${path#meta-data/}";;
				esac
			fi
			_imds_document "$path"
		}
	else
		OPENSTACK_SELECTED_FORMAT=$(_openstack_select_format) || return
	fi
	case "$OPENSTACK_SELECTED_FORMAT" in
		native)
			: "${IMDS_API_VERSION:=latest}"
			IMDS_URI="openstack/$IMDS_API_VERSION"
			IMDS_HOSTNAME="meta_data.json/hostname"
			IMDS_LOCAL_HOSTNAME="$IMDS_HOSTNAME"
			IMDS_SSH_KEYS="meta_data.json/public_keys"
			IMDS_USERDATA="user_data"
			if [ "$OPENSTACK_SELECTED_SOURCE" = http ]; then
				IMDS_DOCUMENT_DIR=$(mktemp -d "$TINY_CLOUD_RUN/openstack.XXXXXX") || {
					unset OPENSTACK_SELECTED_FORMAT
					return 1
				}
				trap 'rm -rf "$IMDS_DOCUMENT_DIR"' 0
				trap 'exit 1' HUP INT TERM
			fi
			;;
		ec2)
			IMDS_URI=latest
			;;
	esac
	if [ -n "$IMDS_DOCUMENT_DIR" ]; then
		[ "$OPENSTACK_SELECTED_FORMAT" != ec2 ] || IMDS_SSH_KEYS=meta-data.json/public-keys

		_imds_ssh_keys() {
			local keys key value result=
			keys=$(_imds "$IMDS_SSH_KEYS") || return
			while IFS= read -r key; do
				[ "$OPENSTACK_SELECTED_FORMAT" != ec2 ] || key="$key/openssh-key"
				value=$(_imds_document "$IMDS_SSH_KEYS/$key") || return
				result="${result}${value}
"
			done <<-EOT
				$keys
			EOT
			printf '%s' "$result" | sort -u
		}
	fi
}

_imds_document() {
	local path="$1" document file tmpfile selector value

	case "$path" in
		''|/*) echo "imds: invalid document path '$path'" >&2; return 1;;
	esac
	case "/$path/" in
		*/../*|*/./*)
			echo "imds: invalid document path '$path'" >&2; return 1;;
	esac
	document="${path%%/*}"
	case "$document" in
		*[!A-Za-z0-9_.-]*)
			echo "imds: invalid document name '$document'" >&2; return 1;;
	esac
	file="$IMDS_DOCUMENT_DIR/$document"
	if [ ! -f "$file" ]; then
		[ "$OPENSTACK_SELECTED_SOURCE" != configdrive ] || return 1
		tmpfile=$(mktemp "$file.XXXXXX") || return
		_imds "$document" "$tmpfile" && mv "$tmpfile" "$file" || {
			rm -f "$tmpfile"
			return 1
		}
	fi
	if [ "$path" = "$document" ]; then
		cat "$file"
		return
	fi

	selector="${path#*/}"
	set --
	while [ -n "$selector" ]; do
		set -- "$@" "${selector%%/*}"
		case "$selector" in
			*/*) selector="${selector#*/}";;
			*) break;;
		esac
	done
	value=$(yx -f "$file" -- "$@") || return
	case "$value" in
		''|null) echo "imds: empty metadata value '$path'" >&2; return 1;;
	esac
	printf '%s\n' "$value"
}

# Nova does not expose AWS's per-interface metadata tree.
unset \
	IMDS_NICS \
	IMDS_MAC \
	IMDS_IPV4 \
	IMDS_IPV6 \
	IMDS_IPV4_NET \
	IMDS_IPV6_NET \
	IMDS_IPV4_PREFIX \
	IMDS_IPV6_PREFIX
