#!/bin/bash
#
# Univention LDAP Server
#  converts a UCS Backup Directory Node into a UCS Primary Directory Node
#
# Copyright 2001-2021 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

LOGFILE="/var/log/univention/backup2master.log"

opt_force=0
opt_wait=30
opt_dry_run=0
opt_old_master=
opt_reference=
opt_verbose=0

Usage() {
  local message=$1
  printf "Usage:\n  %s: [-f] [-h] [-v] [-w 0]\n\n" $( basename $0 )
  printf "Example:\n"
  printf "  %s -h\n\n" $( basename $0 )
  printf "  This script converts a UCS Backup Directory Node into a UCS Primary Directory Node.\n"
  printf "  When using it, you need root privileges.\n"
  printf "  If you invoke it without any options, it allows cancelling before it proceeds.\n"
  printf "  Choose among the options as needed. Default values are listed in parentheses.\n"
  printf "\n"
  printf "Options:\n"
  printf "  -f             force execution of script, even if checks fail ($opt_force).\n"
  printf "  -h             do not convert UCS Backup Node, but print this helpful text\n"
  printf "  -v             enable verbose log output ($opt_verbose).\n"
  printf "  -w seconds     set time to wait before starting conversion ($opt_wait).\n"
  printf "\n"
  if ! test -z "$message" ; then
    printf "\n"
    printf "$message\n"
  fi
}

# A function to detect references to host names in LDAP and allow
# to resolve these references interactively.
# This function uses udm and will work correctly only if an LDAP
# Primary is reachable.
# It looks for all occurrences of the attribute in the module.
# For each such occurrence an interactive change is offered.
# Notice that (while searching) the names are matched instead
# of checked for identity. Accordingly, the change of the old
# name into the new name is not a "setting" but a textual "replacement".
# This is necessary because such changes can occur in SRV records
# and we want the settings of the SRV record (port prio) preserved.
resolve_reference() {
  local module=$1
  local attribute=$2
  local old_ldap_master=$3
  local ldap_master=$4
  local udm_options=$5
  if [ "$#" -ne 5 ] ; then
    echo "resolve_reference received $# parameters and not 5 as expected"
    exit
  fi

  # Search for the DNs of all UDM entries of this module containing the attribute.
  # Save DNs in a temporary file (line by line) and assign fd 5 to the file.
  # This is necessary because fd 0 is already used by user-interaction.
  domains=$(mktemp)
  udm $module list $udm_options |\
    awk -vold=$old_ldap_master -vattr=$attribute \
      '/^DN:/ { $1=""; sub(/^[ ]+/, "") ; DN=$0 ; next };
       $1 ~ attr && $0 ~ old { print DN }
      ' | uniq > $domains
  # Open temporary file containing DNs and assign fd 5 to it.
  exec 5<> $domains
  # Remember that LDAP DNs may contain blank characters.
  # So the DN may possibly span several fields.
  # Beware when quoting DN.
  while read -u 5 DN; do
	while [ 1 ]; do 
      old_value=$( udm $module list $udm_options | \
        awk -vold=$old_ldap_master -vdn="$DN" -vattr=$attribute \
          '/^DN:/ { $1=""; sub(/^[ ]+/, "") ; DN=$0 ; next };
           dn==DN && $1 ~ attr && $0 ~ old { $1=""; sub(/^[ ]+/, ""); print $0 ; exit }
          ' )
      if [ -z "$old_value" -o "$last_answer" = n ]; then
		last_answer=""
      	break
      fi

      echo ""
      echo "udm $module (attribute $attribute) contains a reference to $old_ldap_master in $DN"
      new_value=${old_value//$old_ldap_master/$ldap_master}
      echo
      echo "Do you want this reference to be changed from"
      echo "  \"$old_value\""
      echo "to"
      echo "  \"$new_value\""
      # When changing references to hosts in udm module shares/share first
      # check if the path already exists on the new host.
      if [ "$module" = "shares/share" ] ; then
        echo "[Y|n|remove]? "
        udm $module list |\
          awk -vdn="$DN" -vtarget=$new_value '
            /^DN:/ { $1=""; sub(/^[ ]+/, "") ; DN=$0 ; next };
            $1 == "host:" { host = $2 }
            $1 == "path:" { path = $2 }
            dn == DN && path { old_path = path }
            host && path { count [ host ":" path ] ++ ; host=path="" }
            END {
              if ( (target ":" old_path) in count )
                print "WARNING: path", old_path, "already exists on new host", target
            }
          '
      else
        # Removal not offered (only for shares).
        echo -n "[Y|n]? "
      fi
      while true; do
        read answer
		# Set default to y
		if [ -z "$answer" ]; then
			answer=y
		fi
        case $answer in
          [yY] )
            echo "Ok, changing $attribute in $DN"
            if [ "$module" = "shares/share" -o "$attribute" = "mailHomeServer" ] ; then
              # some attributes are single values, like hosts or mailHomeSevers
              udm $module modify --dn "$DN" --set $attribute="$new_value"
            else 
              # other modules can have the same attribute several times
              udm $module modify --dn "$DN" --append $attribute="$new_value"
              udm $module modify --dn "$DN" --remove $attribute="$old_value"
            fi
            break ;;
          [nN] )
			last_answer=n
            echo "Ok, leaving $DN unchanged"
            break ;;
          remove )
            if [ "$module" = "shares/share" ] ; then
              echo "Ok, removing $n";
              udm $module remove --dn "$DN"
              break
            else
              echo "removal is offered only with udm module shares/share" 
              echo "changing nothing, try again [Y|n]? " 
            fi
            ;;
          * )
            if [ "$module" = "shares/share" ] ; then
              echo -n "[Y|n|remove]? "
            else
              echo -n "[Y|n]? "
            fi
            ;;
        esac
      done
    done
  done 
  # Close fd 5 (domain names) and remove the file.
  exec 5>&-
  rm -f "$domains"
}

STILL_RUNNING=1

consider_termination() {
  # If the script terminates properly, then keep quiet.
  if [ -z "$STILL_RUNNING" ]; then
    exit
  fi
  echo "$(basename "$0") manually interrupted" | tee -a "$LOGFILE"
  echo ""
  echo "You have requested termination of this script at"
  echo "a time when the script has not finished properly."
  echo "Termination at this point will leave the server"
  echo "in an undefined state. You should reconsider to"
  echo "continue the script. If you still choose to end"
  echo "the script, you are left on your own with a server"
  echo "in a state that is (at best) undefined and (at worst)"
  echo "incomplete and therefore unusable."

  while true ; do
    echo ""
    read -p "Do you want to continue with converting this server into a Primary Directory Node [Y|n]? " answer 2>&1
    case $answer in
      [yY] )
        echo "Ok, continue" | tee -a "$LOGFILE"
        return ;;
      [nN] )
        echo "Ok, terminating as requested, leaving new Primary Directory Node incomplete." | tee -a "$LOGFILE"
        STILL_RUNNING=
        exit ;;
     * )     ;;
    esac
  done
}

# log call
echo "$(LC_ALL=C date): started $(basename $0)" >>"$LOGFILE"
echo "$(LC_ALL=C date): with options $*" >>"$LOGFILE"

# save errors in log file
exec 2>>"$LOGFILE"

while getopts dfhvw: name
do
  case "$name" in
    f)  opt_force=1                 ;;
    h)  Usage ; exit                ;;
    v)  opt_verbose=1               ;;
    w)  opt_wait="${OPTARG}"        ;;
  esac
done
shift $(($OPTIND - 1))
if [ $# -ne 0 ] ; then
  Usage;
  printf "You passed obscure arguments: \"%s\"\n" "$*"
  exit 2
fi

# Check if the script univention-config-registry can be executed.
if ! univention-config-registry -v >>"$LOGFILE"; then
  printf "ERROR: This is not a proper UCS host (cannot find command univention-config-registry). Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi
fi

# Check if the current user has root permissions.
if [ $( id -u ) -ne 0 ]; then
  printf "ERROR: You are user $( id -u ) and not root, so you are not allowed to do this. Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi
fi

# Read all Univention variables into shell variables.
eval "$(univention-config-registry shell)"

# Check if the current host has been configured to become Primary Directory Node
if [ "$server_role" != "domaincontroller_backup" ]; then
  printf "ERROR: univention-backup2master can only be started on a Backup Directory Node. Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  # Does it make sense to allow enforcement on a Primary ?
  # Yes, immediately after conversion, we use this script to report remainders.
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi
fi

# check if system is joined
if [ ! -e /var/univention-join/joined ]; then
  printf "ERROR: The system is not joined to an UCS domain. Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi 
fi

# check Primary DNS
if ! host $ldap_master >>"$LOGFILE"; then
  printf "ERROR: LDAP Primary DNS name is not resolvable. Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi
fi

# try to download univention-server-master
apt-get update >>"$LOGFILE"
apt-get -y -d install univention-server-master >>"$LOGFILE"
if [ -z "$(ls /var/cache/apt/archives/univention-server-master_*.deb)" ]; then
  printf "ERROR: Could not download package univention-server-master. Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi
fi

# Try to connect to Primary Directory Node, check if connection is possible
if nc -z $ldap_master $ldap_master_port >>"$LOGFILE"; then
  printf "ERROR: The LDAP Primary Directory Node host ($ldap_master:$ldap_master_port) is still running. Use -f to proceed anyway\n" | tee -a "$LOGFILE"
  if [ $opt_force -eq 0 ] ; then
    exit 1
  fi
fi

# turn on verbose logging
set -x

# The environment seems proper, now start the actual conversion to Primary.
echo
echo "univention-backup2master allows the Backup Directory Node to take over the Primary Directory Node role."
echo
echo "This tool will wait here for $opt_wait seconds..."
echo "Press CTRL-c to abort or press ENTER to continue"
read -t $opt_wait somevar

# From now on this script should not be terminated before proper completion.
trap "consider_termination" TERM EXIT QUIT INT HUP

#remove replication listener module
dpkg-divert --divert /usr/lib/univention-directory-listener/replication.py.divert --rename --add /usr/lib/univention-directory-listener/system/replication.py

# stop openldap, samba, kerberos, listener and notifier
test -x /etc/init.d/slapd && invoke-rc.d slapd stop
test -x /etc/init.d/samba && /etc/init.d/samba stop
test -x /etc/init.d/samba-ad-dc && invoke-rc.d samba-ad-dc stop
test -x /etc/init.d/heimdal-kdc && invoke-rc.d heimdal-kdc stop
systemctl stop univention-directory-notifier
systemctl stop univention-directory-listener

old_ldap_master="$ldap_master"
# set config registy variables
univention-config-registry set \
	ldap/master="$hostname.$domainname" \
	ldap/server/type=master \
	server/role=domaincontroller_master \
	kerberos/adminserver="$hostname.$domainname" \
	kerberos/kpasswdserver="$hostname.$domainname" \
	windows/wins-support=yes \
	ldap/translogfile=/var/lib/univention-ldap/listener/listener

if [ -e /etc/univention/ssl/ucsCA/CAcert.pem ]; then
	cp /etc/univention/ssl/ucsCA/CAcert.pem /var/www/ucs-root-ca.crt
fi

# start openldap, samba, kerberos, notifier and listener
test -x /etc/init.d/slapd && invoke-rc.d slapd start
test -x /etc/init.d/samba && /etc/init.d/samba start
test -x /etc/init.d/samba-ad-dc && invoke-rc.d samba-ad-dc start
test -x /etc/init.d/heimdal-kdc && invoke-rc.d heimdal-kdc start
systemctl start univention-directory-notifier
systemctl start univention-directory-listener

/usr/share/univention-directory-manager-tools/univention-dnsedit --ignore-exists "$domainname" add srv kerberos-adm tcp 0 100 88 "$hostname.$domainname."
/usr/share/univention-directory-manager-tools/univention-dnsedit --ignore-exists "$domainname" remove srv kerberos-adm tcp 0 100 88 "$old_ldap_master."

# Remove the S4 Connector service entry from the old Primary Directory Node
master_dn="$(ldapsearch -x -ZZ -D "$ldap_hostdn" -y /etc/machine.secret '(&(univentionServerRole=master)(univentionService=Samba 4)(univentionService=S4 Connector))' dn | sed -ne 's|dn: ||p')"
if [ -n "$master_dn" ]; then
	univention-directory-manager computers/domaincontroller_master modify --dn "$master_dn" --remove service="S4 Connector"
	if [ "$(dpkg-query -W -f='${Status}\n' univention-s4-connector 2>/dev/null)" = "install ok installed" ]; then
		# reconfigure S4 connector
		ucr unset connector/s4/autostart
		cat /var/univention-join/status | grep -v "^univention-s4-connector " > /var/univention-join/status.temp
		mv /var/univention-join/status.temp /var/univention-join/status
	else
		echo
		echo "WARNING: The S4 Connector is not installed on this system but the S4 Connector was installed and configured on the old Primary Directory Node."
		echo
	fi
fi

# Does the server possess the attribute univentionObjectType in LDAP ?
# UCS versions prior to 3 did not know about this.
# Find out and add the attribute univentionObjectType to the
# objectClass univentionObject if necessary.
found_univentionObject=$( ldapsearch -x -ZZ -D "$ldap_hostdn" -b "$ldap_hostdn" -y /etc/machine.secret -LLL | awk '$1=="objectClass:" && $2=="univentionObject"' )
if [ -z "$found_univentionObject" ]; then
  temp_file=$(mktemp)
  echo "dn: ${ldap_hostdn}"                                      >> "$temp_file"
  echo "changetype: modify"                                      >> "$temp_file"
  echo "add: objectClass"                                        >> "$temp_file"
  echo "objectClass: univentionObject"                           >> "$temp_file"
  echo "-"                                                       >> "$temp_file"
  echo "add: univentionObjectType"                               >> "$temp_file"
  echo "univentionObjectType: computers/domaincontroller_master" >> "$temp_file"

  ldapmodify -x -D "cn=admin,$ldap_base" -w "$(cat /etc/ldap.secret)" -f "$temp_file"
  rm $temp_file
fi

# set ServerRole to master
temp_file=$(mktemp)
echo "dn: ${ldap_hostdn}" >>"$temp_file"
echo "changetype: modify" >>"$temp_file"
echo "replace: univentionServerRole" >>"$temp_file"
echo "univentionServerRole: master" >>"$temp_file"
echo "-" >>"$temp_file"
echo "replace: univentionObjectType" >>"$temp_file"
echo "univentionObjectType: computers/domaincontroller_master" >>"$temp_file"

ldapmodify -x -D "cn=admin,$ldap_base" -w "$(cat /etc/ldap.secret)" -f "$temp_file"

rm "$temp_file"

srv_dn=$(univention-directory-manager dns/srv_record list --superordinate zoneName="$domainname,cn=dns,$ldap_base" --filter relativeDomainName="_domaincontroller_master._tcp" | sed -ne 's|DN: ||p')
univention-directory-manager dns/srv_record modify --superordinate zoneName="$domainname,cn=dns,$ldap_base" --dn "$srv_dn" --set location="0 0 0 $hostname.$domainname."

old_ldap_master_hostname=$(echo "$old_ldap_master" | awk -F '.' '{print $1}')

## =========================== <remove Samba objects> ===========================
samdb='/var/lib/samba/private/sam.ldb'

remove_NTDS_objectGUID_alias() {
	local NTDS_objectGUID="$1"
	if [ -n "$NTDS_objectGUID" ]; then
		NTDS_alias_dn=$(univention-ldapsearch relativeDomainName="$NTDS_objectGUID._msdcs" | ldapsearch-wrapper | sed -n 's/^dn: //p')
		if [ -n "$NTDS_alias_dn" ]; then
			univention-directory-manager dns/alias remove --superordinate "${NTDS_alias_dn#*,}" --dn "$NTDS_alias_dn"
		fi
	fi
}

samdb_remove_dc_account(){
	local machine_name="$1"
	if [ -n "$machine_name" ]; then
		machine_samaccountname="$machine_name\$"
		machine_ldif=$(univention-s4search --controls domain_scope:0 \
					sAMAccountName="$machine_samaccountname" serverReferenceBL | ldapsearch-wrapper)
		machine_dn=$(echo "$machine_ldif" | sed -n 's/^dn: //p')

		if [ -n "$machine_dn" ]; then
			server_dn=$(echo "$machine_ldif" | sed -n 's/^serverReferenceBL: //p')
		fi

		if [ -z "$machine_dn" ] || [ -z "$server_dn" ]; then	## search again directly for objectClass=server
			server_ldif=$(univention-s4search -b "CN=Configuration,$samba4_ldap_base" --controls domain_scope:0 \
						"(&(objectClass=server)(name=$machine_name))" | ldapsearch-wrapper)
			server_dn=$(echo "$server_ldif" | sed -n 's/^dn: //p')
		fi

		if [ -n "$server_dn" ]; then
			NTDS_objectGUID=$(ldbsearch -H /var/lib/samba/private/sam.ldb -b "$server_dn" \
							"CN=NTDS Settings" objectGUID | sed -n 's/^objectGUID: \(.*\)/\1/p')
			remove_NTDS_objectGUID_alias "$NTDS_objectGUID"
		fi

		if [ -n "$server_dn" ]; then
			ldbdel -H "$samdb" --recursive "$server_dn"
		fi
		if [ -n "$machine_dn" ]; then
			ldbdel -H "$samdb" --recursive "$machine_dn"
		fi
	fi
}

udm_remove_dns_service_account(){
	local machine_name="$1"
	dns_service_account_dn=$(udm users/user list --filter "username=dns-$machine_name" | sed -n 's/^DN: //p')
	if [ -n "$dns_service_account_dn" ]; then
		univention-directory-manager users/user remove --dn "$dns_service_account_dn"
	fi
}

if [ -e "$samdb" ]; then
	# Move Samba4 FSMO roles to this host
	#  https://forge.univention.org/bugzilla/show_bug.cgi?id=26986
	if [ -x /usr/bin/samba-tool ]; then
		if samba-tool fsmo show | grep -qi "CN=${old_ldap_master_hostname},CN=Servers,CN=[^,]*,CN=Sites,CN=Configuration"; then
			samba-tool fsmo seize --role=all --force
		fi
	fi

	samdb_remove_dc_account "$old_ldap_master_hostname"
fi
## =========================== </remove Samba objects> ===========================

univention-directory-manager computers/domaincontroller_master remove --filter name="$old_ldap_master_hostname" --remove_referring

res=$(ldapsearch -x -ZZ -D "$ldap_hostdn" -y /etc/machine.secret -LLL "(&(relativeDomainName=univention-directory-manager)(cNameRecord=$old_ldap_master_hostname))")
if [ -n "$res" ]; then
	 /usr/share/univention-directory-manager-tools/univention-dnsedit --stoptls --overwrite "$domainname" add cname univention-directory-manager "$hostname"
fi

res=$(ldapsearch -x -ZZ -D "$ldap_hostdn" -y /etc/machine.secret -LLL "(&(relativeDomainName=univention-repository)(cNameRecord=$old_ldap_master_hostname))")
if [ -n "$res" ]; then
	 /usr/share/univention-directory-manager-tools/univention-dnsedit --stoptls --overwrite "$domainname" add cname univention-repository "$hostname"
fi

# Now that there is a new LDAP Primary, we can safely use resolve_reference.
# Change DNS nameserver
resolve_reference dns/forward_zone nameserver "$old_ldap_master." "$hostname.$domainname." ""

# Change nameserver of reverse zone for reverse DNS lookup
resolve_reference dns/reverse_zone nameserver "$old_ldap_master." "$hostname.$domainname." ""

# Run join scripts
univention-run-join-scripts 2>&1 | tee -a "$LOGFILE"

# Resolve any LDAP references to the old Primary
resolve_reference shares/share   host     $old_ldap_master_hostname $hostname ""
resolve_reference dns/srv_record location $old_ldap_master_hostname $hostname "--superordinate zoneName=$domainname,cn=dns,$ldap_base"

# Is the new Primary Directory Node a IMAP server?
res="$(ldapsearch -x -ZZ -D "$ldap_hostdn" -y /etc/machine.secret -LLL "(&(cn=$hostname)(objectClass=univentionDomainController)(univentionService=IMAP))")"
# Do we have any users who use the old Primary Directory Node as IMAP server?
res_users="$(ldapsearch -x -ZZ -D "$ldap_hostdn" -y /etc/machine.secret -LLL "(univentionMailHomeServer=$old_ldap_master)")"
if [ -n "$res" -a -n "$res_users" ]; then
	resolve_reference users/user     mailHomeServer  "$old_ldap_master_hostname"    "$hostname"      ""
elif [ -n "$res_users" ]; then
	echo
	echo "WARNING: The old Primary Directory Node was a IMAP server. This server is not a registered IMAP server."
	echo
fi

# Remove the reference to old Primary from krb5PrincipalName for LDAP access
ldapdelete -x -D "cn=admin,$ldap_base" -w "$(cat /etc/ldap.secret)" "krb5PrincipalName=ldap/${old_ldap_master}@${kerberos_realm},cn=kerberos,${ldap_base}"

# Install the package of the Primary at the same time when removing the package of the Backup Node.
# This way no dependent packages get removed.
printf "\nReplacing package univention-server-backup with univention-server-master.\n"
apt-get -y install univention-server-master univention-server-backup-

udm_remove_dns_service_account "$old_ldap_master_hostname"

# Any bind9 that might be running shall be restarted.
test -x /etc/init.d/bind9 && /etc/init.d/bind9 force-reload

# Tell trap handler to terminate silently. 
STILL_RUNNING=

exit 0

