#!/bin/sh # Filename: updatezone.sh # Location: # Author: bgstack15@gmail.com # Startdate: 2017-05-26 07:02:47 # Title: Script that Updates a DNS Zone # Purpose: Provides a single command to update dns zones # Package: updatezone # History: # 2017-10-15 Added --flush option # 2024-08-19 fix typo in tmp_rev_file2 # Usage: # Primarily intended for updating forward and reverse zones for bind9. # Reference: ftemplate.sh 2017-05-24a; framework.sh 2017-05-24a # Improve: # Dependencies: # rndc # ssh with password-less authentication to slave servers # each zone file has only a single zone fiversion="2017-05-24a" updatezoneversion="2024-08-19a" usage() { less -F >&2 <> "${zones_to_thaw_file}" # prepare temp file cp -p "${zone_real_file}" "${zone_temp_file}" fi fi } zone_action() { # call: zone_action ${forwardzone} debuglev 9 && ferror "zone_action $@" local action="$1" local zone="$2" case "${action}" in freeze|thaw) rndc "${action}" "${zone}" 2>&1 | grep -viE "a zone reload and thaw|Check the logs to see" ;; *) ferror "${scriptfile} minor error: ignoring unknown zone_action $@" ;; esac } update_real_zone_if_updated() { # call: update_real_zone_if_updated "${UZ_REVERSE_ZONE}" "${UZ_REVERSE_FILE}" "${tmp_rev_file}" debuglev 9 && ferror "update_real_zone_if_updated $@" local zone_name="$1" local zone_real_file="$2" local zone_temp_file="$3" if test -n "${zone_temp_file}" && test -f "${zone_temp_file}"; then if ! cmp -s "${zone_real_file}" "${zone_temp_file}"; then # a change occurred, so increment the serial number and replace the original zone file increment_serial_in_zone_file "${zone_temp_file}" cat "${zone_temp_file}" > "${zone_real_file}" # plan to notify the dns slaves echo "${zone_name}" >> "${zones_to_update_file}" fi fi # If the temp file does not exist, it was deleted because the real file was invalid for whatever reason. } increment_serial_in_zone_file() { # call: increment_serial_in_zone_file "${zone_temp_file}" # dependencies: a single zone in the zone file, with the ";serial" comment after the number. debuglev 9 && ferror "increment_serial_in_zone_file $@" local infile="$1" currentnum="$( grep -iE "[0-9]+\s*;\s*serial" "${infile}" | grep -oIE "[0-9]+" )" nextnum=$(( currentnum + 1 )) sed -i -r -e "s/${currentnum}(\s*;\s*serial)/${nextnum}\1/" "${infile}" } # DEFINE TRAPS clean_updatezone() { rm -rf ${tempdir} > /dev/null 2>&1 [ ] #use at end of entire script if you need to clean up tmpfiles } CTRLC() { #trap "CTRLC" 2 [ ] #useful for controlling the ctrl+c keystroke exit 0 } CTRLZ() { #trap "CTRLZ" 18 [ ] #useful for controlling the ctrl+z keystroke } parseFlag() { flag="$1" hasval=0 case ${flag} in # INSERT FLAGS HERE "d" | "debug" | "DEBUG" | "dd" ) setdebug; ferror "debug level ${debug}";; "u" | "usage" | "help" | "h" ) usage; exit 1;; "V" | "fcheck" | "version" ) ferror "${scriptfile} version ${updatezoneversion}"; exit 1;; #"i" | "infile" | "inputfile" ) getval;infile1=${tempval};; "c" | "conf" | "config" | "conffile" ) getval;conffile=${tempval};; "flush" ) action=flush;; esac debuglev 10 && { test ${hasval} -eq 1 && ferror "flag: ${flag} = ${tempval}" || ferror "flag: ${flag}"; } } # DETERMINE LOCATION OF FRAMEWORK while read flocation; do if test -x ${flocation} && test "$( ${flocation} --fcheck )" -ge 20170524; then frameworkscript="${flocation}"; break; fi; done <&2 && exit 4 # INITIALIZE VARIABLES # variables set in framework: # today server thistty scriptdir scriptfile scripttrim # is_cronjob stdin_piped stdout_piped stderr_piped sendsh sendopts . ${frameworkscript} || echo "$0: framework did not run properly. Continuing..." 1>&2 infile1= outfile1= logfile=${scriptdir}/${scripttrim}.${today}.out define_if_new interestedparties "bgstack15@gmail.com" # SIMPLECONF #define_if_new default_conffile "/etc/ddtools/updatezone.conf" #define_if_new defuser_conffile ~/.config/ddtools/updatezone.conf define_if_new EDITOR vi define_if_new default_dir "/etc/ddtools" # REACT TO OPERATING SYSTEM TYPE case $( uname -s ) in Linux) [ ];; FreeBSD) [ ];; *) echo "${scriptfile}: 3. Indeterminate OS: $( uname -s )" 1>&2 && exit 3;; esac ## REACT TO ROOT STATUS #case ${is_root} in # 1) # proper root # [ ] ;; # sudo) # sudo to root # [ ] ;; # "") # not root at all # #ferror "${scriptfile}: 5. Please run as root or sudo. Aborted." # #exit 5 # [ ] # ;; #esac # VALIDATE PARAMETERS # objects before the dash are options, which get filled with the optvals # to debug flags, use option DEBUG. Variables set in framework: fallopts validateparams - "$@" # CONFIRM TOTAL NUMBER OF FLAGLESSVALS IS CORRECT #if test ${thiscount} -lt 1; #then # #ferror "${scriptfile}: 2. Fewer than 2 flaglessvals. Aborted." # #exit 2 #fi # CONFIGURE VARIABLES AFTER PARAMETERS ## LOAD CONFIG FROM SIMPLECONF ## This section follows a simple hierarchy of precedence, with first being used: ## 1. parameters and flags ## 2. environment ## 3. config file ## 4. default user config: ~/.config/script/script.conf ## 5. default config: /etc/script/script.conf #if test -f "${conffile}"; #then # get_conf "${conffile}" #else # #if test "${conffile}" = "${default_conffile}" || test "${conffile}" = "${defuser_conffile}"; then :; else # ferror "${scriptfile}: Ignoring conf file which is not found: ${conffile}." # #fi #fi #test -f "${defuser_conffile}" && get_conf "${defuser_conffile}" #test -f "${default_conffile}" && get_conf "${default_conffile}" ## REACT TO BEING A CRONJOB #if test ${is_cronjob} -eq 1; #then # [ ] #else # [ ] #fi # SET TRAPS trap "CTRLC" 2 #trap "CTRLZ" 18 trap "clean_updatezone" 0 ## DEBUG SIMPLECONF #debuglev 5 && { # ferror "Using values" # # used values: EX_(OPT1|OPT2|VERBOSE) # set | grep -iE "^UZ_" 1>&2 #} # MAKE TEMP LOCATIONS tempdir=/tmp/updatezone/ if ! mkdir -p "${tempdir}"; then ferror "${scriptfile}: 4. Unable to make temp directory ${tempdir}. Aborted." exit 4 fi # MAIN ACTIONS # EDIT main_action() { # call: main_action "${action}" "${conffile}" local action="${1}" get_conf "${2}" # DEBUG SIMPLECONF debuglev 5 && { ferror "Using values" # used values: EX_(OPT1|OPT2|VERBOSE) set | grep -iE "^UZ_" 1>&2 } local tmp_for_file="$( mktemp -p "${tempdir}" forward.XXXX 2>/dev/null )" local tmp_rev_file="$( mktemp -p "${tempdir}" reverse.XXXX 2>/dev/null )" local zones_to_thaw_file="$( mktemp -p "${tempdir}" thaw.XXXX )" local zones_to_update_file="$( mktemp -p "${tempdir}" update.XXXX )" for word in "${tmp_for_file}" "${tmp_rev_file}"; do if test ! -f "${word}"; then ferror "${scriptfile}: 4. Unable to make temp file ${word}. Aborted." exit 4 fi done # Freezing the zone ensures all records are in the primary files which we check. local pause_to_show_error=0 # Check forward zone file and freeze check_zone_file forward "${UZ_FORWARD_ZONE}" "${UZ_FORWARD_FILE}" "${tmp_for_file}" # Check reverse zone file and freeze check_zone_file reverse "${UZ_REVERSE_ZONE}" "${UZ_REVERSE_FILE}" "${tmp_rev_file}" # Slow down to show errors if any fistruthy "${pause_to_show_error}" && sleep 1.3 case "${action}" in edit) # EDIT FILES INTERACTIVELY local these_temp_files="$( find "${tmp_for_file}" "${tmp_rev_file}" 2>/dev/null | xargs )" test -n "${these_temp_files}" && $EDITOR ${these_temp_files} ;; flush) # FLUSH FILES AUTOMATICALLY # CALCULATE WHICH RECORDS TO FLUSH # get dhcpd ttl. #set -x local tmp_flush_master_file="$( mktemp -p "${tempdir}" flush.master.XXXX 2>/dev/null )" local tmp_flush_for_file="$( mktemp -p "${tempdir}" flush.for.XXXX 2>/dev/null )" local tmp_flush_rev_file="$( mktemp -p "${tempdir}" flush.rev.XXXX 2>/dev/null )" local dhcpd_ttl="$( grep -hE "default-lease-time" $( { $( which dhcpd-control ) --debug 5 nop; } 2>&1 | grep -E "_FILE=" | grep -vE "LEASES_" | cut -d'=' -f2 | xargs ) | cut -d' ' -f2 | tr -d ';' )" # this next statement only returns an integer, but the rounding should be the same as the dns ttl rounding local dns_ttl="$( printf "${dhcpd_ttl}/2\n" | bc )" debuglev 2 && ferror "Flushing entries that have ttl ${dns_ttl} and have a TXT hash" # fetch all dns A and TXT records that have the requested TTL awk '$1 == "$TTL" {a=$2;} $1 != "$TTL" {if(a=='${dns_ttl}' && ($1=="TXT" || $2=="A")) print;}' "${tmp_for_file}" | \ # restrict to the A records that have associated TXT records awk 'NR>1{if ($1=="TXT") print prev,$0;} {prev=$0;}' | \ # only keep ones whose TXT hash is the correct length awk '$4=="TXT" && $5 ~ /"[a-fA-F0-9]{34}"/ {print;}' > "${tmp_flush_master_file}" # prepare items to flush, forward # convert to each a single line in a file, for future grep -v grep -oE "(([0-9]{1,3}\.){3}[0-9]{1,3}|"[a-fA-F0-9]{34}")" "${tmp_flush_master_file}" > "${tmp_flush_for_file}" # prepare items to flush, reverse awk '{print $1}' "${tmp_flush_master_file}" | sed -e 's/^/PTR\\s\*/;' > "${tmp_flush_rev_file}" # flush forward records grep -v -f "${tmp_flush_for_file}" "${tmp_for_file}" > "${tmp_for_file}2"; mv -f "${tmp_for_file}2" "${tmp_for_file}" # flush reverse records grep -v -E -f "${tmp_flush_rev_file}" "${tmp_rev_file}" > "${tmp_rev_file}2"; mv -f "${tmp_rev_file}2" "${tmp_rev_file}" ;; esac # Update the real zone if the temp file was updated update_real_zone_if_updated "${UZ_FORWARD_ZONE}" "${UZ_FORWARD_FILE}" "${tmp_for_file}" update_real_zone_if_updated "${UZ_REVERSE_ZONE}" "${UZ_REVERSE_FILE}" "${tmp_rev_file}" # Thaw zones that need it while read thiszone; do zone_action thaw "${thiszone}" done < "${zones_to_thaw_file}" # Transfer zones that need it # This section exists because my automatic zone transfers/updates do not work. # Build list of commands to run on each dns slave server transfercommand="" while read thiszone; do transfercommand="${transfercommand}rndc retransfer ${thiszone}; " done < "${zones_to_update_file}" # Execute command on each slave server if test -n "${transfercommand}"; then x=0 while test ${x} -lt ${UZ_SLAVE_COUNT}; do x=$(( x + 1 )) eval this_dns_slave=\"\${UZ_SLAVE_${x}}\" debuglev 5 && ferror "ssh ${this_dns_slave} ${transfercommand}" ssh ${this_dns_slave} ${transfercommand} done fi } #| tee -a ${logfile} ####################################################### # DETERMINE ACTION case "${action}" in "flush") debuglev 2 && ferror "Action is flush." ;; *) debuglev 2 && ferror "Action is edit." action=edit ;; esac # MAIN LOOP if test -n "${conffile}"; then ( main_action "${action}" "${conffile}"; ) else # assume the $opt items are the zone names y=0 while test $y -lt $thiscount; do y=$(( y + 1 )) eval "thiszonename=\${opt${y}}" debuglev 1 && ferror "Will try to ${action} zone ${thiszonename}" file_for_this_zone="$( grep -liE "UZ_ZONE_NAME=${thiszonename}" "${default_dir}/"*.conf 2>/dev/null )" if test -n "${file_for_this_zone}" && test -f "${file_for_this_zone}"; then ( main_action "${action}" "${file_for_this_zone}"; ) else ferror "Skipping zone ${thiszonename} for which no file was found in ${default_dir}/" fi done fi # EMAIL LOGFILE #${sendsh} ${sendopts} "${server} ${scriptfile} out" ${logfile} ${interestedparties} ## STOP THE READ CONFIG FILE #exit 0 #fi; done; }