B Updated dhclient Script

The dhclient-exit-hook-set-hostname.sh-ol8 is intended for use on Oracle Linux instances on Oracle Cloud Infrastructure that have been upgraded from Oracle Linux 7 to Oracle Linux 8. This script includes code for the proper handling of connectivity to those upgraded instances.

#!/bin/bash
#
# Copyright (C) 2022 Oracle. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2.  This program is distributed 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 General
# Public License for more details.  You should have received a copy of the GNU
# General Public License along with this program; if not, write to the Free
# Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 021110-1307, USA.

log () {
    logger -t "${0##*/}" "$*"
}

log "set hostname, /etc/hosts, /etc/resolv.conf begin $(date):"

OPC_CONF="/etc/oci-hostname.conf"

# import the oci-hostname configuration info
if [ -f $OPC_CONF ]; then
    . $OPC_CONF
fi

# ER-28862654 - Add custom header text to /etc/resolv.conf
if ! grep -q "docs.cloud.oracle.com" /etc/resolv.conf; then
    sed -i '1i; Any changes made to this file will be overwritten whenever the\
; DHCP lease is renewed. To persist changes you must update the\
; /etc/oci-hostname.conf file. For more information see\
;[https://docs.cloud.oracle.com/iaas/Content/Network/Tasks/managingDHCP.htm#notes]\
;' /etc/resolv.conf
fi

function retry_command()
{

  retry_attempts=30
  retry_interval_sec=2
  while [ "$retry_attempts" -gt 0 ]; do

    command_success=true
    "$@" || { command_success=false; }
    if [ "$command_success" == false ]; then
      (( retry_attempts-- ))
      log "Error occurred running command $@. Will retry in $retry_interval_sec seconds"
      sleep $retry_interval_sec
    else
      log "Successfully executed the command $@"
      break
    fi
  done

  # Check if issue running command still existed after all retry_attempts
  if [ "$command_success" == false ]; then
    log "ERROR: failed to execute command '$@' (Retried $retry_attempts times)"
    return 1
  fi
}

#Usage: add_entries <file name> <keyword> <an array of the corresponding values for the keyword>
#We pass array by name so if the array name is 'arr', pass it as 'arr' instead of $arr
#This function can be used to add entries to files with a mapping format.
#For example, /etc/hosts has <ip> mapped to <fqdn/host alias>
#The function checks to see if a line containing the given 'keyword' is in the file
#If so, we check the given array of values against the existing values for the keyword in that line.
#Append the values specified in the array to the line if it doesn't already exist.
#If the file does not contain a line with the given keyword,
#the function will add a new line with the given keyword mapped to all values in the given array.
function add_entries()
{
    local file=${1}
    local keyword=${2}
    local values=$3[@]
    values=("${!values}")
    if ! grep -qw "^$keyword" $file; then
        log "Line with '$keyword' not found in $file"
        new_entry="$keyword"
        for value in "${values[@]}"
        do
            new_entry="$new_entry $value"
        done
        log "Adding '$new_entry' to $file"
        echo "$new_entry" >>  $file

    else
        log "Found line with '$keyword'"
        target_line=$(grep -w "^$keyword" $file)
        for value in "${values[@]}"
        do
            #First case needs spaces around $value to make sure it's not the prefix or suffix of another value
            #Second case checks if $value is at the end of the line
            if [[ $target_line == *" $value "* ]] || [[ $target_line == *" $value" ]]; then
                log "'$value' already exists in line"
            else
                log "Adding '$value' to line"
                sed -i "s/^\<$keyword\>.*$/& $value/g" $file
            fi
        done
    fi
}


# This function updates the hostname
# Arguments:
#   Arg1 --  OS version information to set hostname accordingly
#   Arg2 --  Hostname that needs to be set
function update_hostname()
{
    local os_version=${1}
    local new_host_name=${2}

    log "Updating hostname"

    # 1. run hostname command
    if [ $os_version -eq 6 ]; then
        # use short hostname for /etc/sysconfig/network
        # https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Installation_Guide/sn-Netconfig-x86.html
        new_host_name_config="HOSTNAME=$new_host_name"
        log "Update /etc/sysconfig/network with new host name $new_host_name_config"

        if grep --quiet '^HOSTNAME=' /etc/sysconfig/network; then
            log "HOSTNAME exists in /etc/sysconfig/network. Updating its value"
            sed -i "s/^HOSTNAME=.*$/$new_host_name_config/g"  /etc/sysconfig/network
        else
            log "Adding HOSTNAME to /etc/sysconfig/network"
            echo "$new_host_name_config" >> /etc/sysconfig/network
        fi

        log "Running hostname command: hostname $new_host_name"
        hostname $new_host_name

    elif [ $os_version -eq 7 ]; then
        log "Running hostnamectl command: hostnamectl set-hostname $new_host_name" 
        hostnamectl set-hostname $new_host_name >> $log_file 2>&1
    elif [ $os_version -eq 8 ]; then
        systemctl is-active --quiet NetworkManager
        nm_status=$?
        if [ $nm_status -eq 0 ]; then
                log "Running nmcli command: nmcli general hostname $new_host_name"
                nmcli general hostname $new_host_name 
        else
                log "Running hostnamectl command: hostnamectl set-hostname $new_host_name" 
                hostnamectl set-hostname $new_host_name
        fi

    fi
}


# This function updates /etc/hosts and /etc/resolv.conf
# Arguments:
#   Arg1 -- new IP address
#   Arg2 -- new hostname of the system
function update_hosts_resolv()
{
    local new_ip_address=${1}
    local new_host_name=${2}

    # Retry params
    fqdn_success=1
    retries=12

    # Remove old entry from /etc/hosts so that we avoid getting
    # stale information from ipcalc.
    # First though, save the entries for
    # restoration if ipcalc encounters failures.
    old_vals=$(grep "^$new_ip_address" /etc/hosts)
    old_fqdn=$(echo $old_vals | awk -F " " '{print $2}')
    old_host_name=$(echo $old_vals | awk -F " " '{print $3}')
    log "Pre-existing fqdn is $old_fqdn and hostname is $old_host_name"
    # Now remove old entry
    sed -i "/^\<$new_ip_address\>.*$/d" /etc/hosts

    while [[ $fqdn_success -ne 0 ]] && [[ $retries -ne 0 ]]; do
        # Get fqdn
        fqdn=$(retry_command ipcalc -h $new_ip_address)
        if [[ "$fqdn" != *.*.*.*.* ]]; then
                # DNS can take upto 120 seconds to return info. Retry as long.
                sleep 10
                (( retries-- ))
        else
                fqdn_success=0
        fi
    done

    if [ $fqdn_success -ne 0 ]; then
        log "ERROR: ipcalc unsuccessful despite multiple retries for 120 seconds"
	 # Restore previously existing hostname entry but first:
        # Check for existing exact matches of hostname and delete them, if any.
        sed -i -e "/[[:space:]]\<$old_host_name\>[[:space:]\.]/d; /[[:space:]]\<$old_host_name\>$/d" /etc/hosts
	fqdn=${fqdn#HOSTNAME=}
	echo "fqdn=$fqdn" >> $log_file


        old_host_values=("$old_fqdn" "$old_host_name")
        
	# Pre-existing FQDN is null for the first boot. 
	if [ -z "$old_fqdn" ]; then
            echo "Pre-existing fqdn is  . Skip updating /etc/hosts"  >> $log_file
        else
            add_entries "/etc/hosts" "$new_ip_address" old_host_values
	fi

    else

        # ipcalc returns HOSTNAME=xxxx, need to remove "HOSTNAME="
        fqdn=${fqdn#HOSTNAME=}

        # get subnet_domain_name
        subnet_domain_name=${fqdn#$new_host_name.}

        # verify that the subnet domain is valid, we expect it is of the
        # form <subnet-name>.<vcn-name>.<oraclevcn>.<com>
        if [[ "$subnet_domain_name" != *.*.*.* ]]; then
            log "ERROR: invalid subnet domain name '$subnet_domain_name'."
        else
            # get vcn domain name - everything after the first dot in the subnet domain name
            vcn_domain_name=${subnet_domain_name#*.}
            log "fqdn=$fqdn"
            log "subnet_domain_name=$subnet_domain_name"
            log "vcn_domain_name=$vcn_domain_name"

            # 2. Update /etc/hosts if needed
            # Check for existing exact matches of hostname and delete them, if any.
            sed -i -e "/[[:space:]]\<$new_host_name\>[[:space:]\.]/d; /[[:space:]]\<$new_host_name\>$/d" /etc/hosts

            new_host_values=("$fqdn" "$new_host_name")
            # Pass array by name
            add_entries "/etc/hosts" "$new_ip_address" new_host_values

            # 3. Update /etc/resolv.conf
            # This is a temp fix till we have a resolution for a proper dhcp response
            new_search_domains=("$subnet_domain_name" "$vcn_domain_name")
            add_entries "/etc/resolv.conf" "search" new_search_domains
            echo -e "[main]\ndns = none\n" > /run/NetworkManager/conf.d/10-oci-dhclient.conf
        fi
    fi
}

# This function updates /etc/resolv.conf
# Arguments:
#   Arg1 -- new IP address
#   Arg2 -- new hostname of the system
function update_resolv()
{
    local new_ip_address=${1}
    local new_host_name=${2}

    # Retry params.
    retries=12
    fqdn_success=1

    while [[ $fqdn_success -ne 0 ]] && [[ $retries -ne 0 ]]; do
    	# Since the hostname might have been changed in /etc/hosts and we're not
    	# updating it, only using ipcalc might give us stale hostname information.
    	# To get the DNS provided hostname, use host and compare it with ipcalc's
    	# generated version. If they don't match use the one from host.
    	host_name=$(retry_command host $new_ip_address | awk -F " " '{print $5}')
    	ipcalc_name=$(retry_command ipcalc -h $new_ip_address)
    	if [[ "$ipcalc_name" != *.*.*.*.* ]] || [[ "$host_name" != *.*.*.*.* ]]; then
                # DNS can take up to 120 seconds to return info. Retry as long.
                sleep 10
                (( retries-- ))
        else
                fqdn_success=0
        fi 
    done

    if [[ $fqdn_success -ne 0 ]]; then
        log "ERROR: ipcalc and host commands failed " \
        log "despite multiple retries for 120 seconds."
    else

        # ipcalc returns HOSTNAME=xxxx, need to remove "HOSTNAME="
        ipcalc_name=${ipcalc_name#HOSTNAME=}
        log "ipcalc returned hostname: $ipcalc_name"

        # host substring will have a "." at the end. Drop it
        host_name=${host_name%?}
        log "host returned hostname: $host_name"

        use_ipcalc_hostname=0
        if [ "$host_name" = "$ipcalc_name" ]; then
            # either one will do
            fqdn=$ipcalc_name
        else
            # Likely that ipcalc has a user changed hostname.
            # This will not match the new_host_name one.
            # Use the one from host. But before that confirm
            # that host has returned a valid name.
            if [[ "$host_name" != *.*.*.*.* ]]; then
                log "Invalid hostname $host_name from host command"
                fqdn=$ipcalc_name

                # Need an additional check here for the host_name
                # being identical to the new_host_name as the user
                # could have changed the /etc/host host_name
                # entry which would not match the dhclient one.
                ipcalc_host_name=$(echo $ipcalc_name | awk -F "." '{print $1}')
                if [[ "$ipcalc_host_name" != "$new_host_name" ]]; then
                    log "ipcalc returned host $ipcalc_host_name"
                    log "dhclient returned host $new_host_name"
                    log "Using ipcalc returned hostname"
                    use_ipcalc_hostname=1
                fi
            else
                fqdn=$host_name
            fi
        fi

        # get subnet_domain_name
        if [[ "$use_ipcalc_hostname" -eq 1 ]]; then
            subnet_domain_name=${fqdn#$ipcalc_host_name.}
        else
            subnet_domain_name=${fqdn#$new_host_name.}
        fi

        # verify that the subnet domain is valid, we expect it is of the
        # form <subnet-name>.<vcn-name>.<oraclevcn>.<com>
        if [[ $subnet_domain_name != *.*.*.* ]]; then
            log "WARNING: invalid subnet domain name '$subnet_domain_name' seen."
        else
            # get vcn domain name - everything after the first dot in the subnet domain name
            vcn_domain_name=${subnet_domain_name#*.}
            log "fqdn=$fqdn"
            log "subnet_domain_name=$subnet_domain_name"
            log "vcn_domain_name=$vcn_domain_name"

            # Update /etc/resolv.conf
            new_search_domains=("$subnet_domain_name" "$vcn_domain_name")
            add_entries "/etc/resolv.conf" "search" new_search_domains
            echo -e "[main]\ndns = none\n" > /run/NetworkManager/conf.d/10-oci-dhclient.conf
        fi
    fi
}

# This function adds NM_CONTROLLED=no entry to the primary interface config file
# So that network manger does not take cotrol when installed.
# Arguments:
# Arg1 -- primary_ip

function disable_NMcontrol()
{
    local primary_ip=${1}

    # find the primary interface
    primary_if=$(ifconfig | grep -B1 $primary_ip | head -n1 | awk -F '[: ]' '{print $1}')

    # generate the primary interface's ifconfig filepath.
    cfg_file="/etc/sysconfig/network-scripts/ifcfg-${primary_if}"

    # check if the file is present.
    if [ ! -f $cfg_file ]; then
        log "$cfg_file not found, skip NM_CONTROLLED setting."
        return
    fi

    # check if the keyword is present or not
    if ! grep -qw "^NM_CONTROLLED" $cfg_file; then
        # append the line..
        echo "NM_CONTROLLED=no" >> $cfg_file
    else
       # modify the line
       sed -i "s/^\<NM_CONTROLLED\>.*$/NM_CONTROLLED=no/g" $cfg_file
    fi
}

os_version=0
if [ -f /etc/os-release ]; then
    os_string=$(grep -w VERSION /etc/os-release | awk -F "\"" '{print $2}')
    log "INFO: Obtained $os_string from /etc/os-release" 
    if [[ "$os_string" == "8."* || "$os_string" == "8"* ]]; then
        os_version=8
    elif [[ "$os_string" == "7."* || "$os_string" == "7"* ]]; then
        os_version=7
    elif [[ "$os_string" == "6."* ]]; then
        os_version=6
    fi
fi

if [ $os_version == 0 ]; then
    log "INFO: Getting OS version via uname -mrs"
    kernel_version=$(uname -mrs)
    if [[ "$kernel_version" == *"el8"* ]]; then
        os_version=8
    elif [[ "$kernel_version" == *"el7"* ]]; then
        os_version=7
    elif [[ "$kernel_version" == *"el6"* ]]; then
        os_version=6
    fi
fi

if [ $os_version == 0 ]; then
    log "ERROR: Could not obtain valid OS version. Exiting.."
    exit 1
fi

# OL8 use NetworkManager. DHCP does not provide a reason.
if [[ "$os_version" -ne 8 ]]; then
    log "$(date): Script Reason:  $reason"
else
    log "$(date): Script Reason:  NM-controlled system"
fi

# For non-OL8 OSs get the primary vnic ip only if interface has been 
# initialized.
# Ref:  https://www.isc.org/wp-content/uploads/2018/02/dhcp44cscript.html#PREINIT
if [ "$os_version" == 8 -o "$reason" != "PREINIT" ]; then
    primary_ip=$(retry_command curl -H "Authorization: Bearer Oracle" http://169.254.169.254/opc/v2/vnics/ -sf  | jq -r '.[0] | .privateIp')
    log "$(date): Primary IP obtained: $primary_ip"
fi

# This script is invoked whenever dhclient is run.
# For non-ol8 instances, We want to skip hostname update if
# $new_ip_address != $primary_ip
# so we don't run this for all interfaces
# For ol8, with the use of NM, we don't get information on the
# new_ip_address, therefore we just use the primary_ip each time.
if [ -z "$primary_ip" ]; then
    log "Skip updating hostname because primary ip is empty."
elif [[ "$os_version" -ne 8 ]] && [[ "$new_ip_address" != "$primary_ip" ]]; then
    log "Skip updating hostname because this was not invoked for the primary vnic"
else
    if [ $os_version -ne 8 ]; then
        # For non-ol8, add NM_Controlled="no" to primary network interface
        # configuration file
        disable_NMcontrol $primary_ip

        # reason why this hook was invoked. It is set by dhclient script when
        # the OS is non-ol8
        log "reason=$reason"

        # https://linux.die.net/man/8/dhclient-script
        if [ "$reason" = "BOUND" ] || [ "$reason" = "RENEW" ] || [ "$reason" = "REBIND" ] || [ "$reason" = "REBOOT" ]; then
            log "os version = $os_version"
            #These variables are set by dhclient script
            log "new_ip_address=$new_ip_address"
            log "new_host_name=$new_host_name"
            log "new_domain_name=$new_domain_name"
        else
            log "Not updating because reason=$reason"
        fi
    fi

    if [[ $PRESERVE_HOSTINFO -eq 2 ]]; then
        log "Skip updating hostname, /etc/hosts and /etc/resolv.conf"
        log "as per PRESERVE_HOSTINFO=${PRESERVE_HOSTINFO} setting"
        return 0
    fi

     #Retrieve hostname from metadata if its empty
     if [ -z $new_host_name ]; then
         new_host_name=$(retry_command curl -sf -H "Authorization: Bearer Oracle" http://169.254.169.254/opc/v2/instance/ | jq '.hostname' -r)
     fi
    
     # Will get retrieved for ol8 since we do not have DHCP provided vars.
     if [ -z $new_ip_address ]; then
         new_ip_address=$(retry_command curl -H "Authorization: Bearer Oracle" http://169.254.169.254/opc/v2/vnics/ -sf  | jq -r '.[0] | .privateIp')
     fi
    
     if [ -z $new_host_name ]; then
         echo "ERROR: new_host_name is empty after retrieving it from metadata json. Exiting."
         exit_status=1
     else
         log "new_ip_address=$primary_ip"
         if [[ $PRESERVE_HOSTINFO -eq 0 ]]; then
             # update the hostname with new hostname
             update_hostname $os_version $new_host_name
             # update hosts and resolv conf files
             update_hosts_resolv $new_ip_address $new_host_name
         elif [[ $PRESERVE_HOSTINFO -eq 1 ]]; then
             log "Skip updating hostname as per"
             log "PRESERVE_HOSTINFO=${PRESERVE_HOSTINFO} setting"
             # update hosts and resolv conf files
             update_hosts_resolv $new_ip_address $new_host_name
         elif [[ $PRESERVE_HOSTINFO -eq 3 ]]; then
             log "Skip updating hostname and /etc/hosts as per"
             log "PRESERVE_HOSTINFO=${PRESERVE_HOSTINFO} setting"
             log "Updating subnet in /etc/resolv"
             # update resolv conf file alone
             update_resolv $new_ip_address $new_host_name
        fi
    fi
fi
log "sethostname, /etc/hosts, /etc/resolv.conf END"