--- /dev/null
+#!/usr/bin/env bash
+
+set -e
+set -u
+
+BASE_NAME="$( basename ${0} )"
+MY_REAL_NAME=$( readlink -f $0 )
+BIN_DIR=$( dirname "${MY_REAL_NAME}" )
+BASE_DIR=$( dirname "${BIN_DIR}" )
+LIB_DIR="${BASE_DIR}/lib"
+CONF_DIR="${BASE_DIR}/etc"
+
+if [[ -f "${LIB_DIR}/functions.rc" ]] ; then
+ . "${LIB_DIR}/functions.rc"
+else
+ echo "Bash resource file '${LIB_DIR}/functions.rc' not found" >&2
+ exit 5
+fi
+
+export LC_ALL="en_US.utf8"
+export LANG="en_US.utf8"
+
+VERSION="1.3"
+
+declare -a ZONES=()
+KEEP_LAST=3
+KEEP_DAYS=15
+KEEP_WEEKS=12
+KEEP_MONTHS=18
+KEEP_YEARS=4
+# Where to store backup copies.
+BACKUP_ROOTDIR="/var/backup/pdns"
+
+BACKUP_TIMESTAMP="$( date +'%Y-%m-%d_%H-%M-%S' )"
+
+# Define, check, create directories.
+BACKUP_DIR=
+
+BYTES_TOTAL=0
+
+detect_color
+
+DESCRIPTION=$( cat <<-EOF
+ Creates a backup of all zones of the global instance of PowerDNS
+ on the current host.
+
+ Only the user '${GREEN}root${NORMAL}' may execute this script.
+
+ EOF
+)
+
+
+
+#------------------------------------------------------------------------------
+usage() {
+ cat <<-EOF
+ Usage: ${BASE_NAME} [Common Options] [-L LAST] [-D DAYS] [-W WEEKS] [-M MONTHS] [-Y YEARS] [--dir DIRECTORY]
+ ${BASE_NAME} [-h|--help]
+ ${BASE_NAME} [-V|--version]
+
+ Options:
+ -L|--last LAST Keep the last ${KEEP_LAST} backup sets.
+ -D|--days DAYS Keep the backup files of the last DAYS. Default: ${KEEP_DAYS} days.
+ -W|--weeks WEEKS
+ Keep the backup files of the last WEEKS. Default: ${KEEP_WEEKS} weeks.
+ -M|--months MONTHS
+ Keep the backup files of the last MONTHS. Default: ${KEEP_MONTHS} months.
+ -Y|--years YEARS
+ Keep the backup files of the last YEARS. Default: ${KEEP_YEARS} years.
+ --dir DIRECTORY The directory, where to keep the backup files. Default: '${BACKUP_ROOTDIR}'."
+ EOF
+
+ echo
+ echo " Common Options:"
+ echo "${STD_USAGE_MSG}"
+
+}
+
+#------------------------------------------------------------------------------
+get_options() {
+
+ local tmp=
+ local base_dir=
+
+ local short_options="L:D:W:M:Y:${STD_SHORT_OPTIONS}"
+ local long_options="last:,days:,weeks:,months:,years:,dir:,${STD_LONG_OPTIONS}"
+
+ set +e
+ tmp=$( getopt -o "${short_options}" --long "${long_options}" -n "${BASE_NAME}" -- "$@" )
+ if [[ $? != 0 ]] ; then
+ echo "" >&2
+ usage >&2
+ exit 1
+ fi
+ set -e
+
+ # Note the quotes around `$TEMP': they are essential!
+ eval set -- "${tmp}"
+ eval_common_options "$@"
+ if [[ "${DEBUG}" == 'y' ]] ; then
+ declare -p REMAINING_OPTS
+ declare -p REMAINING_ARGS
+ fi
+
+ local len="${#REMAINING_OPTS[*]}"
+ local i="0"
+ local j=
+ local arg=
+ while [[ "$i" -lt "${len}" ]] ; do
+
+ arg="${REMAINING_OPTS[$i]}"
+
+ case "${arg}" in
+ -L|--last)
+ j=$(( $i + 1 ))
+ KEEP_LAST="${REMAINING_OPTS[$j]}"
+ i=$(( $i + 2 ))
+ ;;
+ -D|--days)
+ j=$(( $i + 1 ))
+ KEEP_DAYS="${REMAINING_OPTS[$j]}"
+ i=$(( $i + 2 ))
+ ;;
+ -W|--weeks)
+ j=$(( $i + 1 ))
+ KEEP_WEEKS="${REMAINING_OPTS[$j]}"
+ i=$(( $i + 2 ))
+ ;;
+ -M|--months)
+ j=$(( $i + 1 ))
+ KEEP_MONTHS="${REMAINING_OPTS[$j]}"
+ i=$(( $i + 2 ))
+ ;;
+ -Y|--years)
+ j=$(( $i + 1 ))
+ KEEP_YEARS="${REMAINING_OPTS[$j]}"
+ i=$(( $i + 2 ))
+ ;;
+ --dir)
+ j=$(( $i + 1 ))
+ BACKUP_ROOTDIR="${REMAINING_OPTS[$j]}"
+ i=$(( $i + 2 ))
+ ;;
+ *) echo -e "Internal error - option '${RED}${arg}${NORMAL} was wrong!"
+ exit 1
+ ;;
+ esac
+ done
+
+ if [[ "${#REMAINING_ARGS[@]}" != "0" ]] ; then
+ error "Invalid arguments given."
+ echo >&2
+ usage >&2
+ exit 1
+ fi
+
+ local int_val=
+ local wrong_keep_vals="n"
+
+ int_val=$(( $KEEP_LAST + 0 ))
+ if [[ "${int_val}" -le "0" ]] ; then
+ error "Invalid number of keeping '${RED}${KEEP_LAST}${NORMAL}' backup sets."
+ wrong_keep_vals="y"
+ fi
+
+ int_val=$(( $KEEP_DAYS + 0 ))
+ if [[ "${int_val}" -le "0" ]] ; then
+ error "Invalid number of days '${RED}${KEEP_DAYS}${NORMAL}' to keep backup sets."
+ wrong_keep_vals="y"
+ fi
+
+ int_val=$(( $KEEP_WEEKS + 0 ))
+ if [[ "${int_val}" -le "0" ]] ; then
+ error "Invalid number of weeks '${RED}${KEEP_WEEKS}${NORMAL}' to keep backup sets."
+ wrong_keep_vals="y"
+ fi
+
+ int_val=$(( $KEEP_MONTHS + 0 ))
+ if [[ "${int_val}" -le "0" ]] ; then
+ error "Invalid number of months '${RED}${KEEP_MONTHS}${NORMAL}' to keep backup sets."
+ wrong_keep_vals="y"
+ fi
+
+ int_val=$(( $KEEP_YEARS + 0 ))
+ if [[ "${int_val}" -le "0" ]] ; then
+ error "Invalid number of years '${RED}${KEEP_YEARS}${NORMAL}' to keep backup sets."
+ wrong_keep_vals="y"
+ fi
+
+ if [[ "${wrong_keep_vals}" == "y" ]] ; then
+ echo >&2
+ description >&2
+ echo
+ usage >&2
+ exit 1
+ fi
+
+ if command -pv pdnsutil >/dev/null ; then
+ :
+ else
+ error "Command '${RED}pdnsutil${NORMAL}' not found."
+ echo >&2
+ exit 3
+ fi
+
+ if command -pv idn >/dev/null ; then
+ :
+ else
+ error "Command '${RED}idn${NORMAL}' not found."
+ echo >&2
+ exit 3
+ fi
+
+ if command -pv get-file-to-remove >/dev/null ; then
+ :
+ else
+ error "Command '${RED}get-file-to-remove${NORMAL}' not found."
+ echo >&2
+ exit 3
+ fi
+
+ BACKUP_DIR="${BACKUP_ROOTDIR}/${BACKUP_TIMESTAMP}"
+
+}
+
+#########################################
+# Some often used funktions
+
+#------------------------------------------------------------------------------
+MKDIR() {
+ local cmd="mkdir"
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ cmd+=" --verbose"
+ fi
+ eval ${cmd} "$@"
+}
+
+#------------------------------------------------------------------------------
+MV() {
+ local cmd="mv"
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ cmd+=" --verbose"
+ fi
+ eval ${cmd} "$@"
+}
+
+#------------------------------------------------------------------------------
+RMDIR() {
+ local cmd="rmdir"
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ cmd+=" --verbose"
+ fi
+ eval ${cmd} "$@"
+}
+
+#------------------------------------------------------------------------------
+empty_line() {
+# if [[ "${QUIET}" == "y" ]] ; then
+# return 0
+# fi
+ echo
+}
+
+#------------------------------------------------------------------------------
+check_for_root() {
+ local my_id=$( id -u )
+ if [[ "${my_id}" != "0" ]] ; then
+ error "You must be ${RED}root${NORMAL} to execute this script."
+ echo >&2
+ exit 1
+ fi
+}
+
+################################################################################
+
+prepare_dirs() {
+
+ debug "Changing to '${BASE_DIR}' ..."
+ cd "${BASE_DIR}"
+
+ if [[ ! -d "${BACKUP_ROOTDIR}" ]] ; then
+ error "Directory '${RED}${BACKUP_ROOTDIR}${NORMAL}' does not exists or is not a directory."
+ exit 5
+ fi
+ if [[ ! -w "${BACKUP_ROOTDIR}" ]] ; then
+ error "No write access to '${RED}${BACKUP_ROOTDIR}${NORMAL}'."
+ exit 6
+ fi
+
+ info "Creating all necessary directories ..."
+
+ local i=0
+ local new_backup_dir="${BACKUP_DIR}.$( printf "%03d" "$i" )"
+ while [[ -d "${new_backup_dir}" ]] ; do
+ i=$(( $i + 1 ))
+ new_backup_dir="${BACKUP_DIR}.$( printf "%03d" "$i" )"
+ done
+ BACKUP_DIR="${new_backup_dir}"
+ MKDIR -p "${BACKUP_DIR}"
+
+}
+
+#------------------------------------------------------------------------------
+cleanup_old_backups() {
+
+ empty_line
+ info "Cleaning up old backup files and directories ..."
+
+ local verbose_option=""
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ verbose_option="--verbose"
+ fi
+
+ local pattern="\"${BACKUP_ROOTDIR}\"/*"
+ local has_dirs='n'
+ local bdir=
+ local oifs="${IFS}"
+ IFS="
+"
+
+ local -a dirs=()
+
+ local cmd="get-file-to-remove --last ${KEEP_LAST}"
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ cmd+=" --verbose"
+ else
+ cmd+=" --quiet"
+ fi
+ cmd+=" --days ${KEEP_DAYS}"
+ cmd+=" --weeks ${KEEP_WEEKS}"
+ cmd+=" --months ${KEEP_MONTHS}"
+ cmd+=" --years ${KEEP_YEARS}"
+ cmd+=" ${pattern}"
+
+ debug "Executing: ${cmd}"
+
+ for bdir in $( eval ${cmd} ) ; do
+ dirs+=( "${bdir}" )
+ done
+
+ IFS="${oifs}"
+
+ if [[ "${#dirs[*]}" -gt "0" ]] ; then
+ for bdir in "${dirs[@]}" ; do
+ info "Removing directory '${CYAN}${bdir}${NORMAL}' ..."
+ RM -r "${bdir}"
+ done
+ else
+ debug "No old backup directories to remove."
+ fi
+
+}
+
+#------------------------------------------------------------------------------
+get_zones() {
+
+ local zone=
+ for zone in $( pdnsutil list-all-zones | sort ) ; do
+ ZONES+=( "${zone}" )
+ done
+
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ echo
+ echo "Zones to backup:"
+ for zone in "${ZONES[@]}" ; do
+ echo " * '${zone}'"
+ done
+ echo
+ fi
+
+
+}
+
+#------------------------------------------------------------------------------
+backup_zones() {
+
+ local db=
+ for zone in "${ZONES[@]}" ; do
+ backup_zone "${zone}"
+ done
+
+ empty_line
+ info "Backed up ${#ZONES[*]} zones."
+ local k_bytes=$(( ${BYTES_TOTAL} / 1024 ))
+ local m_bytes=$(( ${k_bytes} / 1024 ))
+ local msg=$( printf "Total compressed size: %10d Bytes => %7d KiB => %4d MiB" \
+ "${BYTES_TOTAL}" "${k_bytes}" "${m_bytes}" )
+ info "${msg}"
+}
+
+#------------------------------------------------------------------------------
+backup_zone() {
+
+ local zone="$1"
+ local zone_utf8=$( idn --idna-to-unicode "${zone}" )
+
+ local zone_show="'${zone}'"
+ if [[ "${zone}" != "${zone_utf8}" ]] ; then
+ zone_show="'${zone}' (${zone_utf8})"
+ fi
+
+ local verbose_option=
+ if [[ "${VERBOSE}" == "y" ]] ; then
+ verbose_option="--verbose"
+ fi
+
+ empty_line
+ info "Backing up zone '${GREEN}${zone}${NORMAL}' ..."
+
+ local output_txt="${zone_utf8}.txt"
+ local output_txt_compressed="${output_txt}.bz2"
+ local out_txt_tgt="${BACKUP_DIR}/${output_txt}"
+ local out_txt_tgt_compressed="${BACKUP_DIR}/${output_txt_compressed}"
+
+ local title=
+ local title=$( printf "All information about zone %s:" ${zone_show} )
+ local nr_chars=$( printf "${title}" | wc -c )
+ local title_line=$( printf '%*s\n' "${nr_chars}" '' | tr ' ' '-' )
+ echo -e "\n${title}\n${title_line}\n" > "${out_txt_tgt}"
+ pdnsutil show-zone "${zone}" >> "${out_txt_tgt}" 2>/dev/null
+
+ title="All Resource Records:"
+ nr_chars=$( printf "${title}" | wc -c )
+ title_line=$( printf '%*s\n' "${nr_chars}" '' | tr ' ' '-' )
+ echo -e "\n${title}\n${title_line}\n" >> "${out_txt_tgt}"
+ pdnsutil list-zone "${zone}" >> "${out_txt_tgt}" 2>/dev/null
+
+ local blocks=$(stat -c "%b" "${out_txt_tgt}")
+ local bs=$(stat -c "%B" "${out_txt_tgt}")
+ local bytes=$(stat -c "%s" "${out_txt_tgt}")
+ local b_bytes=$(( ${blocks} * ${bs} ))
+ local k_bytes=$(( ${b_bytes} / 1024 ))
+ local msg=$( printf "Original size of %-60s %10d Bytes => %7d KiB" \
+ "'${output_txt}':" "${bytes}" "${k_bytes}" )
+ info "${msg}"
+
+ debug "Compressing '${out_txt_tgt}' ..."
+ bzip2 ${verbose_option} --best "${out_txt_tgt}"
+
+ blocks=$(stat -c "%b" "${out_txt_tgt_compressed}")
+ bs=$(stat -c "%B" "${out_txt_tgt_compressed}")
+ bytes=$(stat -c "%s" "${out_txt_tgt_compressed}")
+ b_bytes=$(( ${blocks} * ${bs} ))
+ k_bytes=$(( ${b_bytes} / 1024 ))
+
+ BYTES_TOTAL=$(( ${BYTES_TOTAL} + ${b_bytes} ))
+
+ local msg=$( printf "Compressed size of %-60s %10d Bytes => %7d KiB" \
+ "'${output_txt}':" "${bytes}" "${k_bytes}" )
+ info "${msg}"
+
+}
+
+
+
+################################################################################
+##
+## Main
+##
+################################################################################
+
+#------------------------------------------------------------------------------
+main() {
+
+ get_options "$@"
+ check_for_root
+
+ set_locale 'en_US.utf8'
+
+ prepare_dirs
+ info "Starting backup ..."
+ cleanup_old_backups
+ get_zones
+ backup_zones
+
+ empty_line
+ info "Finished."
+
+}
+
+main "$@"
+
+exit 0
+
+# vim: ts=4 et list