Skip to content

Instantly share code, notes, and snippets.

@quinncomendant
Last active February 21, 2022 22:22
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 quinncomendant/389b76f4a4bc8c190f23 to your computer and use it in GitHub Desktop.
Save quinncomendant/389b76f4a4bc8c190f23 to your computer and use it in GitHub Desktop.
Manage cloud DNS services on the command line. A wrapper for DNS provider API tools (denominator, lexicon).
#!/usr/bin/env bash
#
# Quinn Comendant <quinn@strangecode.com>
# 06 Aug 2015 17:51:37
#
# Config
#
set -euo pipefail;
DNS_PROVIDER='rackspace';
API_TOOL='lexicon';
# Default SOA values used when creating a zone.
SOA_EMAIL='dnsadmin@strangecode.com';
DEFAULT_TTL=300; # (300 minimum for Rackspace Cloud DNS).
REGEX_DOMAIN='^(\*\.)?[_.a-zA-Z0-9-]+\.[a-zA-Z]+\.?$';
REGEX_IPADDR='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$';
REGEX_IPV6ADDR='^[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+:[0-9a-fA-F]+$';
REGEX_MX='^[0-9]+\ +[_.a-zA-Z0-9-]+\.[a-zA-Z]+\.?$';
REGEX_SRV='^[0-9]+\ +[0-9]+\ +[0-9]+\ +[_.a-zA-Z0-9-]+\.[a-zA-Z]+\.?$';
SCRIPTNAME=$(basename "$0");
#
# Functions
#
function shortusage {
err "Usage: $SCRIPTNAME [OPTIONS] DOMAIN VERB [NAME TYPE DATA]
Manage cloud DNS services via API. For full documentation run with -h";
}
function usage {
err "Usage: $SCRIPTNAME [OPTIONS] DOMAIN VERB [NAME TYPE DATA]
Manage cloud DNS services. A wrapper for DNS provider API tools (denominator, lexicon).
OPTIONS
-d HOST When creating a new zone, also add default records, using IP or hostname ‘HOST’ as the main A record.
--dry Only show what commands would be run (list and get, read-only functions, will still run).
-h Show this help message.
-o HOST When creating a new zone (options -c -d), also add an origin CNAME to IP or hostname ‘HOST’.
-p NAME The name of the DNS provider. Currently: $DNS_PROVIDER
-r Reduce SOURCEDOMAIN to apex for mirror command, e.g., sub.example.com → example.com.
-v Display verbose output.
-y Auto-confirm 'yes' to all prompts.
DOMAIN The domain name of the zone.
COMMANDS
create
Create a new zone using the given DOMAIN.
destroy
Destroy an existing zone with the given DOMAIN.
list
List all records in zone DOMAIN.
exists
Returns exit code 0 if the domain exists, or code 1 if not exists.
get [NAME] TYPE
Get all records in zone DOMAIN that match NAME and TYPE.
If NAME is excluded, DOMAIN is used as the NAME.
add [NAME] TYPE DATA
Add a record in zone DOMAIN with NAME, TYPE, and DATA.
If NAME is excluded, DOMAIN is used as the NAME.
replace [NAME] TYPE DATA
Replace the DATA in a record from zone DOMAIN that match NAME and TYPE.
If NAME is excluded, DOMAIN is used as the NAME.
rm [NAME] TYPE DATA
remove [NAME] TYPE DATA
Remove a record from zone DOMAIN with NAME, TYPE, and DATA.
If NAME is excluded, DOMAIN is used as the NAME.
mirror SOURCEDOMAIN TYPE|all
Make the records in zone DOMAIN match TYPE|all from SOURCEDOMAIN.
This will delete records from DOMAIN that do not exist in SOURCEDOMAIN.
NAME The name of the record. If excluded, DOMAIN is used as the NAME.
TYPE The type of the record, one of: A, AAAA, CNAME, MX, NS, SRV or TXT.
DATA The data of the record, e.g., and IP address in case of 'A' records,
domain name for CNAME/NS, formatted string for MX/SRV, or any string for TXT records.
String must 'be quoted' if the DATA contains spaces.
EXAMPLES
$SCRIPTNAME example.com create
$SCRIPTNAME example.com destroy
Creates and destroys a zone with the domain example.com.
$SCRIPTNAME -y -d webhost.example.com example.com create
$SCRIPTNAME -y -d 1.1.1.1 example.com create
Both of these create a new zone with some default records for a website
hosted under the IP address 1.1.1.1 or IP resolved by webhost.example.com.
$SCRIPTNAME example.com list
List all records in the zone example.com.
$SCRIPTNAME example.com get www.example.com cname
Gets the data stored for CNAME record(s) of www.example.com
in zone example.com. Can return multiple valus.
$SCRIPTNAME example.com add example.com a 1.1.1.1
Creates an 'A' record for example.com: 1.1.1.1 in the zone example.com.
$SCRIPTNAME example.com add a 1.1.1.1
The same as above (optional NAME excluded).
$SCRIPTNAME example.com add sub.example.com txt 6fa035cfd4216735a7e45a9c2489d3e9
Creates a TXT record for sub.example.com: 6fa035cfd4216735a7e45a9c2489d3e9
in the zone example.com.
$SCRIPTNAME example.com replace www.example.com cname asdf.com
Adds, or replaces (if existing) the CNAME for www.example.com: asdf.com
$SCRIPTNAME example.com remove www.example.com cname asdf.com
Removes the CNAME record for www.example.com containing asdf.com
$SCRIPTNAME example.com mirror foo.com mx
Sets the MX records for example.com to the values used in foo.com
DEVELOPMENT
Run tests with the /root/bin/dns-test script.
";
}
function denominator_wrapper {
# Args in an array to preserve quoting http://mywiki.wooledge.org/BashFAQ/050
local ARGS=("$@") DENOMINATOR_CLI DENOMINATOR_CONFIG_FILE DENOMINATOR_OPTIONS DENOMINATOR_VERBOSE='' DENOMINATOR_CMD_FOR_PRINT;
# Location of denominator binary (just use ~/bin/denominator if not found elsewhere).
DENOMINATOR_CLI=$(command -v denominator 2>/dev/null || echo ~/bin/denominator);
# Default configuration name.
DENOMINATOR_CONFIG_FILE=~/etc/denominator.yml;
# Options to pass to denominator.
DENOMINATOR_OPTIONS="-C $DENOMINATOR_CONFIG_FILE -n $DNS_PROVIDER";
if [[ -n "$VERBOSE" ]]; then
DENOMINATOR_VERBOSE='-q';
fi
DENOMINATOR_CMD_FOR_PRINT="$DENOMINATOR_CLI $DENOMINATOR_VERBOSE $DENOMINATOR_OPTIONS ${ARGS[*]}";
debug "$DENOMINATOR_CMD_FOR_PRINT";
if [[ -n "$DRY" ]]; then
echo "$DENOMINATOR_CMD_FOR_PRINT" 1>&2
else
# shellcheck disable=SC2086
"$DENOMINATOR_CLI" $DENOMINATOR_VERBOSE $DENOMINATOR_OPTIONS "${ARGS[@]}" || err "Command returned error code $?:\n$DENOMINATOR_CMD_FOR_PRINT";
fi
}
function lexicon_wrapper {
# Args in an array to preserve quoting http://mywiki.wooledge.org/BashFAQ/050
local ARGS=("$@") LEXICON_CLI LEXICON_OUTPUT LEXICON_DELEGATED='' LEXICON_LOG_LEVEL='' LEXICON_CMD_FOR_PRINT;
# Load lexicon credentials
if [[ -r ~/etc/lexicon-env.sh ]]; then
# shellcheck disable=SC1090
source ~/etc/lexicon-env.sh;
else
err "Failed to load ~/etc/lexicon-env.sh";
fi
# Location of lexicon (just use ~/.venv/bin/lexicon if not found elsewhere).
LEXICON_CLI=$(command -v lexicon 2>/dev/null || echo ~/.venv/bin/lexicon);
# Lexicon output mode.
LEXICON_OUTPUT="--output TABLE-NO-HEADER";
if [[ -n "$2" ]]; then
# lexicon requires a separate argument to specify the zone apex. Since `dns` expects $DOMAIN to always be the zone apex, we pass it as the --delegated option.
LEXICON_DELEGATED="--delegated $2";
fi
if [[ -n "$VERBOSE" ]]; then
LEXICON_LOG_LEVEL="--log_level DEBUG";
fi
LEXICON_CMD_FOR_PRINT="$LEXICON_CLI $LEXICON_OUTPUT $LEXICON_DELEGATED $LEXICON_LOG_LEVEL $DNS_PROVIDER ${ARGS[*]}";
debug "$LEXICON_CMD_FOR_PRINT";
if [[ -n "$DRY" ]]; then
echo "$LEXICON_CMD_FOR_PRINT" 1>&2
else
# shellcheck disable=SC2086
"$LEXICON_CLI" $LEXICON_OUTPUT $LEXICON_DELEGATED $LEXICON_LOG_LEVEL "$DNS_PROVIDER" "${ARGS[@]}" || err "Command returned error code $?:\n$LEXICON_CMD_FOR_PRINT";
fi
}
function err {
local MSG="$*";
local IFS='';
echo -e "$MSG" 1>&2
exit 1;
}
function debug {
local MSG="$*";
if [[ -n $VERBOSE ]]; then
echo "$MSG" 1>&2
fi
}
function prompt {
local MSG="$*";
if [[ -n "$ALWAYSYES" ]]; then
echo "$MSG (auto-continuing…)";
else
read -rp "$MSG [y/N]: " YN;
case $YN in
(y|Y);;
(*) err "Nokay, exiting.";;
esac
fi
}
function _invalid_args {
local EXPECTED=$1;
local FOUND=$2;
err "Can't find '$EXPECTED' in '$FOUND'.";
}
function _validate {
local NAME=$1;
local TYPE=$2;
local DATA=$3;
debug "Validating: $NAME $TYPE $DATA";
# Error if NAME is not part of DOMAIN.
local REGEX_NAMEINDOMAIN="^((\*\.)?[_.a-zA-Z0-9-]+\.)*$DOMAIN\$";
[[ "$NAME" =~ $REGEX_NAMEINDOMAIN ]] || err "$NAME must be within zone $DOMAIN";
# Error if TYPE not valid.
case $TYPE in
(A) [[ "$DATA" =~ $REGEX_IPADDR ]] || err "$TYPE record data '$DATA' must be an IP address.";;
(AAAA) [[ "$DATA" =~ $REGEX_IPV6ADDR ]] || err "$TYPE record data '$DATA' must be a valid IPv6 address.";;
(CNAME) [[ "$DATA" =~ $REGEX_DOMAIN ]] || err "$TYPE record data '$DATA' must be a domain.";;
(MX) [[ "$DATA" =~ $REGEX_MX ]] || err "$TYPE record data '$DATA' must be formatted like '10 example.com'.";;
(SRV) [[ "$DATA" =~ $REGEX_SRV ]] || err "$TYPE record data '$DATA' must be formatted like '10 100 443 example.com' (priority weight port target).";;
(NS)
[[ "$NAME" == "$DOMAIN" ]] || err "$TYPE record name '$NAME' must match '$DOMAIN'.";
[[ "$DATA" =~ $REGEX_DOMAIN ]] || err "$TYPE record data '$DATA' must be a domain.";
;;
(TXT) [[ -n "$DATA" ]] || err "$TYPE record data must not be empty.";;
(*) err "I don't know how to deal with '$TYPE' records. Try one of: A, AAAA, CNAME, MX, SRV, NS, TXT.";;
esac
}
function get_zone_apex() {
local apex=$1;
local ns='';
while [[ -n "$apex" && -z "$ns" ]]; do
ns=$(dig "$apex." ns +noall +nottlid +nocl +answer | grep -E "^$apex.[[:space:]]+NS[[:space:]]" | awk '{print $3}' | xargs);
if [[ -n "$ns" || $(tr -dC . <<<"$apex" | wc -c) -lt 2 ]] || grep -Eq '\.[a-z][a-z][a-z]?\.[a-z][a-z]$' <<<"$apex"; then
break;
fi
apex=$(perl -pe 's/^[a-z0-9-]+\.//' <<<"$apex");
done
echo "$apex";
return;
}
function _create {
prompt "Create new zone '$DOMAIN' ($DNS_PROVIDER)?";
local APEX;
APEX=$(get_zone_apex "$DOMAIN");
[[ "$APEX" == "$DOMAIN" ]] || err "$DOMAIN is not a zone apex ($APEX is).";
case $API_TOOL in
(denominator) denominator_wrapper zone add --email "$SOA_EMAIL" --ttl "$DEFAULT_TTL" --name "$DOMAIN";;
(lexicon) lexicon_wrapper create_domain "$DOMAIN" --email_address "$SOA_EMAIL" --ttl "$DEFAULT_TTL";;
esac
# If the default Strangecode records were requested, add them now.
if [[ -n "$ADD_DEFAULTS" ]]; then
# If IP passed to -d is not an IP, do a name lookup.
if [[ ! "$DEFAULT_IPADDR" =~ $REGEX_IPADDR ]]; then
DEFAULT_IPADDR=$(dig "$DEFAULT_IPADDR" +short +search);
debug "DEFAULT_IPADDR was given a hostname, resolved to IP: $DEFAULT_IPADDR";
fi
[[ "$DEFAULT_IPADDR" =~ $REGEX_IPADDR ]] || err "-d IP option doesn't look right.";
_add "$DOMAIN" "NS" "ns.strangecode.com";
_add "$DOMAIN" "NS" "ns2.strangecode.com";
_remove "$DOMAIN" "NS" "dns1.stabletransit.com";
_remove "$DOMAIN" "NS" "dns2.stabletransit.com";
_add "$DOMAIN" "A" "$DEFAULT_IPADDR";
_add "www.$DOMAIN" "CNAME" "$DOMAIN";
_add "$DOMAIN" "MX" "10 mx.strangecode.com";
_add "mail.$DOMAIN" "CNAME" "mx.strangecode.com";
_add "webmail.$DOMAIN" "CNAME" "webmail.strangecode.com";
_add "$DOMAIN" "TXT" "v=spf1 include:_spf.strangecode.com ~all";
_add "$(echo "$DOMAIN" | tr -d '.').alias.strangecode.com" "A" "$DEFAULT_IPADDR" "strangecode.com";
if [[ -n "$ORIGIN_IPADDR" ]]; then
# If IP passed to -o is not an IP, do a name lookup.
if [[ ! "$ORIGIN_IPADDR" =~ $REGEX_IPADDR ]]; then
ORIGIN_IPADDR=$(dig "$ORIGIN_IPADDR" +short +search);
debug "ORIGIN_IPADDR was given a hostname, resolved to IP: $ORIGIN_IPADDR";
fi
_add "$(echo "$DOMAIN" | tr -d '.').origin.strangecode.com" "A" "$ORIGIN_IPADDR" "strangecode.com";
fi
fi
}
function _destroy {
prompt "Destroy entire zone '$DOMAIN' ($DNS_PROVIDER)?";
case $API_TOOL in
(denominator)
# Get ID of zone.
local ZONEID;
ZONEID=$(denominatorwrapper zone list --name "$DOMAIN" | awk '{print $1}');
debug "Zone ID of requested domain: $ZONEID";
[[ "$ZONEID" =~ ^[0-9]+$ ]] || err "Zone not found for domain '$DOMAIN'";
# Delete zone having this ID.
denominator_wrapper zone delete --id "$ZONEID";
;;
(lexicon) lexicon_wrapper delete_domain "$DOMAIN";;
esac
}
function _list {
local DOMAIN=${1:-$DOMAIN};
echo "Listing zone '$DOMAIN' ($DNS_PROVIDER).";
local oDRY=$DRY && DRY=''; # Turn off dry-run for this read-only function.
case $API_TOOL in
(denominator) denominator_wrapper record -z "$DOMAIN" list;;
(lexicon) lexicon_wrapper list "$DOMAIN" | cut -d ' ' -f 2- | sed -e 's/^ *//g' -e 's/ *$//g';;
esac
DRY=$oDRY;
}
function _get {
local NAME=$1;
local TYPE=$2;
local DOMAIN=${3:-$DOMAIN};
echo "Getting '$NAME $TYPE' from zone '$DOMAIN' ($DNS_PROVIDER).";
local oDRY=$DRY && DRY=''; # Turn off dry-run for this read-only function.
case $API_TOOL in
(denominator) denominator_wrapper record -z "$DOMAIN" get -n "$NAME" -t "$TYPE";;
(lexicon) lexicon_wrapper list "$DOMAIN" "$TYPE" --name "$NAME" | cut -d ' ' -f 2- | sed 's/^ *//g';;
# (lexicon) lexicon_wrapper list "$DOMAIN" "$TYPE" | awk "$(printf '$2 == toupper("%s") && $3 == tolower("%s")' "$TYPE" "$NAME")";;
esac
DRY=$oDRY;
}
function _add {
local NAME=$1;
local TYPE=$2;
local DATA=("$3");
local DOMAIN=${4:-$DOMAIN};
prompt "Add '$NAME $TYPE ${DATA[*]}' to zone '$DOMAIN' ($DNS_PROVIDER)?";
case $API_TOOL in
(denominator) denominator_wrapper record -z "$DOMAIN" add -n "$NAME" -t "$TYPE" -d "${DATA[@]}";;
(lexicon)
case $TYPE in
(A|AAAA|CNAME|NS|TXT) lexicon_wrapper create "$DOMAIN" "$TYPE" --name "$NAME" --content "${DATA[@]}" --ttl "$DEFAULT_TTL";;
(MX)
local MXPRIO MXHOST;
MXPRIO=$(cut -f 1 -d ' ' <<<"${DATA[@]}");
MXHOST=$(cut -f 2 -d ' ' <<<"${DATA[@]}");
lexicon_wrapper create "$DOMAIN" "$TYPE" --name "$NAME" --priority "$MXPRIO" --content "$MXHOST" --ttl "$DEFAULT_TTL";;
(SRV)
# '$DATA' must be formatted like '10 100 443 example.com' (priority weight port target).
local SRVPRIO SRVDATA;
SRVPRIO=$(cut -f 1 -d ' ' <<<"${DATA[@]}");
SRVDATA=$(cut -f 2- -d ' ' <<<"${DATA[@]}");
lexicon_wrapper create "$DOMAIN" "$TYPE" --name "$NAME" --priority "$SRVPRIO" --content "$SRVDATA" --ttl "$DEFAULT_TTL";;
(*) err "Unknown record type: $TYPE";
esac
;;
esac
}
function _replace {
local NAME=$1;
local TYPE=$2;
local DATA=("$3");
local DOMAIN=${4:-$DOMAIN};
prompt "Add '$NAME $TYPE ${DATA[*]}' to zone '$DOMAIN' ($DNS_PROVIDER)?";
case $API_TOOL in
(denominator) denominator_wrapper record -z "$DOMAIN" replace -n "$NAME" -t "$TYPE" -d "${DATA[@]}";;
(lexicon)
case $TYPE in
(A|AAAA|CNAME|NS|TXT) lexicon_wrapper update "$DOMAIN" "$TYPE" --name "$NAME" --content "${DATA[@]}" --ttl "$DEFAULT_TTL";;
(MX)
local MXPRIO MXHOST;
MXPRIO=$(cut -f 1 -d ' ' <<<"${DATA[@]}");
MXHOST=$(cut -f 2 -d ' ' <<<"${DATA[@]}");
lexicon_wrapper update "$DOMAIN" "$TYPE" --name "$NAME" --priority "$MXPRIO" --content "$MXHOST" --ttl "$DEFAULT_TTL";;
(SRV)
# '$DATA' must be formatted like '10 100 443 example.com' (priority weight port target).
local SRVPRIO SRVDATA;
SRVPRIO=$(cut -f 1 -d ' ' <<<"${DATA[@]}");
SRVDATA=$(cut -f 2- -d ' ' <<<"${DATA[@]}");
lexicon_wrapper update "$DOMAIN" "$TYPE" --name "$NAME" --priority "$SRVPRIO" --content "$SRVDATA" --ttl "$DEFAULT_TTL";;
(*) err "Unknown record type: $TYPE";
esac
;;
esac
}
function _remove {
local NAME=$1;
local TYPE=$2;
local DATA=("$3");
prompt "Remove '$NAME $TYPE ${DATA[*]}' from zone '$DOMAIN' ($DNS_PROVIDER)?";
case $API_TOOL in
(denominator) denominator_wrapper record -z "$DOMAIN" remove -n "$NAME" -t "$TYPE" -d "${DATA[@]}";;
(lexicon)
case $TYPE in
(A|AAAA|CNAME|NS|TXT|MX|SRV) lexicon_wrapper delete "$DOMAIN" "$TYPE" --name "$NAME" --content "${DATA[@]}";;
(*) err "Unknown record type: $TYPE";
esac
;;
esac
}
function _mirror {
local SOURCEDOMAIN=$1;
local TYPE=$2;
prompt "Copy $TYPE records from '$SOURCEDOMAIN' into zone '$DOMAIN' ($DNS_PROVIDER)?";
if [[ "$API_TOOL" == denominator ]]; then
err "_mirror function does not work with denominator without updating order of columns returned by _list";
fi
local ORIGINALRECORDS;
ORIGINALRECORDS=$(_list 2>/dev/null | grep -E -v '^Listing zone' || true); # … || `true` to prevent pipefail.
if [[ -z "$ORIGINALRECORDS" ]]; then
err "The target domain $DOMAIN does not yet exist in DNS. Please add it \`$SCRIPTNAME $DOMAIN create\`";
fi
local SOURCERECORDS;
SOURCERECORDS=$(_list "$SOURCEDOMAIN" 2>/dev/null | grep -E -v '^Listing zone' || true); # … || `true` to prevent pipefail.
if [[ -z "$SOURCERECORDS" ]]; then
err "The source domain $SOURCEDOMAIN does not yet exist in DNS. Please add it \`$SCRIPTNAME -d 111.222.333.444 $SOURCEDOMAIN create\`";
fi
# _list returns records like this:
# asdf2.dev 3600 A 1.2.3.6
# sub.asdf2.dev 3600 CNAME fig.strangecode.com
# asdf2.dev 3600 MX 10 mx.strangecode.com
# asdf2.dev 300 NS dns1.stabletransit.com
# asdf2.dev 300 NS dns2.stabletransit.com
if [[ "$TYPE" == "ALL" ]]; then
# When cloning all records, exclude name servers.
ORIGINALRECORDS=$(grep -Ev " [0-9]+ +NS " <<<"$ORIGINALRECORDS" || true);
SOURCERECORDS=$(grep -Ev " [0-9]+ +NS " <<<"$SOURCERECORDS" || true);
else
# Filter records by $TYPE if not doing all.
ORIGINALRECORDS=$(grep -E " [0-9]+ +$TYPE " <<<"$ORIGINALRECORDS" || true);
SOURCERECORDS=$(grep -E " [0-9]+ +$TYPE " <<<"$SOURCERECORDS" || true);
fi
# Replace source domain with the target domain in all source records.
SOURCERECORDS=$(sed "s/$SOURCEDOMAIN/$DOMAIN/g" <<<"$SOURCERECORDS");
if [[ -z "$SOURCERECORDS" ]]; then
err "$TYPE records not found in source '$SOURCEDOMAIN'. Try \`$SCRIPTNAME $SOURCEDOMAIN list\` to see what there is.";
fi
echo "===========================================================";
echo "The original records from target zone '$DOMAIN':";
echo "$ORIGINALRECORDS";
echo "===========================================================";
echo "…will be replaced by these records from the source zone '$SOURCEDOMAIN':";
echo "$SOURCERECORDS";
echo "===========================================================";
if [[ $- == *i* ]]; then
ALWAYSYES="";
prompt "Proceed? (It will be -y from here forward!)";
fi
ALWAYSYES=1;
# Remove original records.
while read -r OREC; do
if [[ -n $OREC ]]; then
local NAME TYPE TTL DATASTR;
NAME=$(awk '{print $1}' <<<"$OREC");
TYPE=$(awk '{print $3}' <<<"$OREC");
# TTL=$(awk {'print $3'} <<<"$OREC");
DATASTR=$(awk '{$1=$2=$3=""; gsub(/^ +| +$/, "", $0); print $0}' <<<"$OREC");
_validate "$NAME" "$TYPE" "$DATASTR";
_remove "$NAME" "$TYPE" "$DATASTR";
fi
done <<<"$ORIGINALRECORDS";
# Add new records from source.
while read -r SREC; do
if [[ -n $SREC ]]; then
local NAME TYPE TTL DATASTR;
NAME=$(awk '{print $1}' <<<"$SREC");
TYPE=$(awk '{print $3}' <<<"$SREC");
# TTL=$(awk {'print $3'} <<<"$SREC");
DATASTR=$(awk '{$1=$2=$3=""; gsub(/^ +| +$/, "", $0); print $0}' <<<"$SREC");
_validate "$NAME" "$TYPE" "$DATASTR";
_add "$NAME" "$TYPE" "$DATASTR";
fi
done <<<"$SOURCERECORDS";
echo "Please manually check extraneous NS records have been removed.";
}
#
# Main
#
# Display help if run without arguments.
if [[ $# == 0 ]]; then
shortusage;
exit 0;
fi
# Process command line options.
DRY='';
ADD_DEFAULTS='';
REDUCETOAPEX='';
VERBOSE='';
ALWAYSYES='';
ORIGIN_IPADDR='';
while getopts "d:ho:p:rvy-:" OPT; do
case $OPT in
(d) ADD_DEFAULTS=1; DEFAULT_IPADDR=$OPTARG;;
(h) usage; exit 1;;
(o) ORIGIN_IPADDR=$OPTARG;;
(p) DNS_PROVIDER=$OPTARG;
case $DNS_PROVIDER in
(cloudflare) DEFAULT_TTL=1;;
esac
;;
(r) REDUCETOAPEX=1;;
(v) VERBOSE=1;;
(y) ALWAYSYES=1;;
(-) case $OPTARG in
(dry) DRY=1; echo "Only doing dry run…";;
(help) usage; exit;;
(*) err "An invalid double-hyphen option was encountered.";;
esac ;;
(?) err "Invalid option: '$OPT'. Run with -h for help.";;
(*) err "An unexpected error occurred.";;
esac
done
# Remove options.
shift $((OPTIND - 1));
DOMAIN=${1:-''};
shift;
[[ "$DOMAIN" =~ $REGEX_DOMAIN ]] || err "Domain '$DOMAIN' looks wrong. Ask for --help?";
VERB=${1:-''};
shift || true; # Avoid stopping execution if shift returns false.
case $VERB in
(create)
[[ $# -gt 0 ]] && _invalid_args "(no args)" "$*";
_create;
;;
(destroy)
[[ $# -gt 0 ]] && _invalid_args "(no args)" "$*";
_destroy;
;;
(list)
[[ $# -gt 0 ]] && _invalid_args "(no args)" "$*";
_list;
;;
(exists)
[[ $# -gt 0 ]] && _invalid_args "(no args)" "$*";
_list &>/dev/null;
exit $?;
;;
(get)
case $# in
(1)
NAME=$DOMAIN;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$1");
;;
(2)
NAME=$1;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$2");
;;
(*) _invalid_args "[NAME] TYPE" "$*";;
esac
# _validate "$NAME" "$TYPE"
_get "$NAME" "$TYPE";
;;
(add)
case $# in
(2)
NAME=$DOMAIN;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$1");
DATA=("$2");
;;
(3)
NAME=$1;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$2");
DATA=("$3");
;;
(*) _invalid_args "[NAME] TYPE DATA" "$*";;
esac
_validate "$NAME" "$TYPE" "${DATA[@]}";
_add "$NAME" "$TYPE" "${DATA[@]}";
;;
(replace)
case $# in
(2)
NAME=$DOMAIN;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$1");
DATA=("$2");
;;
(3)
NAME=$1;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$2");
DATA=("$3");
;;
(*) _invalid_args "[NAME] TYPE DATA" "$*";;
esac
_validate "$NAME" "$TYPE" "${DATA[@]}";
_replace "$NAME" "$TYPE" "${DATA[@]}";
;;
(rm|remove)
case $# in
(2)
NAME=$DOMAIN;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$1");
DATA=("$2");
;;
(3)
NAME=$1;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$2");
DATA=("$3");
;;
(*) _invalid_args "[NAME] TYPE DATA" "$*";;
esac
_validate "$NAME" "$TYPE" "${DATA[@]}";
_remove "$NAME" "$TYPE" "${DATA[@]}";
;;
(mirror)
case $# in
(2)
SOURCEDOMAIN=$1;
TYPE=$(tr '[:lower:]' '[:upper:]' <<<"$2");
;;
(*) _invalid_args "SOURCEDOMAIN TYPE|all" "$*";;
esac
# _validate "$NAME" "$TYPE";
if [[ -n $REDUCETOAPEX ]]; then
_mirror "$(get_zone_apex "$SOURCEDOMAIN")" "$TYPE";
else
_mirror "$SOURCEDOMAIN" "$TYPE";
fi
;;
(help)
usage; exit 1;;
(*)
err "Verb '$VERB' not understood (create|destroy|list|get|add|remove|replace|mirror). Ask for --help?";;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment