#!/bin/bash
#
# Univention Join
#  joins a server to an univention domain
#
# SPDX-FileCopyrightText: 2004-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

export PATH="$PATH:/sbin:/usr/sbin:/bin:/usr/bin"

. /usr/share/univention-lib/all.sh

if [ -d "$HOME" ]; then
	LOGFILE="$HOME/.univention-server-join.log"
else
	USERTMP="$(mktemp -d)"
	LOGFILE="$USERTMP/.univention-server-join.log"
fi
ADMINOPTIONS=()
ADMINOPTIONS+=(--logfile "$LOGFILE")

display_help() {
	display_header
	cat <<-EOL
	Syntax:
	  univention-server-join -hostname <hostname> -role <role> [options]
	  univention-server-join [--help] [--version]

	Options:
	  -role <role>:            [domaincontroller_master|domaincontroller_backup|
	                            domaincontroller_slave|memberserver]
	  -hostname <hostname>:    hostname of computer system
	  -domainname <domainname>:    domainname of computer system
	  -ip <ip-address>:        IP address of computer system
	  -netmask <netmask>:      Netmask of computer system
	  -mac <mac-address>:      MAC address of computer system

	  -bindaccount             UID for Univention Directory Manager (deprecated)
	  -binddn                  DN for Univention Directory Manager
	  -bindpwfile:             Password for Univention Directory Manager

	  -h | --help | -?:        print this usage message and exit program
	  --version:               print version information and exit program

	Description:
	  univention-server-join joins a server to an univention domain,
	  e.g. univention-server-join -role memberserver -hostname membsrv -ip 1.2.3.4

	EOL
}

display_header() {
	echo "univention-server-join: joins a server to an univention domain"
	echo "copyright (c) 2001-@%@copyright_lastyear@%@ Univention GmbH, Germany"
	echo ""
}

display_version() {
	echo "univention-server-join @%@package_version@%@"
}

log() {
	if [ "$1" = 1 ]; then
		shift
		echo "$@"				>>"$LOGFILE"
		echo "$@"
	else
		shift
		echo "$@"				>>"$LOGFILE"
	fi
}

DN1 () { sed -ne 's/^DN: //p;T;q'; }

MAC=()
BINDDN=""
BINDPWFILE=""
DOMAINNAME=""
log 0 "univention-server-join called"
echo "Parameter: $@" >>"$LOGFILE"

set -o pipefail

while [ $# -gt 0 ]
do
	case "$1" in
		"-role")
			ROLE="${2:?missing role}"
			shift 2 || exit 2
			;;
		"-hostname")
			NEWHOSTNAME="${2:?missing host name}"
			shift 2 || exit 2
			;;
		"-domainname")
			DOMAINNAME="${2:?missing domain name}"
			shift 2 || exit 2
			;;
		"-ip")
			IP="${2:?missing IP address}"
			shift 2 || exit 2
			;;
		"-netmask")
			NETMASK="${2:?missing netmask}"
			shift 2 || exit 2
			;;
		"-certs")
			CERTS="${2:?missing certificate}"
			shift 2 || exit 2
			;;
		"-mac")
			MAC+=("${2:?missing ethernet MAC address}")
			shift 2 || exit 2
			;;
		"-bindaccount")
			BINDACCOUNT="${2:?missing account name for bind}"
			shift 2 || exit 2
			;;
		"-binddn")
			BINDDN="${2:?missing bind DN}"
			shift 2 || exit 2
			;;
		"-bindpwfile")
			BINDPWFILE="${2:?missing password file for bind}"
			shift 2 || exit 2
			;;
		"-position")
			POSITION="${2:?missing LDAP position}"
			shift 2 || exit 2
			;;
		"--version")
			display_version
			exit 0
			;;
		"--help"|"-h"|"-?")
			display_help
			exit 0
			;;
		*)
			display_help
			exit 1
			;;
	esac
done

# extend options for univention-directory-manager
if [ -z "$BINDDN" ]; then
	if [ -n "$BINDACCOUNT" ]; then
		# FIXME: This not longer works with anonymous bind disabled!
		BINDDN="$(ldapsearch -x "(&(uid=$BINDACCOUNT)(objectclass=posixAccount))" dn | ldapsearch-wrapper | sed -ne 's|dn: ||p')"
		log 0 "found BINDDN: $BINDDN" >>"$LOGFILE"
		if [ -z "$BINDDN" ]; then
			log 1 "E: failed to get binddn for $BINDACCOUNT"
			exit 1
		fi
	fi
fi

if [ -n "$BINDDN" ]; then
	ADMINOPTIONS+=(--binddn "$BINDDN")
fi
if [ -n "$BINDPWFILE" ]; then
	ADMINOPTIONS+=(--bindpwdfile "$BINDPWFILE")
fi


eval "$(univention-config-registry shell)"
if [ -z "$ROLE" ]; then
	display_help
	log 1 "E: 	-role is missing"
	exit 1
fi
if [ -z "$NEWHOSTNAME" ]; then
	display_help
	log 1 "E: 	-hostname is missing"
	exit 1
fi

if [ -z "$DOMAINNAME" ]; then
	DOMAINNAME="$domainname"
fi

display_header
create_entry () {
	local desc="${1?:missing description}"
	local module="${2?:missing computer module}"
	local position="${3?:missing LDAP position}"
	local primaryGroup="$4"
	local group="$5"
	log 0 "Join $desc"
	local rc
	local old_dn
	local udm_object
	local conflict_dn
	local conflict_host
	local conflict_module
	local udm_object2
	local conflict_dn2
	local conflict_host2
	local udm_object_owned_macs
	local udm_object_owns_ip
	local pre_check_conflicts
	pre_check_conflicts=()

	### check for conflicts
	udm_object="$(univention-directory-manager "$module" list --filter name="$NEWHOSTNAME" "${ADMINOPTIONS[@]}")"
	rc=$?
	old_dn="$(DN1 <<<"$udm_object")"
	if [ $rc -gt 0 ]; then
		log 1 "E: failed search $desc [$old_dn]"
		exit 1
	elif [ -z "$old_dn" ]; then
		udm_object="$(univention-directory-manager computers/computer list --filter name="$NEWHOSTNAME" "${ADMINOPTIONS[@]}")"
		conflict_dn="$(DN1 <<<"$udm_object")"
		if [ -n "$conflict_dn" ]; then
			## try to convert DN to short name
			conflict_module="$(univention-ldapsearch -xLLL -s base -b "$conflict_dn" univentionObjectType | sed -ne 's|univentionObjectType: ||p')"
			if [ -n "$conflict_module" ]; then
				udm_object="$(univention-directory-manager "$conflict_module" list --position "$conflict_dn" --filter name="$NEWHOSTNAME" "${ADMINOPTIONS[@]}")"
				conflict_host="$(sed -n 's/^ *name: //p' <<<"$udm_object")"
			else
				conflict_host="$conflict_dn"
			fi
			pre_check_conflicts+=("E: Host name $NEWHOSTNAME is already assigned to a computer object of different role, please use a different name or remove that object.")
		fi
	fi

	if [ -n "${MAC[0]}" ]; then
		udm_object_owned_macs="$(sed -n 's/^ *mac: //p' <<<"$udm_object")"
		for i in "${MAC[@]}"; do
			if ! grep --quiet "${udm_object_owned_macs,,}" <<< "${i,,}"; then
				## check if some other computer object owns this MAC address
				udm_object2="$(univention-directory-manager computers/computer list --filter mac="$i" "${ADMINOPTIONS[@]}")"
				conflict_dn2="$(DN1 <<<"$udm_object2")"
				if [ -n "$conflict_dn2" ]; then
					## try to convert DN to short name
					conflict_module="$(univention-ldapsearch -xLLL -s base -b "$conflict_dn2" univentionObjectType | sed -ne 's|univentionObjectType: ||p')"
					if [ -n "$conflict_module" ]; then
						udm_object2="$(univention-directory-manager "$conflict_module" list --position "$conflict_dn2" --filter mac="$i" "${ADMINOPTIONS[@]}")"
						conflict_host2="$(sed -n 's/^ *name: //p' <<<"$udm_object2")"
					else
						conflict_host2="$conflict_dn2"
					fi
					pre_check_conflicts+=("E: MAC address $i is already assigned to $conflict_host2, please remove that object or change the MAC address.")
				fi
			elif [ -n "$conflict_host" ]; then
				## MAC found and udm_object has same name
				pre_check_conflicts+=("E: MAC address $i is already assigned to $conflict_host.")
			fi
		done
	fi

	if [ -n "$IP" ]; then
		udm_object_owns_ip="$(sed -n 's/^ *ip: //p' <<<"$udm_object")"
		if [ "$udm_object_owns_ip" != "$IP" ]; then
			## check if some other computer object owns this IP address
			udm_object2="$(univention-directory-manager computers/computer list --filter ip="$IP" "${ADMINOPTIONS[@]}")"
			conflict_dn2="$(DN1 <<<"$udm_object2")"
			if [ -n "$conflict_dn2" ]; then
				## try to convert DN to short name
				conflict_module="$(univention-ldapsearch -xLLL -s base -b "$conflict_dn2" univentionObjectType | sed -ne 's|univentionObjectType: ||p')"
				if [ -n "$conflict_module" ]; then
					udm_object2="$(univention-directory-manager "$conflict_module" list --position "$conflict_dn2" --filter ip="$IP" "${ADMINOPTIONS[@]}")"
					conflict_host2="$(sed -n 's/^ *name: //p' <<<"$udm_object2")"
				else
					conflict_host2="$conflict_dn2"
				fi
				pre_check_conflicts+=("E: IP address $IP is already assigned to $conflict_host2, please remove that object or change the IP address.")
			fi
		elif [ -n "$conflict_host" ]; then
			## IP found and udm_object has same name
			pre_check_conflicts+=("E: IP address $IP is already assigned to $conflict_host.")
		fi
	fi
	if [ ${#pre_check_conflicts[@]} -gt 0 ]; then
		log 1 "${pre_check_conflicts[*]}"
		exit 1
	fi
	### check for conflicts done

	args=()
	if [ -z "$old_dn" ]; then
		log 0 "	Create new $desc "

		if [ -n "$IP" ]; then
			args+=(--set ip="$IP")
			# DNS
			if [ -n "$forwardZone" ]; then
				args+=(--set dnsEntryZoneForward="$forwardZone")
				if [ -n "$reverseZone" ]; then
					args+=(--set dnsEntryZoneReverse="$reverseZone")
				fi
			fi
		fi
		if [ -n "$MAC" ]; then
			for i in "${MAC[@]}"; do
				args+=(--set mac="$i")
			done
		fi

		cmd=(univention-directory-manager "$module" create \
			--position "$position" \
			--set name="$NEWHOSTNAME" \
			--set domain="$DOMAINNAME" \
			--set password="$computerPassword" \
			--set unixhome=/dev/null \
			--set shell=/bin/sh \
			--set primaryGroup="$primaryGroup" \
			"${args[@]}" "${ADMINOPTIONS[@]}")
		#log 0 "${cmd[@]}"
		if ! rc="$("${cmd[@]}" 2>&1 | grep -v 'WARNING:')"
		then
			log 1 "E: failed to create $desc (1) [$rc]"
			exit 1
		fi

		if [ -z "$rc" ]; then
			log 1 "E: failed to create $desc: no result"
			exit 1
		fi

		ldap_dn="$(echo $rc | sed -ne 's|Object created: ||p')"
		if [ -z "$ldap_dn" ]; then
			log 1 "E: failed to create $desc (2) [$rc]"
			exit 1
		fi

		echo "ldap_dn=\"$ldap_dn\""

		if [ -n "$group" ]; then
			rc="$(univention-directory-manager groups/group modify \
				--dn="$group" \
				--append users="$ldap_dn" \
				"${ADMINOPTIONS[@]}" 2>&1 | grep -v 'WARNING:')"
			if [ $? -gt 0 ]; then
				log 1 "E: failed to modify groups/group for $desc [$rc]"
				exit 1
			fi
		fi
	else
		log 0 "Modify $desc [$old_dn]"

		if [ -n "$MAC" ]; then
			for i in "${MAC[@]}"; do
				args+=(--set mac="$i")
			done
		fi
		if [ -n "$IP" ]; then
			args+=(--set ip="$IP")
		fi
		rc="$(univention-directory-manager "$module" modify \
			--dn "$old_dn" \
			--set password="$computerPassword" \
			--set domain="$DOMAINNAME" \
			"${args[@]}" "${ADMINOPTIONS[@]}" 2>&1 | grep -v 'WARNING:')"
		if [ $? -gt 0 ]; then
			log 1 "E: failed to modify $desc $old_dn [$rc]"
			exit 1
		fi

		echo "ldap_dn=\"$old_dn\" "
	fi

	# Invalidate caches
	if [ "$USER" = "root" ]
	then
		sss_cache -U || true
		# sss_cache -G || true
		nscd -i group || true
		nscd -i hosts || true
	fi
}

if [ -n "$IP" ]; then
	if [ -n "$NETMASK" ]; then
		subnet="$(univention-ipcalc6 --ip "$IP" --netmask "$NETMASK" --output reverse --calcdns)"
	else
		# Fallback
		var_primary_netmask="interfaces_${interfaces_primary:-eth0}_netmask"
		subnet="$(univention-ipcalc6 --ip "$IP" --netmask "${!var_primary_netmask}" --output reverse --calcdns)"
	fi
	log 0 "	Calculated subnet = $subnet"

	forwardZone="$(univention-directory-manager dns/forward_zone list \
		--filter zone="$DOMAINNAME" \
		"${ADMINOPTIONS[@]}" | DN1)"
	reverseZone="$(univention-directory-manager dns/reverse_zone list \
		--filter subnet="$subnet" \
		"${ADMINOPTIONS[@]}" | DN1)"
	dhcpEntry="$(univention-directory-manager dhcp/service list \
		--filter name="$DOMAINNAME" \
		"${ADMINOPTIONS[@]}" | DN1)"

	log 0 "	forwardZone $forwardZone"
	log 0 "	reverseZone $reverseZone"
	log 0 "	dhcpEntry $dhcpEntry"
fi

computerPassword="$(create_machine_password)"

case "$ROLE" in
domaincontroller_master)
	create_entry "Primary Directory Node" "computers/domaincontroller_master" \
		"${POSITION:-cn=dc,cn=computers,$ldap_base}" \
		"cn=DC Backup Hosts,cn=groups,$ldap_base"
	;;
domaincontroller_backup)
	create_entry "Backup Directory Node" "computers/domaincontroller_backup" \
		"${POSITION:-cn=dc,cn=computers,$ldap_base}" \
		"cn=DC Backup Hosts,cn=groups,$ldap_base" \
		"cn=DC Slave Hosts,cn=groups,$ldap_base"
	;;
domaincontroller_slave)
	create_entry "Replica Directory Node" "computers/domaincontroller_slave" \
		"${POSITION:-cn=dc,cn=computers,$ldap_base}" \
		"cn=DC Slave Hosts,cn=groups,$ldap_base"
	;;
memberserver)
	create_entry "Managed Node" "computers/memberserver" \
		"${POSITION:-cn=memberserver,cn=computers,$ldap_base}" \
		"cn=Computers,cn=groups,$ldap_base"
	;;
*)
	log 1 "E: 	-role $ROLE is unknown"
	exit 1
esac

echo "KerberosPasswd=\"$computerPassword\" "
# vim:set sw=4 ts=4 noet:
