aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/etc/bash_completion.d/updatezone13
-rw-r--r--src/etc/ddtools/ipa.example.com.conf.example7
-rw-r--r--src/etc/sysconfig/dhcpd-control5
-rwxr-xr-xsrc/usr/bin/dhcpd-control478
-rwxr-xr-xsrc/usr/bin/updatezone434
-rw-r--r--src/usr/share/doc/ddtools/README.txt61
-rw-r--r--src/usr/share/doc/ddtools/version.txt1
7 files changed, 999 insertions, 0 deletions
diff --git a/src/etc/bash_completion.d/updatezone b/src/etc/bash_completion.d/updatezone
new file mode 100644
index 0000000..f55d1ce
--- /dev/null
+++ b/src/etc/bash_completion.d/updatezone
@@ -0,0 +1,13 @@
+_updatezone_autocomplete() {
+ #devtty=/dev/pts/2
+ local cur prev words cword;
+ _init_completion || return
+ local _tmpfile1="$( mktemp )"
+ grep -iE "UZ_ZONE_NAME=" /etc/ddtools/*.conf 2>/dev/null | sed -r -e 's/^UZ_.*=//;' >> "${_tmpfile1}"
+ #echo "----- ${cur} --- prev: ${prev} -- words: ${words} -- cword: ${cword} --" > "${devtty}"
+ grep -viE "${cur}" "${_tmpfile1}"
+ COMPREPLY=($( compgen -W "$( grep -viE "${prev}" "${_tmpfile1}" )" -- "$cur" ))
+ command rm -f "${_tmpfile1}" 1>/dev/null 2>&1
+ return 0
+}
+complete -F _updatezone_autocomplete updatezone
diff --git a/src/etc/ddtools/ipa.example.com.conf.example b/src/etc/ddtools/ipa.example.com.conf.example
new file mode 100644
index 0000000..060005a
--- /dev/null
+++ b/src/etc/ddtools/ipa.example.com.conf.example
@@ -0,0 +1,7 @@
+UZ_ZONE_NAME=ipa.example.com
+UZ_FORWARD_ZONE=ipa.example.com
+UZ_FORWARD_FILE=/var/named/data/db.ipa.example.com
+UZ_REVERSE_ZONE=1.168.192.in-addr.arpa
+UZ_REVERSE_FILE=/var/named/data/db.192.168.1
+UZ_SLAVE_COUNT=1
+UZ_SLAVE_1=dns2
diff --git a/src/etc/sysconfig/dhcpd-control b/src/etc/sysconfig/dhcpd-control
new file mode 100644
index 0000000..3073169
--- /dev/null
+++ b/src/etc/sysconfig/dhcpd-control
@@ -0,0 +1,5 @@
+DHCPD_CONTROL_COMBINED_FILE=/etc/dhcp/dhcpd.combined.conf
+DHCPD_CONTROL_DHCPD_FILE=/etc/dhcp/dhcpd.conf
+DHCPD_CONTROL_LEASES_FILE=/var/lib/dhcpd/dhcpd.leases
+DHCPD_CONTROL_LEASES_TEMP_FILE=/var/lib/dhcpd/dhcpd.leases~
+DHCPD_CONTROL_SERVICE=dhcpd
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; }
diff --git a/src/usr/share/doc/ddtools/README.txt b/src/usr/share/doc/ddtools/README.txt
new file mode 100644
index 0000000..747eee1
--- /dev/null
+++ b/src/usr/share/doc/ddtools/README.txt
@@ -0,0 +1,61 @@
+File: usr/share/ddtools/docs/README.txt
+Package: ddtools
+Author: bgstack15
+Startdate: 2017-05-26
+Title: Readme file for ddtools
+Purpose: All packages should come with a readme
+Usage: Read it.
+Reference: README.txt
+Improve:
+Document: Below this line
+
+### WELCOME
+ddtools is a suite of shell scripts that help manage dns and dhcpd.
+Updatezone provides an easy way to update dns zone files. Intended primarily for bind9 zone files, experimentation is encouraged.
+Instead of running the series of commands manually: rndc freeze, vi zonefile, rndc thaw and so on, use updatezone.
+dhcpd-control helps manage paired dhcpd servers.
+
+### CONFIGURATION
+The conf files belong in /etc/ddtools/. See example in /usr/share/ddtools/examples/.
+
+### USING THIS TOOL
+
+$ updatezone ipa.smith122.com
+Where this file exists: /etc/ddtools/ipa.smith122.com.conf
+
+ UZ_ZONE_NAME=ipa.smith122.com
+ UZ_FORWARD_ZONE=ipa.smith122.com
+ UZ_FORWARD_FILE=/var/named/data/db.ipa.smith122.com
+ UZ_REVERSE_ZONE=1.168.192.in-addr.arpa
+ UZ_REVERSE_FILE=/var/named/data/db.192.168.1
+ UZ_SLAVE_COUNT=1
+ UZ_SLAVE_1=dns2
+
+The updatezone tool searches for the value of UZ_ZONE_NAME to declare a match and use that configuration file.
+The zone definitions are used in the freeze/thaw/retransfer commands.
+
+This tool will only request updates for zones that are updated. Also, you do not need to adjust the serial number at all. The script will detect changes and then increment the serial number for you.
+
+You can also specify multiple zones on the command line.
+$ updatezone ipa.smith122.com ad.smith122.com
+
+You can also use the --flush flag to clear out the A and PTR records whose TTL matches the dhcp server TTL. It ties in nicely with the dhcpd-control --flush command. Remember that you need to give a zone name (or -c conffile) option as well.
+
+Example:
+updatezone --flush ipa.smith122.com
+
+### NOTES
+
+### REFERENCE
+
+### CHANGELOG
+2017-05-27 B Stack <bgstack15@gmail.com> 0.0-2
+- Initial package construction
+
+2017-10-15 B Stack <bgstack15@gmail.com> 0.0-3
+- Rearranged directory structure to match current standards
+- Added bash autocompletion definition for updatezone
+- Added --flush to updatezone to match dhcpd-control
+
+2024-08-19 B. Stack <bgstack15@gmail.com> - 0.0.4-1
+- Rearranged directory structure to match current standards
diff --git a/src/usr/share/doc/ddtools/version.txt b/src/usr/share/doc/ddtools/version.txt
new file mode 100644
index 0000000..81340c7
--- /dev/null
+++ b/src/usr/share/doc/ddtools/version.txt
@@ -0,0 +1 @@
+0.0.4
bgstack15