8 Commits

Author SHA1 Message Date
asif 1732de343d refactor(func): Remove unused functions
Remvoe the logging functions replaced by the new structured implementation.
2026-07-23 23:22:17 -06:00
asif 51b2b455a2 feat(log): Use plain-text logging for all messages
Convert all logging output to plain-text structured messages using new function.
2026-07-23 23:16:12 -06:00
asif f8f3bf1539 refactor(func): Change default err code to 0 2026-07-23 22:17:15 -06:00
asif f8908d7830 feat(func): Write modified syslog fmt msg
Take input parameters and generate a modified syslog-style log entry.
2026-07-23 21:59:58 -06:00
asif 8a5a72e15a feat(func): Convert to uniform log levels
Take input text with various references to log levels and return a standardized uppercase log level name.
2026-07-23 21:57:30 -06:00
asif 1da49b06db feat(func): Convert text ucase, lcase 2026-07-23 21:56:19 -06:00
asif 1336c8e63b feat(func): Retrieve machine host name 2026-07-23 21:55:56 -06:00
asif 450733a8d5 feat(func): Generate ISO-8601 timestamp 2026-07-23 21:55:25 -06:00
+137 -172
View File
@@ -31,7 +31,6 @@ else
width=80
fi
### functions
badParam() {
if [ "$1" = "null" ]; then
printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2"
@@ -47,65 +46,80 @@ badParam() {
fi
}
exitError() {
case "$1" in
3)
errMsg="Unable to connect to Cloudflare servers. This is probably a temporary networking issue. Please try again later."
;;
10)
errMsg="Unable to auto-detect IP address. Try again later or supply the IP address to be used."
;;
20)
errMsg="Cloudflare authorized email address (cfEmail) is either null or undefined. Please check your Cloudflare credentials file."
;;
21)
errMsg="Cloudflare authorized API key (cfKey) is either null or undefined. Please check your Cloudflare credentials file."
;;
22)
errMsg="Cloudflare zone id (cfZoneId) is either null or undefined. Please check your Cloudflare credentials file."
;;
25)
errMsg="Cloudflare API error. Please review any 'CF-ERR:' lines in this log for details."
;;
26)
errMsg="${failedHostCount} host update(s) failed. Any 'CF-ERR:' lines noted in this log may help determine what went wrong."
;;
*)
writeLog error "An unspecified error occurred. (code: 99)"
printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile"
exit 99
;;
esac
writeLog error "$errMsg" "$1"
printf "%s[%s] -- Cloudflare DDNS update-script: completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile"
exit "$1"
getHostname() {
(hostname -s)
}
exitOK() {
printf "%s[%s] -- Cloudflare DDNS update-script: completed successfully --%s\n" "$ok" "$(stamp)" "$norm" >>"$logFile"
exit 0
getTimeStamp() {
(date -u +"%Y-%m-%dT%H:%M:%SZ")
}
lowercase() {
echo "$1" | tr '[:upper:]' '[:lower:]'
}
uppercase() {
echo "$1" | tr '[:lower:]' '[:upper:]'
}
# 1: cloudflare error object, 2: operation identifier
listCFErrors() {
# extract error codes and messages in separate variables, replace newlines with underscores
codes="$(printf "%s" "$1" | jq -r '.errors | .[] | .code' | tr '\n' '_')"
messages="$(printf "%s" "$1" | jq -r '.errors | .[] | .message' | tr '\n' '_')"
# iterate codes and messages and assemble into coherent messages in log
# iterate codes and messages, assemble into a coherent log message
while [ -n "$codes" ] && [ -n "$messages" ]; do
# get first code and message in respective sets
# get the first code and message in respective sets
code="${codes%%_*}"
message="${messages%%_*}"
# update codes and messages sets by removing first item in each set
# update sets of codes and messages by removing the first item (above) in each set
codes="${codes#*_}"
messages="${messages#*_}"
# output to log
writeLog cf "$message" "$code"
writePlainTextLog "${message}" "err" "fail" "$2" "$code"
done
}
standardizeLogLevels() {
LEVEL_TO_STANDARDIZE="$(lowercase "$1")"
case "$LEVEL_TO_STANDARDIZE" in
"critical" | "crit" | "fatal")
echo "CRIT"
;;
"error" | "err")
echo "ERR"
;;
"warning" | "warn")
echo "WARN"
;;
"information" | "info")
echo "INFO"
;;
"debug" | "verbose")
echo "DEBUG"
;;
*)
echo "INFO"
;;
esac
}
# modified syslog fmt: <ISO-8601 timestamp> <hostname> <tag>[pid]: [LEVEL:$2] [STATUS:$3] <MESSAGE:$1> (<OPERATION:$4>:<CODE:$5>)
writePlainTextLog() {
# not enough information to generate a meaningful log message
if [ -z "$1" ] || [ $# -lt 3 ]; then
return
fi
printf "%s %s %s[%s]: [%s] [%s] %s (%s:%s)\n" \
"$(getTimeStamp)" "$(getHostname)" "$LOG_PROGRAM_NAME" "$$" \
"$(standardizeLogLevels "$2")" "$(uppercase "$3")" "$1" "${4:-UNSPECIFIED}" "${5:-0}" \
>>"$logFile"
}
scriptExamples() {
newline
printf "Update Cloudflare DNS host A/AAAA records with current IP address.\n"
@@ -192,10 +206,6 @@ scriptHelp() {
exit 0
}
stamp() {
(date +%F" "%T)
}
newline() {
printf "\n"
}
@@ -212,47 +222,6 @@ textBlockSwitches() {
printf "%s%s%s\n" "$cyan" "$1" "$norm"
}
writeLog() {
case "$1" in
cf)
printf "[%s] CF-ERR: %s (code: %s)\n" "$(stamp)" "$2" "$3" >>"$logFile"
;;
err)
printf "%s[%s] ERR: %s%s\n" "$err" "$(stamp)" "$2" "$norm" >>"$logFile"
;;
error)
printf "%s[%s] ERROR: %s (code: %s)%s\n" "$err" "$(stamp)" "$2" "$3" "$norm" >>"$logFile"
;;
process)
printf "%s[%s] %s... %s" "$cyan" "$(stamp)" "$2" "$norm" >>"$logFile"
;;
process-done)
printf "%s%s%s\n" "$cyan" "$2" "$norm" >>"$logFile"
;;
process-error)
printf "%sERROR%s\n" "$err" "$norm" >>"$logFile"
;;
process-warning)
printf "%s%s%s\n" "$warn" "$2" "$norm" >>"$logFile"
;;
stamped)
printf "[%s] %s\n" "$(stamp)" "$2" >>"$logFile"
;;
success)
printf "%s[%s] SUCCESS: %s%s\n" "$ok" "$(stamp)" "$2" "$norm" >>"$logFile"
;;
warn)
printf "%s[%s] WARN: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile"
;;
warning)
printf "%s[%s] WARNING: %s%s\n" "$warn" "$(stamp)" "$2" "$norm" >>"$logFile"
;;
*)
printf "%s\n" "$2" >>"$logFile"
;;
esac
}
### default variable values
scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)"
scriptName="$(basename "$0")"
@@ -362,18 +331,24 @@ done
### pre-flight checks
if ! command -v curl >/dev/null; then
printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm"
writePlainTextLog "'curl' must be installed and accessible" "fatal" "fail" "preexec" 2
exit 2
fi
if ! command -v jq >/dev/null; then
printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm"
writePlainTextLog "'jq' must be installed and accessible" "fatal" "fail" "preexec" 2
exit 2
fi
[ -z "$dnsRecords" ] && badParam errMsg "You must specify at least one DNS record to update. Exiting."
# verify credentials file exists and is not empty (default check)
if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then
badParam errMsg "Cannot find Cloudflare credentials file (${accountFile}). Exiting."
if [ -z "$dnsRecords" ]; then
badParam errMsg "You must specify at least one DNS record to update. Exiting."
writePlainTextLog "At least one DNS record to update must be specified" "fatal" "fail" "params" 1
fi
if ! command -v logger >/dev/null 2>&1; then
# verify the credentials file exists and is not empty (default check)
if [ ! -f "$accountFile" ] || [ ! -s "$accountFile" ]; then
badParam errMsg "Cannot find the Cloudflare credentials file (${accountFile}). Exiting."
writePlainTextLog "Cannot find the specified Cloudflare credentials file (${accountFile})" "fatal" "fail" "params" 1
fi
if [ "$logToJournal" -eq 1 ] && ! command -v logger >/dev/null 2>&1; then
printf "\n%sThis script requires 'logger' be installed to write entries to your journaling system. Exiting.%s\n\n" "$err" "$norm"
exit 2
fi
@@ -390,56 +365,51 @@ if [ "$colourizeLogFile" -eq 0 ]; then
fi
### initial log entries
{
writePlainTextLog "starting '${scriptName}'" "info" "ok" "script"
writePlainTextLog "script path: ${scriptPath}/${scriptName}" "debug" "ok" "startup"
writePlainTextLog "credentials file: ${accountFile}" "debug" "ok" "startup"
printf "%s[%s] -- Cloudflare DDNS update-script: starting --%s\n" "$ok" "$(stamp)" "$norm"
printf "Parameters:\n"
printf "script path: %s\n" "$scriptPath/$scriptName"
printf "credentials file: %s\n" "$accountFile"
if [ "$ip4" -eq 1 ]; then
writePlainTextLog "mode: IPv4" "debug" "ok" "startup"
elif [ "$ip6" -eq 1 ]; then
writePlainTextLog "mode: IPv6" "debug" "ok" "startup"
fi
# detect and report IP address
if [ -z "$ipAddress" ]; then
# detect public ip address
if [ "$ip4" -eq 1 ]; then
printf "mode: IP4\n"
elif [ "$ip6" -eq 1 ]; then
printf "mode: IP6\n"
if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then
writePlainTextLog \
"Unable to auto-detect this machine's public IP address; try again later or supply the IP address to be used" "err" "fail" "detectip" 10
exit 10
fi
fi
# detect and report IP address
if [ -z "$ipAddress" ]; then
# detect public ip address
if [ "$ip4" -eq 1 ]; then
if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then
printf "ddns ip address:%s ERROR%s\n" "$err" "$norm"
exitError 10
fi
if [ "$ip6" -eq 1 ]; then
if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then
writePlainTextLog \
"Unable to auto-detect this machine's public IP address; try again later or supply the IP address to be used" "err" "fail" "detectip" 10
exit 10
fi
if [ "$ip6" -eq 1 ]; then
if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then
printf "ddns ip address:%s ERROR%s\n" "$err" "$norm"
exitError 10
fi
fi
printf "ddns ip address (detected): %s\n" "$ipAddress"
else
printf "ddns ip address (supplied): %s\n" "$ipAddress"
fi
writePlainTextLog "DDNS IP address (detected): $ipAddress" "info" "ok" "startup"
else
writePlainTextLog "DDNS IP address (supplied): $ipAddress" "info" "ok" "startup"
fi
# iterate DNS records to update
dnsRecordsToUpdate="$(printf '%s' "${dnsRecords}" | sed "s/${dnsSeparator}*$//")$dnsSeparator"
while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do
record="${dnsRecordsToUpdate%%${dnsSeparator}*}"
dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}"
# iterate DNS records to update
dnsRecordsToUpdate="$(printf '%s' "${dnsRecords}" | sed "s/${dnsSeparator}*$//")$dnsSeparator"
while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator" ]; do
record="${dnsRecordsToUpdate%%${dnsSeparator}*}"
dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}"
if [ -z "$record" ]; then
continue
fi
printf "updating record: %s\n" "$record"
done
printf "(end of parameter list)\n"
} >>"$logFile"
if [ -z "$record" ]; then
continue
fi
writePlainTextLog "DNS host record '${record}' queued for update" "info" "ok" "startup"
done
### read Cloudflare credentials
writeLog process "Reading Cloudflare credentials"
case "$accountFile" in
/*)
# absolute path, use as-is
@@ -453,14 +423,14 @@ case "$accountFile" in
;;
esac
if [ -z "$cfKey" ]; then
writeLog process-error
exitError 21
writePlainTextLog "Cloudflare authorized API key (cfKey) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 21
exit 21
fi
if [ -z "$cfZoneId" ]; then
writeLog process-error
exitError 22
writePlainTextLog "Cloudflare zone id (cfZoneId) is either null or undefined; please check your Cloudflare credentials file" "err" "fail" "creds" 22
exit 22
fi
writeLog process-done "DONE"
writePlainTextLog "Cloudflare credentials file read successfully" "debug" "ok" "creds"
### connect to Cloudflare and do what needs to be done!
dnsRecordsToUpdate="$dnsRecords$dnsSeparator"
@@ -478,7 +448,6 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator"
if [ -z "$record" ]; then
continue
fi
writeLog process "Processing ${record}"
# exit if curl/network error
if ! cfLookup="$(
@@ -486,43 +455,39 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator"
-H "Authorization: Bearer ${cfKey}" \
-H "Content-Type: application/json"
)"; then
writeLog process-error
exitError 3
writePlainTextLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "cflogin" 3
exit 3
fi
# exit if API error
# exit here since API errors on GET request probably indicates authentication error which would affect all remaining operations
# no reason to continue processing other hosts and pile-up errors which might look like a DoS attempt
# an API error on this GET request likely indicates an authentication error that would affect all remaining operations
# no reason to continue processing other dns records and pile-up errors which might look like a DoS attempt
cfSuccess="$(printf "%s" "$cfLookup" | jq -r '.success')"
if [ "$cfSuccess" = "false" ]; then
writeLog process-error
listCFErrors "$cfLookup"
exitError 25
listCFErrors "$cfLookup" "cflogin"
writePlainTextLog "Cloudflare API error; review any previously logged 'CF-ERR:' lines for details." "err" "fail" "cflogin" 25
exit 25
fi
resultCount="$(printf "%s" "$cfLookup" | jq '.result_info.count')"
# skip to next host if cannot find existing host record (this script *updates* only, does not create!)
# skip to the next host if an existing host record cannot be found (this script *updates* only, does not create!)
if [ "$resultCount" = "0" ]; then
# warn if record of host not found
writeLog process-warning "NOT FOUND"
writeLog warn "Cannot find existing record to update for DNS entry: ${record}"
writePlainTextLog "Cannot find an existing record matching '${record}' to update" "warn" "warn" "ddns"
invalidDomainCount=$((invalidDomainCount + 1))
continue
fi
objectId=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .id')
currentIpAddr=$(printf "%s" "$cfLookup" | jq -r '.result | .[] | .content')
writeLog process-done "FOUND: IP = ${currentIpAddr}"
writePlainTextLog "The current IP address for '${record}' is ${currentIpAddr}" "debug" "ok" "ddns"
# skip to next hostname if record already up-to-date
# skip to next hostname if record already up to date
if [ "$currentIpAddr" = "$ipAddress" ]; then
writeLog stamped "IP address for ${record} is already up-to-date"
writePlainTextLog "The IP address for '${record}' is already up to date" "info" "ok" "ddns"
continue
fi
# update record
writeLog process "Updating IP address for ${record}"
updateJSON="$(jq -n --arg key0 content --arg value0 "${ipAddress}" '{($key0):$value0}')"
# exit if curl/network error
@@ -532,19 +497,17 @@ while [ -n "$dnsRecordsToUpdate" ] && [ "$dnsRecordsToUpdate" != "$dnsSeparator"
-H "Content-Type: application/json" \
--data "${updateJSON}"
)"; then
writeLog process-error
exitError 3
writePlainTextLog "Unable to connect to Cloudflare servers; please try again later." "err" "fail" "ddns" 3
exit 3
fi
# note update success or failure
cfSuccess="$(printf "%s" "$cfResult" | jq '.success')"
if [ "$cfSuccess" = "true" ]; then
writeLog process-done "DONE"
writeLog success "IP address for ${record} updated."
writePlainTextLog "The IP address for '${record}' successfully updated" "info" "ok" "ddns"
else
writeLog process-error
listCFErrors "$cfResult"
writeLog err "Unable to update IP address for ${record}"
listCFErrors "$cfResult" "ddns"
writePlainTextLog "Unable to update the IP address for '${record}'" "err" "fail" "ddns"
# do not exit with error, API error here is probably an update issue specific to this host
# increment counter and note it after all processing finished
failedHostCount=$((failedHostCount + 1))
@@ -553,22 +516,24 @@ done
# exit
if [ "$invalidDomainCount" -ne 0 ]; then
writeLog warning "${invalidDomainCount} invalid host(s) supplied for updating."
writePlainTextLog "${invalidDomainCount} invalid host(s) were supplied for updating" "warn" "warn" "ddns"
fi
if [ "$failedHostCount" -ne 0 ]; then
exitError 26
writePlainTextLog \
"${failedHostCount} host update(s) failed; review 'CF-ERR:' lines in this log to help determine what may have gone wrong" "err" "fail" "ddns" 26
exit 26
else
exitOK
writePlainTextLog "${scriptName} completed successfully" "info" "ok" "script"
fi
### exit return codes
# 0: normal exit, no errors
# 1: invalid or unknown parameter
# 2: cannot find or access required external program(s)
# 3: curl error (probably connection)
# 10: cannot auto-detect IP address
# 21: accountFile has a null or missing cfKey variable
# 22: accountFile has a null or missing cfZoneId variable
# 25: Cloudflare API error
# 26: one or more updates failed
# 99: unspecified error occurred
# 0: normal exit, no errors
# 1: invalid or unknown parameter
# 2: cannot find or access required external program(s)
# 3: curl error (probably connection)
# 10: cannot auto-detect IP address
# 21: accountFile has a null or missing cfKey variable
# 22: accountFile has a null or missing cfZoneId variable
# 25: Cloudflare API error
# 26: one or more updates failed
# 99: unspecified error occurred