aboutsummaryrefslogtreecommitdiff
path: root/src/usr/bin/updatezone
diff options
context:
space:
mode:
Diffstat (limited to 'src/usr/bin/updatezone')
-rwxr-xr-xsrc/usr/bin/updatezone434
1 files changed, 434 insertions, 0 deletions
diff --git a/src/usr/bin/updatezone b/src/usr/bin/updatezone
new file mode 100755
index 0000000..dc6efde
--- /dev/null
+++ b/src/usr/bin/updatezone
@@ -0,0 +1,434 @@
+#!/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 <<ENDUSAGE
+usage: updatezone.sh [-duV] [ --flush ] [ -c conffile | zone1 zone2 ... ]
+version ${updatezoneversion}
+ -d debug Show debugging info, including parsed variables.
+ -u usage Show this usage block.
+ -V version Show script version number.
+ --flush Wipe all the dhcpd-defined entries.
+ -c conffile Choose which conffile. Required if you do not name specific zones
+ zone1 Dns zone defined as UZ_ZONE_NAME in any .conf file in ${default_dir}/
+If the flush action is not requested, the normal action is edit, so you will interactively edit the zonefiles.
+Return values:
+0 Normal
+1 Help or version info displayed
+2 Count or type of flaglessvals is incorrect
+3 Incorrect OS type
+4 Unable to find dependency
+5 Not run as root or sudo
+6 Invalid configuration
+7 Unable to modify zone files
+ENDUSAGE
+}
+
+# DEFINE FUNCTIONS
+
+check_zone_file() {
+ # call: check_zone_file forward "${zone_name}" "${forward_file}" "{tmp_forward_file}"
+ debuglev 9 && ferror "check_zone_file $@"
+ local zone_type="$1"
+ local zone_name="$2"
+ local zone_real_file="$3"
+ local zone_temp_file="$4"
+
+ # if this zone is defined
+ if test -n "${zone_real_file}";
+ then
+ # if this zone file does not exist
+ if test ! -f "${zone_real_file}";
+ then
+ ferror "${scriptfile}: 6. Cannot find file: ${zone_real_file}. Skipping ${zone_type} zone."
+ pause_to_show_error=1
+ rm -f "${zone_temp_file}"
+ else
+ # so the zone file exists
+
+ # make sure we can modify it
+ if ! touch "${zone_real_file}";
+ then
+ ferror "${scriptfile}: 7. Unable to modify zone file ${zone_real_file}. Aborted."
+ exit 7
+ fi
+
+ # freeze zone so the file is up to date
+ zone_action freeze "${zone_name}"
+ echo "${zone_name}" >> "${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 <<EOFLOCATIONS
+./framework.sh
+${scriptdir}/framework.sh
+~/bin/bgscripts/framework.sh
+~/bin/framework.sh
+~/bgscripts/framework.sh
+~/framework.sh
+/usr/local/bin/bgscripts/framework.sh
+/usr/local/bin/framework.sh
+/usr/bin/bgscripts/framework.sh
+/usr/bin/framework.sh
+/bin/bgscripts/framework.sh
+/usr/share/bgscripts/framework.sh
+/usr/libexec/bgscripts/framework.sh
+EOFLOCATIONS
+test -z "${frameworkscript}" && echo "$0: framework not found. Aborted." 1>&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; }
bgstack15