diff options
author | B. Stack <bgstack15@gmail.com> | 2024-08-19 12:30:40 -0400 |
---|---|---|
committer | B. Stack <bgstack15@gmail.com> | 2024-08-19 12:30:40 -0400 |
commit | 8f27989ba7524851b53140258390bb896674c9c4 (patch) | |
tree | a5adf03f058a6f47b0b7d5dfb8170b356ed40811 /src/usr/bin | |
parent | fix date in rpm changelog (diff) | |
download | ddtools-8f27989ba7524851b53140258390bb896674c9c4.tar.gz ddtools-8f27989ba7524851b53140258390bb896674c9c4.tar.bz2 ddtools-8f27989ba7524851b53140258390bb896674c9c4.zip |
Diffstat (limited to 'src/usr/bin')
-rwxr-xr-x | src/usr/bin/dhcpd-control | 478 | ||||
-rwxr-xr-x | src/usr/bin/updatezone | 434 |
2 files changed, 912 insertions, 0 deletions
diff --git a/src/usr/bin/dhcpd-control b/src/usr/bin/dhcpd-control new file mode 100755 index 0000000..7ee4e0a --- /dev/null +++ b/src/usr/bin/dhcpd-control @@ -0,0 +1,478 @@ +#!/bin/sh +# Filename: dhcpd-control.sh +# Location: +# Author: bgstack15@gmail.com +# Startdate: 2017-05-28 18:18:46 +# Title: Script that Facilitates the Configuration of DHCPD Across a Server Pair +# Purpose: Provides a single command for would take a series of steps +# Package: ddtools +# History: +# 2024-08-19 fix framework path +# Usage: +# Reference: ftemplate.sh 2017-05-24a; framework.sh 2017-05-24a +# order of dhcpd servers to restart https://kb.isc.org/article/AA-01043/0/Recommendations-for-restarting-a-DHCP-failover-pair.html +# merge lines with sed http://www.linuxquestions.org/questions/programming-9/merge-lines-in-a-file-using-sed-191121/ +# Improve: +# provide mechanisms for non-systemd service control +# provide ways to specify what to see in the --list output +# list leases on both servers +# Dependencies: +# systemd +fiversion="2017-05-24a" +dhcpdcontrolversion="2024-08-19a" + +usage() { + less -F >&2 <<ENDUSAGE +usage: dhcpd-control.sh [-duV] [ --flush | --edit | --edit-local | --edit-other | --remove-mac <mac> | --list ] [ --force ] +version ${dhcpdcontrolversion} + -d debug Show debugging info, including parsed variables. + -u usage Show this usage block. + -V version Show script version number. + --flush Clears all current leases + --edit Edit the combined file-- the one shared by both servers. + --edit-local Edit the local file. + --edit-other Edit the other server dhcpd file. + --remove-mac <MAC> Clears the leases for this MAC address. + --list List current leases. +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 +ENDUSAGE +} + +# DEFINE FUNCTIONS + +# DEFINE TRAPS + +clean_dhcpdcontrol() { + { + rm -f ${tmp_dhcpd_combined_file} ${tmp_dhcpd_local_file} ${tmp_dhcpd_other_file} ${tmp_mac_local_file} ${tmp_macless_local_file} ${tmp_leases_other_file} ${tmp_mac_other_file} ${tmp_macless_other_file} + } 1>/dev/null 2>&1 + #use at end of entire script if you need to clean up tmpfiles +} + +CTRLC() { + #trap "CTRLC" 2 + clean_dhcpdcontrol + #useful for controlling the ctrl+c keystroke +} + +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 ${dhcpdcontrolversion}"; exit 1;; + #"i" | "infile" | "inputfile" ) getval;infile1=${tempval};; + "f" | "force" ) DHCPD_CONTROL_FORCE=1;; + "flush" ) action="flush";; + "edit-local" ) action="edit-local";; + "edit-other" ) action="edit-other";; + "edit" ) action="edit";; + "remove-mac" ) getval; DHCPD_CONTROL_MAC_TO_REMOVE="${tempval}"; action="remove-mac";; + "list" ) action="list";; + 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 +action="" +define_if_new interestedparties "bgstack15@gmail.com" +# SIMPLECONF +#define_if_new default_conffile "/etc/sysconfig/dhcpd-control" +define_if_new default_conffile "/etc/sysconfig/dhcpd-control" +#define_if_new defuser_conffile ~/.config/dhcpdcontrol/dhcpdcontrol.conf +define_if_new EDITOR vi + +# 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 + +# SET CUSTOM SCRIPT AND VALUES +#setval 1 sendsh sendopts<<EOFSENDSH # if $1="1" then setvalout="critical-fail" on failure +#/usr/share/bgscripts/send.sh -hs # setvalout maybe be "fail" otherwise +#/usr/local/bin/send.sh -hs # on success, setvalout="valid-sendsh" +#/usr/bin/mail -s +#EOFSENDSH +#test "${setvalout}" = "critical-fail" && ferror "${scriptfile}: 4. mailer not found. Aborted." && exit 4 + +# 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 2; +#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}" + +## START READ CONFIG FILE TEMPLATE +#oIFS="${IFS}"; IFS="$( printf '\n' )" +#infiledata=$( ${sed} ':loop;/^\/\*/{s/.//;:ccom;s,^.[^*]*,,;/^$/n;/^\*\//{s/..//;bloop;};bccom;}' "${infile1}") #the crazy sed removes c style multiline comments +#IFS="${oIFS}"; infilelines=$( echo "${infiledata}" | wc -l ) +#{ echo "${infiledata}"; echo "ENDOFFILE"; } | { +# while read line; do +# # the crazy sed removes leading and trailing whitespace, blank lines, and comments +# if test ! "${line}" = "ENDOFFILE"; +# then +# line=$( echo "${line}" | sed -e 's/^\s*//;s/\s*$//;/^[#$]/d;s/\s*[^\]#.*$//;' ) +# if test -n "${line}"; +# then +# debuglev 8 && ferror "line=\"${line}\"" +# if echo "${line}" | grep -qiE "\[.*\]"; +# then +# # new zone +# zone=$( echo "${line}" | tr -d '[]' ) +# debuglev 7 && ferror "zone=${zone}" +# else +# # directive +# varname=$( echo "${line}" | awk -F= '{print $1}' ) +# varval=$( echo "${line}" | awk -F= '{$1=""; printf "%s", $0}' | sed 's/^ //;' ) +# debuglev 7 && ferror "${zone}${varname}=\"${varval}\"" +# # simple define variable +# eval "${zone}${varname}=\${varval}" +# fi +# ## this part is untested +# #read -p "Please type something here:" response < ${thistty} +# #echo "${response}" +# fi +# else + +## REACT TO BEING A CRONJOB +#if test ${is_cronjob} -eq 1; +#then +# [ ] +#else +# [ ] +#fi + +# SET TRAPS +trap "CTRLC" 2 +#trap "CTRLZ" 18 +trap "clean_dhcpdcontrol" 0 + +# MAIN LOOP +#{ + + # use DHCPD_CONTROL_COMBINED_FILE and DHCPD_CONTROL_DHCPD_FILE + + # derive if primary or secondary server + is_primary="$( sed -n -r -e '/failover.*\{/,/\}/p' ${DHCPD_CONTROL_DHCPD_FILE} | grep -iE "^\s*primary" )" + if ! test -n "${is_primary}"; + then + if ! fistruthy "${DHCPD_CONTROL_FORCE}"; + then + ferror "${scriptfile}: 4. Canot determine that this is the primary server. Try --force option. Aborted." + exit 4 + fi + fi + + # Derive secondary server for later actions + define_if_new DHCPD_CONTROL_OTHER_SERVER "$( grep -oiE "peer address [0-9.]{7,15}\s*;" "${DHCPD_CONTROL_DHCPD_FILE}" | tr -dc '[0-9.]' )" + + # DEBUG SIMPLECONF + debuglev 5 && { + ferror "Using values" + # used values: EX_(OPT1|OPT2|VERBOSE) + set | grep -iE "^DHCPD_CONTROL_" 1>&2 + } + + update_conf_local=0 + update_leases_local=0 + restart_service_local=0 + update_conf_other=0 + update_leases_other=0 + restart_service_other=0 + debuglev 8 && ferror "BEGIN action ${action}" + case "${action}" in + + "flush") + debuglev 8 && ferror "BEGIN flush" + # Clear temorary leases file + debuglev 4 && ferror "Flushing all leases" + if test -z "${DHCPD_CONTROL_LEASES_TEMP_FILE}"; + then + ferror "Skipping leases temp file. Variable not defined: DHCPD_CONTROL_LEASES_TEMP_FILE." + else + if test -f "${DHCPD_CONTROL_LEASES_TEMP_FILE}"; + then + case "${DHCPD_CONTROL_LEASES_TEMP_FILE}" in + /var/lib/dhcp*) + systemctl stop "${DHCPD_CONTROL_SERVICE}" + rm -f "${DHCPD_CONTROL_LEASES_TEMP_FILE}" + update_leases_other=1; + restart_service_other=1; + restart_service_local=1; + ;; + *) + ferror "Will not delete unsafe leases temp file ${DHCPD_CONTROL_LEASES_TEMP_FILE}." + ;; + esac + fi + fi + # Clear leases file + if test -z "${DHCPD_CONTROL_LEASES_FILE}"; + then + ferror "Skipping leases file. Variable not defined: DHCPD_CONTROL_LEASES_FILE." + else + if test -f "${DHCPD_CONTROL_LEASES_FILE}"; + then + case "${DHCPD_CONTROL_LEASES_FILE}" in + /var/lib/dhcp*) + systemctl stop "${DHCPD_CONTROL_SERVICE}" + printf "" > "${DHCPD_CONTROL_LEASES_FILE}" + update_leases_other=1; + restart_service_other=1; + restart_service_local=1; + ;; + *) + ferror "Will not clear unsafe leases file ${DHCPD_CONTROL_LEASES_FILE}." + ;; + esac + fi + fi + ;; + + "edit") + debuglev 8 && ferror "BEGIN edit" + # prepare temp file + tmp_dhcpd_combined_file="$( mktemp -p /tmp dhcpd.combined.XXXXX )" + cp -p "${DHCPD_CONTROL_COMBINED_FILE}" "${tmp_dhcpd_combined_file}" + # edit file + $EDITOR "${tmp_dhcpd_combined_file}" + # if change occurred, prepare to replace + if ! cmp -s "${DHCPD_CONTROL_COMBINED_FILE}" "${tmp_dhcpd_combined_file}"; + then + debuglev 1 && ferror "Updating dhcpd combined file." + bup "${DHCPD_CONTROL_COMBINED_FILE}" + cp -p "${tmp_dhcpd_combined_file}" "${DHCPD_CONTROL_COMBINED_FILE}" + update_conf_other=1 + restart_service_local=1 + restart_service_other=1 + fi + ;; + + "edit-local") + debuglev 8 && ferror "BEGIN edit-local" + # prepare temp file + tmp_dhcpd_local_file="$( mktemp -p /tmp dhcpd.XXXXX )" + cp -p "${DHCPD_CONTROL_DHCPD_FILE}" "${tmp_dhcpd_local_file}" + $EDITOR "${tmp_dhcpd_local_file}" + # if change occurred, prepare to replace + if ! cmp -s "${DHCPD_CONTROL_DHCPD_FILE}" "${tmp_dhcpd_local_file}"; + then + debuglev 1 && ferror "Updating local dhcpd file." + bup "${DHCPD_CONTROL_DHCPD_FILE}" + cp -p "${tmp_dhcpd_local_file}" "${DHCPD_CONTROL_DHCPD_FILE}" + restart_service_local=1 + fi + ;; + + "edit-other") + debuglev 8 && ferror "BEGIN edit-other" + tmp_dhcpd_other_file="$( mktemp -p /tmp dhcpd.other.XXXXX )" + scp -p "${DHCPD_CONTROL_OTHER_SERVER}:${DHCPD_CONTROL_DHCPD_FILE}" "${tmp_dhcpd_other_file}" + cp -p "${tmp_dhcpd_other_file}" "${tmp_dhcpd_other_file}8" #arbitrary number # edit file + ${EDITOR} "${tmp_dhcpd_other_file}8" + if ! cmp -s "${tmp_dhcpd_other_file}" "${tmp_dhcpd_other_file}8"; + then + debuglev 1 && ferror "Updating other server dhcpd file." + ssh "${DHCPD_CONTROL_OTHER_SERVER}" bup "${DHCPD_CONTROL_DHCPD_FILE}"; + scp -p "${tmp_dhcpd_other_file}8" "${DHCPD_CONTROL_OTHER_SERVER}:${DHCPD_CONTROL_DHCPD_FILE}"; + restart_service_other=1 + fi + ;; + + "remove-mac") + debuglev 8 && ferror "BEGIN remove-mac" + # working on this + # sed -n -r -e '/^lease.*\{/,/^\}/{/^lease|hardware|\}/{p}}' /tmp/foo1 | sed -e ':a;/\}/!{N;s/\n/ /;ba};' # base form + # sed -n -r -e '/^lease.*\{/,/^\}/{p}' /tmp/foo1 | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' # slightly trimmed + # sed -n -r -e '/\{/,/^\}/{p}' /tmp/foo1 | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | grep -iE "ec:9a:74:48:bc:c4" # find the one mac address + + # prepare temp files + tmp_mac_local_file="$( mktemp -p /tmp leases.mac.XXXXX )" + tmp_macless_local_file="$( mktemp -p /tmp leases.macless.XXXXX )" + tmp_leases_other_file="$( mktemp -p /tmp leases.other.XXXXX )" + tmp_mac_other_file="$( mktemp -p /tmp leases.mac.XXXXX )" + tmp_macless_other_file="$( mktemp -p /tmp leases.macless.XXXXX )" + if test -z "${DHCPD_CONTROL_MAC_TO_REMOVE}"; + then + ferror "${scripttrim}: 2. No MAC address provided. aborted." + exit 2 + fi + + scp -p "${DHCPD_CONTROL_OTHER_SERVER}:${DHCPD_CONTROL_LEASES_FILE}" "${tmp_leases_other_file}" + + # prepare list of local leases to clear + sed -n -r -e '/\{/,/^\}/{p}' "${DHCPD_CONTROL_LEASES_FILE}" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | grep -iE "${DHCPD_CONTROL_MAC_TO_REMOVE}" > "${tmp_mac_local_file}" + # prepare list of other leases to clear + sed -n -r -e '/\{/,/^\}/{p}' "${tmp_leases_other_file}" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | grep -iE "${DHCPD_CONTROL_MAC_TO_REMOVE}" > "${tmp_mac_other_file}" + + # remove leases from this dhcpd server + if test -n "$( cat "${tmp_mac_local_file}" )"; + then + ferror "Removing leases from this dhcpd server:" + cat "${tmp_mac_local_file}" 1>&2 + fi + sed -n -r -e '/\{/,/^\}/{p}' "${DHCPD_CONTROL_LEASES_FILE}" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | grep -viE "${DHCPD_CONTROL_MAC_TO_REMOVE}" > "${tmp_macless_local_file}" + if ! cmp -s "${tmp_macless_local_file}" "${DHCPD_CONTROL_LEASES_FILE}" + then + systemctl stop "${DHCPD_CONTROL_SERVICE}" + cp -p "${tmp_macless_local_file}" "${DHCPD_CONTROL_LEASES_FILE}" + restart_service_local=1 + fi + + # remove leases from other dhcpd server + if test -n "$( cat "${tmp_mac_other_file}" )"; + then + ferror "Removing leases from other dhcpd server:" + cat "${tmp_mac_other_file}" 1>&2 + fi + sed -n -r -e '/\{/,/^\}/{p}' "${tmp_leases_other_file}" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | grep -viE "${DHCPD_CONTROL_MAC_TO_REMOVE}" > "${tmp_macless_other_file}" + if ! cmp -s "${tmp_macless_other_file}" "${tmp_leases_other_file}"; + then + ssh "${DHCPD_CONTROL_OTHER_SERVER}" systemctl stop "${DHCPD_CONTROL_SERVICE}" + scp -p "${tmp_macless_other_file}" "${DHCPD_CONTROL_LEASES_FILE}" + restart_service_other=1 + fi + ;; + + "list") + debuglev 8 && ferror "BEGIN list" + lease_type="active" + #sed -n -r -e '/\{/,/^\}/{p}' /var/lib/dhcpd/dhcpd.leases | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | grep -iE "active" + #sed -n -r -e '/\{/,/^\}/{p}' /var/lib/dhcpd/dhcpd.leases | grep -iE "\{|\}|client-fqdn|hostname|hardware|starts|ends" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' | awk '!x[$14]++' | sort -k2 + #sed -n -r -e '/\{/,/^\}/{p}' /var/lib/dhcpd/dhcpd.leases | grep -iE "\{|\}|client-fqdn|hostname|hardware|starts|ends" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' -e 's | awk '!x[$14]++' | sort -k2 + #sed -n -r -e '/\{/,/^\}/{p}' /var/lib/dhcpd/dhcpd.leases | grep -iE "\{|\}|client-fqdn|hostname|hardware|starts|ends" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' -e 's/hardware ethernet/mac/;' | awk '!x[$13]++' | sort -k2 + sed -n -r -e '/\{/,/^\}/{p}' "${DHCPD_CONTROL_LEASES_FILE}" | grep -iE "\{|\}|client-fqdn|hostname|hardware|starts|ends" | sed -e ':a;/\}/!{N;s/\n/ /;ba};' -e 's/\s\+/ /g;' -e 's/hardware ethernet/mac/;' | grep -viE "failover peer" | awk '!x[$13]++' | sort -k2 + ;; + *) + ferror "Gotta say unh! Action ${action} ${fallopts} not yet implemented. Aborted." + exit 1 + ;; + esac + + # Prepare instructions for other server + debuglev 8 && ferror "BEGIN prepare instructions for other server" + local_instructions="" + other_instructions="" + instructions="" + fistruthy "${update_leases_other}" && \ + other_instructions="${other_instructions}systemctl stop ${DHCPD_CONTROL_SERVICE}\; rm -f ${DHCPD_CONTROL_LEASES_TEMP_FILE}\; echo \"\" \> ${DHCPD_CONTROL_LEASES_FILE}\; " + fistruthy "${update_conf_other}" && \ + local_instructions="${local_instructions}scp ${DHCPD_CONTROL_COMBINED_FILE} ${DHCPD_CONTROL_OTHER_SERVER}:${DHCPD_CONTROL_COMBINED_FILE}; " + fistruthy "${restart_service_local}" && \ + instructions="${instructions}systemctl restart ${DHCPD_CONTROL_SERVICE}.service" + fistruthy "${restart_service_other}" && \ + other_instructions="${other_instructions}systemctl restart ${DHCPD_CONTROL_SERVICE}" + + # Instruct other server to act + debuglev 8 && ferror "BEGIN instruct other server to act" + if test -n "${DHCPD_CONTROL_OTHER_SERVER}"; + then + debuglev 1 && { + test -n "${local_instructions}" && { + ferror "run local commands:" + ferror "${local_instructions}" + } + test -n "${other_instructions}" && { + ferror "run on other server ${DHCPD_CONTROL_OTHER_SERVER}:" + ferror "ssh ${DHCPD_CONTROL_OTHER_SERVER} ${other_instructions}" + } + } + test -n "${local_instructions}" && ${local_instructions} + test -n "${other_instructions}" && ssh ${DHCPD_CONTROL_OTHER_SERVER} eval ${other_instructions} + fi + + # Local actions regardless of other server + if test -n "${instructions}"; + then + debuglev 1 && { + ferror "run commands:" + ferror "${instructions}" + } + ${instructions} + fi + +#} | tee -a ${logfile} + +# EMAIL LOGFILE +#${sendsh} ${sendopts} "${server} ${scriptfile} out" ${logfile} ${interestedparties} + +## STOP THE READ CONFIG FILE +#exit 0 +#fi; done; } 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; } |