Skip to content

Instantly share code, notes, and snippets.

@dennisse
Last active April 30, 2017 11:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dennisse/c28bfb20e0c82d7c2963e87227295212 to your computer and use it in GitHub Desktop.
Save dennisse/c28bfb20e0c82d7c2963e87227295212 to your computer and use it in GitHub Desktop.
nsupdate.sh
#!/usr/bin/env bash
# This file:
#
# - Will check to see if your IP-address has updated, and use `nsupdate` to
# update DNS for you if it has.
#
# Usage:
#
# ./nsupdate.sh -k nsupdate.keyfile
#
# The MIT License (MIT)
# Copyright (c) 2017 Dennis Eriksen <dennis@eriksen.im>
# Web: https://dnns.no
#
#
# Based on a template by BASH3 Boilerplate v2.3.0
# http://bash3boilerplate.sh/#authors
#
# The MIT License (MIT)
# Copyright (c) 2013 Kevin van Zonneveld and contributors
# You are not obligated to bundle the LICENSE file with your b3bp projects as long
# as you leave these references intact in the header comments of your source files.
# Exit on error. Append "|| true" if you expect an error.
set -o errexit
# Exit on error inside any functions or subshells.
set -o errtrace
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
set -o nounset
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
set -o pipefail
# Turn on traces, useful while debugging but commented out by default
# set -o xtrace
__server="DNS-SERVER"
__zone="ZONE-NAME"
__host="HOST TO UPDATE"
__external_ip_host="https://ifup.no"
__tempfile=$(mktemp "/tmp/$(basename "${BASH_SOURCE[0]}").XXXXX")
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
__i_am_main_script="0" # false
if [[ "${__usage+x}" ]]; then
if [[ "${BASH_SOURCE[1]}" = "${0}" ]]; then
__i_am_main_script="1" # true
fi
__b3bp_external_usage="true"
__b3bp_tmp_source_idx=1
fi
else
__i_am_main_script="1" # true
[[ "${__usage+x}" ]] && unset -v __usage
[[ "${__helptext+x}" ]] && unset -v __helptext
fi
# Set magic variables for current file, directory, os, etc.
__dir="$(cd "$(dirname "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[${__b3bp_tmp_source_idx:-0}]}")"
__base="$(basename "${__file}" .sh)"
# Define the environment variables (and their defaults) that this script depends on
LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency
NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected
### Functions
##############################################################################
function __b3bp_log () {
local log_level="${1}"
shift
# shellcheck disable=SC2034
local color_debug="\x1b[35m"
# shellcheck disable=SC2034
local color_info="\x1b[32m"
# shellcheck disable=SC2034
local color_notice="\x1b[34m"
# shellcheck disable=SC2034
local color_warning="\x1b[33m"
# shellcheck disable=SC2034
local color_error="\x1b[31m"
# shellcheck disable=SC2034
local color_critical="\x1b[1;31m"
# shellcheck disable=SC2034
local color_alert="\x1b[1;33;41m"
# shellcheck disable=SC2034
local color_emergency="\x1b[1;4;5;33;41m"
local colorvar="color_${log_level}"
local color="${!colorvar:-${color_error}}"
local color_reset="\x1b[0m"
if [[ "${NO_COLOR:-}" = "true" ]] || [[ "${TERM:-}" != "xterm"* ]] || [[ ! -t 2 ]]; then
if [[ "${NO_COLOR:-}" != "false" ]]; then
# Don't use colors on pipes or non-recognized terminals
color=""; color_reset=""
fi
fi
# all remaining arguments are to be printed
local log_line=""
while IFS=$'\n' read -r log_line; do
if [[ "${__logfile:-}" ]]; then
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" | tee -a "${__logfile}"
else
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${log_level}")${color_reset} ${log_line}" 1>&2
fi
done <<< "${@:-}"
}
function emergency () { __b3bp_log emergency "${@}"; exit 1; }
function alert () { [[ "${LOG_LEVEL:-0}" -ge 1 ]] && __b3bp_log alert "${@}"; true; }
function critical () { [[ "${LOG_LEVEL:-0}" -ge 2 ]] && __b3bp_log critical "${@}"; true; }
function error () { [[ "${LOG_LEVEL:-0}" -ge 3 ]] && __b3bp_log error "${@}"; exit 1; }
function warning () { [[ "${LOG_LEVEL:-0}" -ge 4 ]] && __b3bp_log warning "${@}"; true; }
function notice () { [[ "${LOG_LEVEL:-0}" -ge 5 ]] && __b3bp_log notice "${@}"; true; }
function info () { [[ "${LOG_LEVEL:-0}" -ge 6 ]] && __b3bp_log info "${@}"; true; }
function debug () { [[ "${LOG_LEVEL:-0}" -ge 7 ]] && __b3bp_log debug "${@}"; true; }
function help () {
echo "" 1>&2
echo " ${*}" 1>&2
echo "" 1>&2
echo " ${__usage:-No usage available}" 1>&2
echo "" 1>&2
if [[ "${__helptext:-}" ]]; then
echo " ${__helptext}" 1>&2
echo "" 1>&2
fi
exit 1
}
### Parse commandline options
##############################################################################
# Commandline options. This defines the usage page, and is used to parse cli
# opts & defaults from. The parsing is unforgiving so be precise in your syntax
# - A short option must be preset for every long option; but every short option
# need not have a long option
# - `--` is respected as the separator between options and arguments
# - We do not bash-expand defaults, so setting '~/app' as a default will not resolve to ${HOME}.
# you can use bash variables to work around this (so use ${HOME} instead)
# shellcheck disable=SC2015
[[ "${__usage+x}" ]] || read -r -d '' __usage <<-'EOF' || true # exits non-zero when EOF encountered
-k --keyfile [arg] Keyfile for nsupdate. Required.
-t --tempfile [arg] Location of tempfile. Default="/tmp/nsupdate.sh.XXXXXX"
-l --logfile [arg] Location of logfile.
-v Enable verbose mode, print script as it is executed
-d --debug Enables debug mode
-h --help This page
-n --no-color Disable color output
-1 --one Do just one thing
EOF
# shellcheck disable=SC2015
[[ "${__helptext+x}" ]] || read -r -d '' __helptext <<-'EOF' || true # exits non-zero when EOF encountered
This is Bash3 Boilerplate's help text. Feel free to add any description of your
program or elaborate more on command-line arguments. This section is not
parsed and will be added as-is to the help.
EOF
# Translate usage string -> getopts arguments, and set $arg_<flag> defaults
while read -r __b3bp_tmp_line; do
if [[ "${__b3bp_tmp_line}" =~ ^- ]]; then
# fetch single character version of option string
__b3bp_tmp_opt="${__b3bp_tmp_line%% *}"
__b3bp_tmp_opt="${__b3bp_tmp_opt:1}"
# fetch long version if present
__b3bp_tmp_long_opt=""
if [[ "${__b3bp_tmp_line}" = *"--"* ]]; then
__b3bp_tmp_long_opt="${__b3bp_tmp_line#*--}"
__b3bp_tmp_long_opt="${__b3bp_tmp_long_opt%% *}"
fi
# map opt long name to+from opt short name
printf -v "__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}" '%s' "${__b3bp_tmp_opt}"
printf -v "__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt}" '%s' "${__b3bp_tmp_long_opt//-/_}"
# check if option takes an argument
if [[ "${__b3bp_tmp_line}" =~ \[.*\] ]]; then
__b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg
__b3bp_tmp_init="" # it has an arg. init with ""
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "1"
elif [[ "${__b3bp_tmp_line}" =~ \{.*\} ]]; then
__b3bp_tmp_opt="${__b3bp_tmp_opt}:" # add : if opt has arg
__b3bp_tmp_init="" # it has an arg. init with ""
# remember that this option requires an argument
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2"
else
__b3bp_tmp_init="0" # it's a flag. init with 0
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "0"
fi
__b3bp_tmp_opts="${__b3bp_tmp_opts:-}${__b3bp_tmp_opt}"
fi
[[ "${__b3bp_tmp_opt:-}" ]] || continue
if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Default= ]]; then
# ignore default value if option does not have an argument
__b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}"
if [[ "${!__b3bp_tmp_varname}" != "0" ]]; then
__b3bp_tmp_init="${__b3bp_tmp_line##*Default=}"
__b3bp_tmp_re='^"(.*)"$'
if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then
__b3bp_tmp_init="${BASH_REMATCH[1]}"
else
__b3bp_tmp_re="^'(.*)'$"
if [[ "${__b3bp_tmp_init}" =~ ${__b3bp_tmp_re} ]]; then
__b3bp_tmp_init="${BASH_REMATCH[1]}"
fi
fi
fi
fi
if [[ "${__b3bp_tmp_line}" =~ (^|\.\ *)Required\. ]]; then
# remember that this option requires an argument
printf -v "__b3bp_tmp_has_arg_${__b3bp_tmp_opt:0:1}" '%s' "2"
fi
printf -v "arg_${__b3bp_tmp_opt:0:1}" '%s' "${__b3bp_tmp_init}"
done <<< "${__usage:-}"
# run getopts only if options were specified in __usage
if [[ "${__b3bp_tmp_opts:-}" ]]; then
# Allow long options like --this
__b3bp_tmp_opts="${__b3bp_tmp_opts}-:"
# Reset in case getopts has been used previously in the shell.
OPTIND=1
# start parsing command line
set +o nounset # unexpected arguments will cause unbound variables
# to be dereferenced
# Overwrite $arg_<flag> defaults with the actual CLI options
while getopts "${__b3bp_tmp_opts}" __b3bp_tmp_opt; do
[[ "${__b3bp_tmp_opt}" = "?" ]] && help "Invalid use of script: ${*} "
if [[ "${__b3bp_tmp_opt}" = "-" ]]; then
# OPTARG is long-option-name or long-option=value
if [[ "${OPTARG}" =~ .*=.* ]]; then
# --key=value format
__b3bp_tmp_long_opt=${OPTARG/=*/}
# Set opt to the short option corresponding to the long option
__b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${__b3bp_tmp_long_opt//-/_}"
printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}"
OPTARG=${OPTARG#*=}
else
# --key value format
# Map long name to short version of option
__b3bp_tmp_varname="__b3bp_tmp_opt_long2short_${OPTARG//-/_}"
printf -v "__b3bp_tmp_opt" '%s' "${!__b3bp_tmp_varname}"
# Only assign OPTARG if option takes an argument
__b3bp_tmp_varname="__b3bp_tmp_has_arg_${__b3bp_tmp_opt}"
printf -v "OPTARG" '%s' "${@:OPTIND:${!__b3bp_tmp_varname}}"
# shift over the argument if argument is expected
((OPTIND+=__b3bp_tmp_has_arg_${__b3bp_tmp_opt}))
fi
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists
fi
__b3bp_tmp_varname="arg_${__b3bp_tmp_opt:0:1}"
__b3bp_tmp_default="${!__b3bp_tmp_varname}"
__b3bp_tmp_value="${OPTARG}"
if [[ -z "${OPTARG}" ]] && [[ "${__b3bp_tmp_default}" = "0" ]]; then
__b3bp_tmp_value="1"
fi
printf -v "${__b3bp_tmp_varname}" '%s' "${__b3bp_tmp_value}"
debug "cli arg ${__b3bp_tmp_varname} = (${__b3bp_tmp_default}) -> ${!__b3bp_tmp_varname}"
done
set -o nounset # no more unbound variable references expected
shift $((OPTIND-1))
if [[ "${1:-}" = "--" ]] ; then
shift
fi
fi
### Automatic validation of required option arguments
##############################################################################
for __b3bp_tmp_varname in ${!__b3bp_tmp_has_arg_*}; do
# validate only options which required an argument
[[ "${!__b3bp_tmp_varname}" = "2" ]] || continue
__b3bp_tmp_opt_short="${__b3bp_tmp_varname##*_}"
__b3bp_tmp_varname="arg_${__b3bp_tmp_opt_short}"
[[ "${!__b3bp_tmp_varname}" ]] && continue
__b3bp_tmp_varname="__b3bp_tmp_opt_short2long_${__b3bp_tmp_opt_short}"
printf -v "__b3bp_tmp_opt_long" '%s' "${!__b3bp_tmp_varname}"
[[ "${__b3bp_tmp_opt_long:-}" ]] && __b3bp_tmp_opt_long=" (--${__b3bp_tmp_opt_long//_/-})"
help "Option -${__b3bp_tmp_opt_short}${__b3bp_tmp_opt_long:-} requires an argument"
done
### Cleanup Environment variables
##############################################################################
for __tmp_varname in ${!__b3bp_tmp_*}; do
unset -v "${__tmp_varname}"
done
unset -v __tmp_varname
### Externally supplied __usage. Nothing else to do here
##############################################################################
if [[ "${__b3bp_external_usage:-}" = "true" ]]; then
unset -v __b3bp_external_usage
return
fi
### Signal trapping and backtracing
##############################################################################
function __b3bp_cleanup_before_exit () {
info "Cleaning up. Done"
rm "${__tempfile}"
}
trap __b3bp_cleanup_before_exit EXIT
# requires `set -o errtrace`
__b3bp_err_report() {
local error_code
error_code=${?}
error "Error in ${__file} in function ${1} on line ${2}"
exit ${error_code}
}
# Uncomment the following line for always providing an error backtrace
# trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR
### Command-line argument switches (like -d for debugmode, -h for showing helppage)
##############################################################################
# debug mode
if [[ "${arg_d:?}" = "1" ]]; then
set -o xtrace
LOG_LEVEL="7"
# Enable error backtracing
trap '__b3bp_err_report "${FUNCNAME:-.}" ${LINENO}' ERR
fi
# verbose mode
if [[ "${arg_v:?}" = "1" ]]; then
set -o verbose
fi
# no color mode
if [[ "${arg_n:?}" = "1" ]]; then
NO_COLOR="true"
fi
# help mode
if [[ "${arg_h:?}" = "1" ]]; then
# Help exists with code 1
help "Help using ${0}"
fi
# log-file
if [[ "${arg_l:-}" ]]; then
# check if we have write-permissions to logfile
touch "${arg_l}" >/dev/null 2>&1 || error "You can not write to ${arg_l}"
__logfile="${arg_l}"
fi
# key-file
# check if we can read the key-file
if [ -r "${arg_k}" ]; then
__keyfile="${arg_k}"
else
error "You can not read ${arg_k}"
fi
### Validation. Error out if the things required for your script are not present
##############################################################################
[[ "${arg_k:-}" ]] || help "Setting a keyfile with -k or --keyfile is required"
[[ "${LOG_LEVEL:-}" ]] || emergency "Cannot continue without LOG_LEVEL. "
command -v nsupdate >/dev/null 2>&1 || emergency "This script requires the program `nsupdate` to be installed. It is part of the `dnsutils`-package."
command -v curl >/dev/null 2>&1 || emergency "This script requires the program `curl` to be installed."
command -v sipcalc >/dev/null 2>&1 || emergency "This script requires the program `sipcalc` to be installed."
command -v dig >/dev/null 2>&1 || emergency "This script requires the program `dig` to be installed."
### Runtime
##############################################################################
# first we check IP
if ! __curip=$(curl -s "${__external_ip_host}") \
|| ! sipcalc "${__curip}" | grep -E "CIDR|IPV6 INFO" >/dev/null 2>&1; then
error "Could not determine current IP-address"
fi
info "Current IP-address: ${__curip}"
info "Generating zonefile..."
cat >"${__tempfile}" <<EOL
server ${__server}
zone ${__zone}
update delete ${__host}. A
update add ${__host}. 600 A ${__curip}
show
send
EOL
# we need to get current IP in dns, and validate the returned value
if ! __dnsip=$(dig +short A ${__host} @${__server} 2>&1) \
|| ! sipcalc "${__dnsip}" | grep -E "CIDR|IPV6 INFO" >/dev/null 2>&1; then
error "Could not get current IP-address stored in DNS"
fi
info "Current IP-address in DNS: ${__dnsip}"
# if old and new IP does not mach, we need to do work
if [[ "${__curip}" == "${__dnsip}" ]]; then
notice "Current IP-address (${__curip}) and address in DNS (${__dnsip}) matches. DNS is up to date."
exit 0
fi
info "Current IP-address and address in DNS does not match."
info "Updating DNS..."
nsupdate -k "${__keyfile}" -v "${__tempfile}" >/dev/null || error "Could not update DNS"
info "DNS Updated."
info "Checking if update was correct..."
if ! __updatedip=$(dig +short A ${__host} @${__server} 2>&1) \
|| ! sipcalc "${__updatedip}" | grep -E "CIDR|IPV6 INFO" >/dev/null 2>&1; then
error "Could not get current IP-address stored in DNS"
fi
if [[ "${__curip}" == "${__updatedip}" ]]; then
notice "Update successful.. Old IP: ${__dnsip} New IP: ${__updatedip}"
exit 0
else
error "The new IP in DNS does not match your current IP. Something went wrong with the DNS-update."
fi
# if we've gotten here, the
#info "__i_am_main_script: ${__i_am_main_script}"
#info "__file: ${__file}"
#info "__dir: ${__dir}"
#info "__base: ${__base}"
#info "OSTYPE: ${OSTYPE}"
#
#info "arg_k: ${arg_k}"
#info "arg_z: ${arg_z}"
#info "arg_l: ${arg_l}"
#info "arg_d: ${arg_d}"
#info "arg_v: ${arg_v}"
#info "arg_h: ${arg_h}"
#
#info "$(echo -e "multiple lines example - line #1\nmultiple lines example - line #2\nimagine logging the output of 'ls -al /path/'")"
#
## All of these go to STDERR, so you can use STDOUT for piping machine readable information to other software
#debug "Info useful to developers for debugging the application, not useful during operations."
#info "Normal operational messages - may be harvested for reporting, measuring throughput, etc. - no action required."
#notice "Events that are unusual but not error conditions - might be summarized in an email to developers or admins to spot potential problems - no immediate action required."
#warning "Warning messages, not an error, but indication that an error will occur if action is not taken, e.g. file system 85% full - each item must be resolved within a given time. This is a debug message"
#error "Non-urgent failures, these should be relayed to developers or admins; each item must be resolved within a given time."
#critical "Should be corrected immediately, but indicates failure in a primary system, an example is a loss of a backup ISP connection."
#alert "Should be corrected immediately, therefore notify staff who can fix the problem. An example would be the loss of a primary ISP connection."
#emergency "A \"panic\" condition usually affecting multiple apps/servers/sites. At this level it would usually notify all tech staff on call."
# Cronfile for nsupdate.sh
#
# Copyright 2017 by Dennis Eriksen <dennis@eriksen.im>, web: <https://dnns.no>
*/5 * * * * root LOG_LEVEL=5 /usr/local/sbin/nsupdate.sh -k /root/mrslave.nsupdate.key -l /var/log/nsupdate.sh.log >/dev/null
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment