#!/bin/bash
##########################################################################
#
# This script is intended to be run via cron job to sync a local,
# authoritative DNS server's root zone with the registry information
# provided by dn42regsrv.
#
# As is, the script is specific to updating PDNS within the burble.dn42
# network, however it is also intended to be easily adaptable to other
# DNS servers and networks
#
##########################################################################

##########################################################################
# This array is used to define additional, local networks that the
# DNS server may be authoritative for. The array is used to prevent
# the related resource records from being removed automatically,
# as any records not listed here or in the registry will get deleted.
# Make sure to include '.' here or the local NS and SOA records
# will get removed

IGNORE_RECORDS=(
    '.'
    '$ORIGIN'
    'ns1.burble.dn42'
    'burble.dn42'
    'collector.dn42'
    '1.0.6.2.2.4.2.4.2.4.d.f.ip6.arpa'
    '160/27.129.20.172.in-addr.arpa'
    '0/27.129.20.172.in-addr.arpa'
)

##########################################################################
# The functions here are used to actually update the DNS server
# Change these to use a different server than PDNS

PDNSUTIL='/usr/bin/pdnsutil'
PDNSCTRL='/usr/bin/pdns_control'

# replace_rr <name> <type> <content>
function replace_rr {
    local rr_name=$1; shift
    local rr_type=$1; shift
    local rr_content=$*

    echo "Replace: ${rr_name} ${rr_type} '${rr_content}'"
    
    if [ ${DEBUG} -eq 0 ]
    then
        ${PDNSUTIL} replace-rrset . ${rr_name} ${rr_type} "${rr_content}"
    fi
}


# delete_rr <name> <type>
function delete_rr {
    local rr_name=$1
    local rr_type=$2

    echo "Delete: ${rr_name} ${rr_type}"

    if [ ${DEBUG} -eq 0 ]
    then
        ${PDNSUTIL} delete-rrset . ${rr_name} ${rr_type}
    fi
}

# list the current contents of the root zone
function get_current_root_zone {
    local rra rr_name rr_type rr_content
    while read -r -a rra
    do
        rr_name=${rra[0]}
        rr_type=${rra[3]}
        rr_content=${rra[@]:4}

        current_rz["${rr_name} ${rr_type}"]=${rr_content}

    done < <(${PDNSUTIL} list-zone .)
}

# update the . SOA record after a change
function update_soa {
    echo "Incrementing SOA serial"
    if [ ${DEBUG} -eq 0 ]
    then
        ${PDNSUTIL} increase-serial .
    fi
}

# used to trigger a notify to any slaves of this server
function notify_slaves {
    echo "Notfying slaves"    
    if [ ${DEBUG} -eq 0 ]
    then    
        ${PDNSCTRL} notify .
    fi
}

##########################################################################
# No further local customisation should be needed from here
##########################################################################

# initialise script parameters and global vars

function usage {
    echo "Usage: $0 [-h] [-d] [-a URL] [-c FILE]"
    echo " -h: this help"
    echo " -d: enable debugging and don't action changes"
    echo " -a: URL to dn42regsrv API"
    echo " -c: file in which to store previous commit number"
}

# default options
DEBUG=0
APIURL="http://explorer.burble.dn42/api"
COMMITFILE="/tmp/.sync_rz_commit"

# parse any arguments passed to the script
while getopts ":hda:" opt
do
    case ${opt} in
        d)
            DEBUG=1
            ;;
        a)
            APIURL=${OPTARG}
            ;;
        *)
            usage
            exit 0
            ;;
    esac
done

# global vars
declare -A current_rz
declare -A new_rz
current_commit=''
new_commit=''
deleted_records=0
updated_records=0

##########################################################################
# fetch and parse the root zone data from the API

function fetch_new_root_zone {
    local line fields rr_name rr_type rr_content
    while read -r line
    do
        if [[ ${line} == ';; Commit Reference:'* ]]
        then
            new_commit=${line#;; Commit Reference: }
        else
            # strip out comments and create array
            fields=( ${line%%;*} )

            # if the line is a valid record
            if [ ${#fields[@]} -ge 4 ]
            then
                rr_name=${fields[0]}
                rr_type=${fields[2]}
                rr_content=${fields[@]:3}
                new_rz["${rr_name} ${rr_type}"]=${rr_content}
            fi
        fi
    done < <(/usr/bin/wget -O - -q "${APIURL}/dns/root-zone?format=bind")

    if [ ${DEBUG} -eq 1 ]; then echo "New Commit: ${new_commit}"; fi
}

##########################################################################
# load and store the previous commit

function load_current_commit {
    read -r current_commit < "${COMMITFILE}" 
    if [ ${DEBUG} -eq 1 ]; then echo "Current Commit: ${current_commit}"; fi
}

function store_current_commit {
    if [ ${DEBUG} -eq 0 ]
    then
        echo "${1}" > "${COMMITFILE}"
    fi
}

##########################################################################
# remove records that have been deleted

function is_ignored {
    local rr_name=$1
    for i in "${IGNORE_RECORDS[@]}"
    do
        [ "${i}" == "${rr_name}" ] && echo "ignored"
    done
    echo ""
}

function remove_deleted_records {
    local key rr_name ignored
    deleted_records=0

    # check each record in the old root zone
    for key in "${!current_rz[@]}"
    do
        ignored=$(is_ignored ${key% })

        # if record is not ignored, and no new record exists
        if [ "${ignored}" == '' -a "${new_rz[${key}]}" == '' ]
        then
            # then get rid of it
            delete_rr ${key}
            deleted_records=$((deleted_records + 1))
        fi
    done

    echo "Deleted ${deleted_records} records"
}

##########################################################################
# update records that have been added or changed

function update_new_records {
    local key content
    updated_records=0

    # check each new record
    for key in "${!new_rz[@]}"
    do
	      content="${new_rz[${key}]}"
	
        # if old record didn't exist, or content differs
        if [ "${current_rz[${key}]}" != "${content}" ]
        then
            # update the record
            replace_rr $key ${content}
            updated_records=$((updated_records + 1))
        fi
        
    done

    echo "Updated ${updated_records} records"    
}

##########################################################################
# main flow of the script starts here

echo "DN42 Root Zone Sync"
date
echo

fetch_new_root_zone

# check that the commit was populated
if [ "${new_commit}" == '' ]
then
    echo "Unable to fetch new root zone, aborting"
    exit 1
fi

load_current_commit

# now check if anything actually needs to be done
if [ "${new_commit}" == "${current_commit}" ]
then
    echo "Commits are equal, nothing to do"
    exit 0
fi

get_current_root_zone

# apply changes
remove_deleted_records
update_new_records

# bail out if there were no actual differences
if [ $((deleted_records + updated_records)) -eq 0 ]
then
    echo "No records were updated, exiting"
else

    # update the SOA and send out a notification to slaves
    update_soa
    notify_slaves

fi

# finally store the new commit to show it's been updated
store_current_commit "${new_commit}"

echo "All done"

##########################################################################
# end of code