#!/usr/bin/bash

#
# Intialise a named (bind) dynamic SOA + HA zone for an OCA Cluster
#

function usage {
	local ERROR=$1
	if [ -n "$ERROR" ]; then
		echo
		echo -e " \033[31;1mERROR:\033[0;33m $ERROR\033[0m"
	fi
	echo
	echo -e " \033[1mUSAGE\033[0m: $(basename $0) [--force] (--from-ksconfig | <soa> <host1>[.<az1>] <host2>[.<az2>] [<host1-az1-ip1> <host2-az2-ip1> [<host1-az1-ip2> <host2-az2-ip2> [<ns1> <ns2>]]])"
	echo
	echo "   --force         := overwrite any existing config"
	echo "   --from-ksconfig := Read parameters from /etc/sysconfig/ng-kickstart instead of the command line"
	echo "     <soa>         := DNS Start of Authority"
	echo "   <hostN>         := Server host short name"
	echo "     <azN>         := Data Centre / Availability Zone"
	echo "   <hostN-azN-ipN> := Public IPv4 address(es)"
	echo "                      these can be omitted if delegation glue records"
	echo "                      have already been provisioned for <soa>"
	echo "     <nsN>         := Hostname of Nameserver N ('ns1','ns2')"
	echo
	echo "   eg  $(basename $0) demo-soa.nogginoca.com oca01.syd1 oca02.bne1"
	echo
	exit 1
}

CONFFILE=/etc/named/cluster-zones.conf
ACLFILE=/etc/named/cluster-acl.conf
SOAZONE=/var/named/cluster-soa.zone
HAZONE=/var/named/dynamic/cluster-ha.zone

# Are we clobbering stuff?
if [ "$1" == "--force" ]; then
	shift
elif [ -f "$CONFFILE" ] || [ -f "$ACLFILE" ] || [ -f "$SOAZONE" ] || [ -f "$HAZONE" ]; then
	usage " !! One or more of $CONFFILE, $ACLFILE, $SOAZONE or $HAZONE" \
	      "already exists.  remove them or use --force"
fi 

if [ "$1" == "--from-ksconfig" ]; then
    shift
	# If we got no arguments, try loading from /etc/sysconfig/ng-kickstart
	if [[ $# -eq 0 && -f /etc/sysconfig/ng-kickstart ]]; then
		source /etc/sysconfig/ng-kickstart
		if [ -z "${clustersoa}" ] || [ -z "${clusterhosts}" ]; then
			echo "DNS SOA not initialised: /etc/sysconfig/ng-kickstart incomplete"
			exit 0
		fi
		set -- "${clustersoa}${clusterdomain:+.$clusterdomain}" "${clusterhosts%%,*}" "${clusterhosts##*,}" "" "" "" "" "${clusterns%%,*}" "${clusterns##*,}"
	else
		echo "DNS SOA not initialised: /etc/sysconfig/ng-kickstart not present"
		exit 0
	fi
fi

SOA=$1
HOST1=$2
HOST2=$3
AZ1_IP1=$4
AZ2_IP1=$5
AZ1_IP2=$6
AZ1_IP2=$7
NS1=$8
NS2=$9


[[ $# -lt 3 || $# -gt 9 ]] && usage

# Are we clobbering stuff?

# Validate SOA
if [[ ! "$SOA" =~ ^([a-z][-a-z0-9]+\.)+[a-z][-a-z0-9]+$ ]]; then
	usage "!! '$SOA' is not a valid domain name"
fi

# Validate hosts
if [[ ! "$HOST1" =~ ^[a-z][-a-z0-9]+(\.[a-z][-a-z0-9]+)?$ ]]; then
	usage "!! host1 '$HOST1' is not a valid short name"
fi
if [[ ! "$HOST2" =~ ^[a-z][-a-z0-9]+(\.[a-z][-a-z0-9]+)?$ ]]; then
	usage "!! host2 '$HOST2' is not a valid short name"
fi

# Validate nameserver names
if [[ ! "$NS1" =~ ^[a-z][-a-z0-9]+$ ]]; then
	usage "!! ns1 hostname '$NS1' is not a valid short name"
fi
if [[ ! "$NS2" =~ ^[a-z][-a-z0-9]+$ ]]; then
	usage "!! ns1 hostname '$NS2' is not a valid short name"
fi


# Split the hosts & AZs
AZ1=${HOST1#*.}
AZ2=${HOST2#*.}
[ "$AZ1" == "$HOST1" ] && AZ1=
[ "$AZ2" == "$HOST2" ] && AZ2=
[ -n "$AZ1" ] && AZ1=".${AZ1}"
[ -n "$AZ2" ] && AZ2=".${AZ2}" 
HOST1=${HOST1%%.*}
HOST2=${HOST2%%.*}

# Set the default nameservers if not provided
[ -z "$NS1" ] && NS1="ns1"
[ -z "$NS2" ] && NS2="ns2"


# Extract A glue records from delegation authority
function digglue {
	local SOA=$2
	local NAME=$1
	dig +norecurse +noall +additional -t A ${NAME} @${SOA} \
		| awk "\$1 == \"${NAME}.\" && \$4 == \"A\" {print \$5}"
}

# TODO: 1p/2p won't cut it here, results are rotated already, this should be an array
# If were weren't given IPs try to pull them from the delegation glue records
PARENT_SOA=$(dig +short -t SOA ${SOA#*.} | head -1 | cut -d\  -f 1 | sed -re 's/\.$//g')
if [ -n "$PARENT_SOA" ]; then
	[ -z "$AZ1_IP1" ] && AZ1_IP1=$(digglue ${NS1}.${SOA} ${PARENT_SOA} | sed -n '1p')
	[ -z "$AZ1_IP2" ] && AZ1_IP2=$(digglue ${NS1}.${SOA} ${PARENT_SOA} | sed -n '1p')
	[ -z "$AZ2_IP1" ] && AZ2_IP1=$(digglue ${NS2}.${SOA} ${PARENT_SOA} | sed -n '1p')
	[ -z "$AZ2_IP2" ] && AZ2_IP2=$(digglue ${NS2}.${SOA} ${PARENT_SOA} | sed -n '1p')
fi

# If IP2 is missing for an AZ try substituting IP1
# We're going to write a zone with two IPs per AZ we'll let named sort out
# he fact that they're duplicate if necessary because it greatyly simplifies
# the zone template to just assume two network links per site
[ -z "$AZ1_IP2" ] && [ -n "$AZ1_IP1" ] && AZ1_IP2=$AZ1_IP1
[ -z "$AZ2_IP2" ] && [ -n "$AZ2_IP1" ] && AZ2_IP2=$AZ2_IP1

# Validate the IP addresses - have we got anything?
[ -z "$AZ1_IP1" ] && usage "!! host1-az1-ip1 could not be determined"
[ -z "$AZ2_IP1" ] && usage "!! host2-az2-ip1 could not be determined"

# Validate the IP addresses - do they look like IPv4?
IPV4REGEX="^((1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\.){3}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])"
[[ "$AZ1_IP1" =~ $IPV4REGEX ]] || usage "!! host1-az1-ip1 '$AZ1_IP1' isn't a valid IPv4 address"
[[ "$AZ1_IP2" =~ $IPV4REGEX ]] || usage "!! host1-az1-ip2 '$AZ1_IP2' isn't a valid IPv4 address"
[[ "$AZ2_IP1" =~ $IPV4REGEX ]] || usage "!! host2-az2-ip1 '$AZ2_IP1' isn't a valid IPv4 address"
[[ "$AZ2_IP2" =~ $IPV4REGEX ]] || usage "!! host2-az2-ip2 '$AZ2_IP2' isn't a valid IPv4 address"

# Generate the TSIG Secret
echo " * Generating TSIG secret key"
TSIGKEY=$(ng-mk-auth-token tsig:${SOA} 44)

#
# Write the named config file
#
[ -d $(dirname ${CONFFILE}) ] || mkdir -m 0755 $(dirname ${CONFFILE})
echo -e " * Writing named zone configuration to ${CONFFILE}" 
touch -a         ${CONFFILE}
chown root:named ${CONFFILE}
chmod 0640       ${CONFFILE}
cat <<-EOD >${CONFFILE}

	// TSIG Key for NSUpdate
	key "ha.$SOA" {
	    algorithm hmac-sha256;
	    secret "${TSIGKEY}";
	};

	// The static SOA zone
	zone "$SOA" {
	    type master;
	    file "${SOAZONE}";
	};

	// The dynamic (HA) zone
	zone "ha.${SOA}" {
	    type master;
	    file "${HAZONE}";
	    allow-update { key "ha.${SOA}"; };
	};

EOD

echo -e " * Writing named acl configuration to ${ACLFILE}"
cat <<-EOD | uniq >${ACLFILE}

	// Cluster nodes
	acl cluster_nodes {
	    127.0.0.1/8;
	    $AZ1_IP1;
	    $AZ1_IP2;
	    $AZ2_IP1;
	    $AZ2_IP2;
	};

EOD

#
# Write the static zone
#
echo " * Writing SOA zone to ${SOAZONE}"
cat <<-EOD | uniq >${SOAZONE}
	;
	; Noggin OCA Cluser SOA
	; ${SOA}
	;
	; !! IMPORTANT !!
	; Any updates to this zone _must_ be made on all cluster nodes
	;

	\$TTL 4h

	@ 3600 IN SOA ns1.${SOA}. hostmaster.noggin.com.au. (
	         $(date +%Y%m%d)00 ; Serial
	         4h         ; Slave refresh
	         1h         ; Slave retry
	         1d         ; Slave expiry
	         300        ; Failed lookup cache time
	)

	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	; Nameservers (delegation)
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

	@ 			IN	NS	${NS1}
	@ 			IN	NS	${NS2}

	${NS1}			IN	A	${AZ1_IP1}
	${NS1}			IN	A	${AZ1_IP2}
	${NS2}			IN	A	${AZ2_IP1}
	${NS2}			IN	A	${AZ2_IP2}


	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	; Actual Servers (management)
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

	${HOST1}${AZ1}		IN	A	${AZ1_IP1}
	${HOST1}${AZ1}		IN	A	${AZ1_IP2}
	${HOST2}${AZ2}		IN	A	${AZ2_IP1}
	${HOST2}${AZ2}		IN	A	${AZ2_IP2}


	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	; Service name targets (end-user access)
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

	\$TTL 180

	; Base (shared)
	${HOST1}-base${AZ1}	IN	A	${AZ1_IP1}
	${HOST1}-base${AZ1}	IN	A	${AZ1_IP2}
	${HOST2}-base${AZ2}	IN	A	${AZ2_IP1}
	${HOST2}-base${AZ2}	IN	A	${AZ2_IP2}


EOD

#
# Write the dynamic (HA) zone
#
echo " * Writing HA zone stub to ${HAZONE}" 
cat <<-EOD >${HAZONE}
	;
	; Noggin OCA Cluster HA Zone
	;  ha.${SOA}
	;
	\$TTL 4h

	@ 3600 IN SOA ns1.${SOA}. hostmaster.noggin.com.au. (
	      $(date +%Y%m%d)00 ; Serial
	      4h         ; Slave refresh
	      1h         ; Slave retry
	      1d         ; Slave expiry
	      300        ; Negative cache time (NXDomain)
	)

	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	; Nameservers (delegation)
	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

	@                       IN      NS      ${NS1}.${SOA}.
	@                       IN      NS      ${NS2}.${SOA}.

	\$TTL 180
	

EOD
chown named:named ${HAZONE}

#
# include the named config fragment from /etc/named.conf
#
echo " * Including ${ACLFILE} from /etc/named.conf"
if ! grep -Pq "^\\s*include\\s*\"${ACLFILE}\"\s*;" /etc/named.conf; then
	# There is no augeas lens for named ;-(
	sed -i /etc/named.conf -re "s|^[ \\t]*options[ \\t]+\\{|include \"${ACLFILE}\";\\n\\n\\0|g"

	sed  -i /etc/named.conf \
	    -re 's/^([ \t]*allow-recursion[ \t]+).*$/\1 { cluster_nodes; };/g' \
	     -e 's/^([ \t]*allow-query[ \t]+).*$/\1 { any; };/g'

fi
# Insert allow-recursion if missing
if ! grep -Pq '^\s*allow-recursion\s+' /etc/named.conf; then
	sed -i /etc/named.conf -re 's/^([ \t]+)allow-query[ \t].*$/\1allow-recursion { cluster_nodes; };\n\0/g';
fi

# Insert allow-transfer if missing
if ! grep -Pq '^\s*allow-transfer\s+' /etc/named.conf; then
	sed -i /etc/named.conf -re 's/^([ \t]+)allow-query[ \t].*$/\1allow-transfer { cluster_nodes; };\n\0/g';
fi

# Insert "version none" if missing
if ! grep -Pq '^\s*version\s+' /etc/named.conf; then
	sed -i /etc/named.conf -re 's/^([ \t]+)allow-query[ \t].*$/\0\n\1version         none;/g';
fi
# Insert "hostname none" if missing
if ! grep -Pq '^\s*hostname\s+' /etc/named.conf; then
	sed -i /etc/named.conf -re 's/^([ \t]+)allow-query[ \t].*$/\0\n\1hostname        none;/g';
fi


# Listen 127.0.0.1 => any (if it's NOT 127.0.0.1 the admin changed it => leave it alone)
sed -i /etc/named.conf -re 's/^(\s*listen-on\s+port\s+53\s+\{\s*)127.0.0.1;\s*\}\s*;\s*$/\1any; };/g'


echo " * Including ${CONFFILE} from /etc/named.conf"
if ! grep -Pq "^\\s*include\\s*\"${CONFFILE}\"\s*;" /etc/named.conf; then
	# There is no augeas lens for named ;-(
	echo -e "\ninclude \"${CONFFILE}\";\n" >>/etc/named.conf
fi

echo " * Enabling named-chroot service"
systemctl enable named-chroot

# WARNING: Do not start named-chroot here, it conflicts with the init service Before=

