|
| 1 | +#!/usr/bin/env bash |
| 2 | +# dns_gcloud |
| 3 | +# Add/Del/List TXT record using the Google Cloud DNS gcloud command |
| 4 | +# ver 2025-09-23 # shellcheck has read, even I'll say eveything was so nice |
| 5 | +# org. version: |
| 6 | +# https://github.com/kshji/gitssl_gcloud |
| 7 | +# |
| 8 | +# Main reason to make was to support getssl using DNS validation with Google Cloud DNS |
| 9 | +# You can use this script to any host setting TXT records, default is _acme-challenge |
| 10 | +# |
| 11 | +# dns_gloud -c command domain token |
| 12 | +# |
| 13 | +# get help: |
| 14 | +# dns_gloud -? | --help |
| 15 | +# |
| 16 | +# dns_gloud -c add example.com "testN" |
| 17 | +# dns_gloud -c list example.com |
| 18 | +# dns_gloud -c del example.com "testN" |
| 19 | +# dns_gloud -c add example.com "test1" "test2" |
| 20 | +# dns_gloud -c list example.com |
| 21 | +# dns_gloud -c del example.com "test1" "test2" |
| 22 | +# |
| 23 | +# options: |
| 24 | +# -d 0|1 # debug on/off to the file /var/tmp/getssl/...log |
| 25 | +# -s 10 # sleeptime after gcloud add/del process, default 10 |
| 26 | +# -h hostname # default is "_acme-challenge." |
| 27 | +# # if like to use domain without host, set host empty string !!! |
| 28 | +# -t ttlvalue # set ttl, default 60 = 1 min |
| 29 | +# |
| 30 | +# |
| 31 | + |
| 32 | +PRG="$0" |
| 33 | +BINDIR="${PRG%/*}" |
| 34 | +[ "$PRG" = "$BINDIR" ] && BINDIR="." # - same dir as program |
| 35 | +PRG="${PRG##*/}" |
| 36 | + |
| 37 | +####################################################################################### |
| 38 | +usage() |
| 39 | +{ |
| 40 | +cat <<EOT |
| 41 | +usage:$PRG -c COMMAND [ -d 0|1 ] [ -t ttlvalue ] [ -s sleep_sec_after_gcloud ] [ -h hostname ] DOMAIN TOKEN |
| 42 | +or |
| 43 | +$PRG --command COMMAND [ --debug 0|1 ] [ --ttl ttlvalue ] [ --sleep sleep_sec_after_gcloud ] [ --host hostname ] DOMAIN TOKEN |
| 44 | +
|
| 45 | + -d 1 # print some debug data and make debug file to the dir /var/tmp/getssl/ |
| 46 | + -t NNN # set TTL value, default 60. Have to be same in the ADD and DEL |
| 47 | + -s NN # default 10 s, sleep seconds after gcloud process |
| 48 | + -h hostname # hostname, default is _acme-challenge, empty string = use domain |
| 49 | + |
| 50 | +EOT |
| 51 | + |
| 52 | +} |
| 53 | + |
| 54 | +####################################################################################### |
| 55 | +err() |
| 56 | +{ |
| 57 | + echo "$PRG err:$*" |
| 58 | +} |
| 59 | + |
| 60 | +####################################################################################### |
| 61 | +dbgstr() |
| 62 | +{ |
| 63 | + ((DEBUG<1)) && return |
| 64 | + echo "$PRG debug:$*" |
| 65 | +} |
| 66 | + |
| 67 | +####################################################################################### |
| 68 | +dbg() |
| 69 | +{ |
| 70 | + |
| 71 | + |
| 72 | + Xdomain="$1" # domain + command |
| 73 | + Xcmd="$2" |
| 74 | + [ "$Xdomain" = "" ] && Xdomain="default" |
| 75 | + [ "$Xcmd" = "" ] && Xcmd="cmd" |
| 76 | + Xdomain="${Xdomain%.}" # del last dot |
| 77 | + tmpd="/var/tmp/getssl" |
| 78 | + tmpf="$tmpd/$PRG.$Xdomain.$Xcmd.log" |
| 79 | + mkdir -p "$tmpd" 2>/dev/null |
| 80 | + chmod 1777 "$tmpd" 2>/dev/null |
| 81 | + |
| 82 | + |
| 83 | + cnt=0 |
| 84 | + # save only last execute info |
| 85 | + { |
| 86 | + date |
| 87 | + echo "GCLOUD_PROJECTID:$GCLOUD_PROJECTID" |
| 88 | + echo "GCLOUD_ZONE:$GCLOUD_ZONE" |
| 89 | + echo "GCLOUD_ACCOUNT:$GCLOUD_ACCOUNT" |
| 90 | + echo "GCLOUD_KEYFILE:$GCLOUD_KEYFILE" |
| 91 | + env |
| 92 | + } > "$tmpf" |
| 93 | + for var in $all |
| 94 | + do |
| 95 | + ((cnt++)) |
| 96 | + echo "$cnt:<$var>" >> "$tmpf" |
| 97 | + done |
| 98 | + |
| 99 | +} |
| 100 | + |
| 101 | +####################################################################################### |
| 102 | +check_end_dot() |
| 103 | +{ |
| 104 | + # gcloud need fulldomain ending dot = absolut domain path |
| 105 | + # set the last dot if missing |
| 106 | + Xorg="$1" |
| 107 | + Xnodot="${Xorg%.}" # remove last dot if there is |
| 108 | + echo "$Xnodot." # add dot allways |
| 109 | +} |
| 110 | + |
| 111 | +####################################################################################### |
| 112 | +list_txt() |
| 113 | +{ |
| 114 | + |
| 115 | + Xname="$1" |
| 116 | + list=$(gcloud dns record-sets list --zone="$GCLOUD_ZONE" --name="$Xname" --type="TXT" ) |
| 117 | + stat=$? |
| 118 | + (( stat > 0 )) && err "gcloud error to list TXT record" && exit 2 |
| 119 | + variables=variables |
| 120 | + |
| 121 | + # Some shell checkers (ex.shellcheck) like to do next different way. I'll say both works |
| 122 | + # you can read 1st line to var and then use {$var?} on reading |
| 123 | + # in this case you'll get same result. This read 1st loop varianle variables, look value of variables |
| 124 | + # on the second loop it read variables, which was on the 1st line ... command line process is so nice |
| 125 | + # this do exactly what we need to do ... (not that what https://www.shellcheck.net/wiki/SC2229 explain) |
| 126 | + oifs="$IFS" |
| 127 | + cnt=0 |
| 128 | + echo "$list" | while read $variables |
| 129 | + do |
| 130 | + (( cnt++ )) |
| 131 | + # 1st line is header, read next line |
| 132 | + (( cnt == 1 )) && continue |
| 133 | + echo "name:$NAME type:$TYPE ttl:$TTL data:$DATA" |
| 134 | + # next line works just what we need, shellcheck not like this ... |
| 135 | + # Wiki's last part: it's okay https://www.shellcheck.net/wiki/SC2206 |
| 136 | + IFS="," values=($DATA) |
| 137 | + IFS="$oifs" |
| 138 | + numOfvalues=${#values[@]} |
| 139 | + for (( var=0; var<numOfvalues ; var++ )) |
| 140 | + do |
| 141 | + echo " - data($var):${values[$var]}" |
| 142 | + done |
| 143 | + done |
| 144 | + sleep 1 |
| 145 | + exit 0 |
| 146 | +} |
| 147 | + |
| 148 | +####################################################################################### |
| 149 | +del_txt() |
| 150 | +{ |
| 151 | + |
| 152 | + Xname="$1" |
| 153 | + shift |
| 154 | + # |
| 155 | + Xtoken="" |
| 156 | + while [ $# -gt 0 ] |
| 157 | + do |
| 158 | + Xtoken="$Xtoken \"$1\"" |
| 159 | + shift |
| 160 | + done |
| 161 | + dbgstr "<$Xtoken>" |
| 162 | + |
| 163 | +#exit |
| 164 | + # start transaction |
| 165 | + gcloud dns record-sets transaction start --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 166 | + stat=$? |
| 167 | + (( stat > 0 )) && err "gcloud start transaction error" && exit 2 |
| 168 | + |
| 169 | + # del TXT |
| 170 | + dbgstr gcloud dns record-sets transaction remove --name="$Xname" --ttl="$ttl" --type="TXT" \ |
| 171 | + --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" "$Xtoken" |
| 172 | + gcloud dns record-sets transaction remove --name="$Xname" --ttl="$ttl" --type="TXT" \ |
| 173 | + --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" "$Xtoken" |
| 174 | + stat=$? |
| 175 | + if (( stat > 0 )) ; then |
| 176 | + err "gcloud remove error" |
| 177 | + gcloud dns record-sets transaction abort --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 178 | + exit 2 |
| 179 | + fi |
| 180 | + |
| 181 | + gcloud dns record-sets transaction execute --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 182 | + stat=$? |
| 183 | + (( stat > 0 )) && err "gcloud transaction execute error" && exit 2 |
| 184 | + |
| 185 | + # if not sleep, get error ??? |
| 186 | + sleep "$sleepafter" |
| 187 | + exit 0 |
| 188 | +} |
| 189 | + |
| 190 | +####################################################################################### |
| 191 | +add_txt() |
| 192 | +{ |
| 193 | + |
| 194 | + Xname="$1" |
| 195 | + shift |
| 196 | + # could be 1-n values |
| 197 | + Xtoken="" |
| 198 | + while [ $# -gt 0 ] |
| 199 | + do |
| 200 | + Xtoken="$Xtoken \"$1\"" |
| 201 | + shift |
| 202 | + done |
| 203 | + dbgstr "<$Xtoken>" |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | + # start transaction |
| 208 | + gcloud dns record-sets transaction start --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 209 | + stat=$? |
| 210 | + if (( stat > 0 )) ; then |
| 211 | + err "gcloud start transaction error" |
| 212 | + gcloud dns record-sets transaction abort --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 213 | + exit 2 |
| 214 | + fi |
| 215 | + |
| 216 | + # add TXT |
| 217 | + dbgstr gcloud dns record-sets transaction add --name="$Xname" --ttl="$ttl" --type="TXT" \ |
| 218 | + --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" "$Xtoken" |
| 219 | + gcloud dns record-sets transaction add --name="$Xname" --ttl="$ttl" --type="TXT" \ |
| 220 | + --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" "$Xtoken" |
| 221 | + stat=$? |
| 222 | + if (( stat > 0 )) ; then |
| 223 | + err "gcloud add error" |
| 224 | + gcloud dns record-sets transaction abort --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 225 | + exit 2 |
| 226 | + fi |
| 227 | + |
| 228 | + gcloud dns record-sets transaction execute --zone="$GCLOUD_ZONE" --project="$GCLOUD_PROJECTID" |
| 229 | + stat=$? |
| 230 | + #echo "execute stat:$stat" |
| 231 | + (( stat > 0 )) && err "gcloud transaction execute error" && exit 2 |
| 232 | + |
| 233 | + # if not sleep, get error ??? |
| 234 | + sleep "$sleepafter" |
| 235 | + exit 0 |
| 236 | +} |
| 237 | + |
| 238 | +####################################################################################### |
| 239 | +# MAIN |
| 240 | +####################################################################################### |
| 241 | + |
| 242 | +DEBUG=0 |
| 243 | +command="" |
| 244 | +sleepafter=10 |
| 245 | +# default host to manipulate |
| 246 | +host="_acme-challenge." |
| 247 | +ttl=60 |
| 248 | + |
| 249 | +while [ $# -gt 0 ] |
| 250 | +do |
| 251 | + arg="$1" |
| 252 | + case "$arg" in |
| 253 | + -c|--command|--cmd) command="$2"; shift ;; |
| 254 | + -d|--debug) DEBUG="$2" ; shift ;; |
| 255 | + -s|--sleep) sleepafter="$2"; shift ;; |
| 256 | + -h|--host) host="$2" |
| 257 | + [ $# -lt 2 ] && usage && exit 2 |
| 258 | + host="$2" |
| 259 | + shift |
| 260 | + ;; |
| 261 | + -t|--ttl) ttl="$2"; shift ;; |
| 262 | + -?|--help) usage; exit 2 ;; |
| 263 | + -*) # unknown option |
| 264 | + err "unknown option $arg" |
| 265 | + usage |
| 266 | + exit 2 |
| 267 | + ;; |
| 268 | + *) # arguments, stop the option parser |
| 269 | + break |
| 270 | + ;; |
| 271 | + esac |
| 272 | + shift |
| 273 | +done |
| 274 | + |
| 275 | +[ "$GCLOUD_PROJECTID" = "" ] && err "GCLOUD_PROJECTID is not set. Unable to set TXT records." && exit 2 |
| 276 | +[ "$GCLOUD_ZONE" = "" ] && err "GCLOUD_ZONE is not set. Unable to set TXT records." && exit 2 |
| 277 | +[ "$GCLOUD_ACCOUNT" = "" ] && err "GCLOUD_ACCOUNT is not set. Unable to set TXT records." && exit 2 |
| 278 | +[ "$GCLOUD_KEYFILE" = "" ] && err "GCLOUD_KEYFILE is not set. Unable to set TXT records." && exit 2 |
| 279 | +[ ! -f "$GCLOUD_KEYFILE" ] && err "file not usable:$GCLOUD_KEYFILE" && exit 2 |
| 280 | + |
| 281 | + |
| 282 | +all="$*" |
| 283 | +fulldomain="$1" |
| 284 | +shift |
| 285 | +token="$*" # could be 1-n tokens if del |
| 286 | + |
| 287 | +case "$command" in |
| 288 | + add) ;; |
| 289 | + del) ;; |
| 290 | + list) ;; |
| 291 | + *) command="" ;; |
| 292 | +esac |
| 293 | + |
| 294 | + |
| 295 | +[ "$command" = "" ] && err "need option -c add | -c del | -c list" && exit 2 |
| 296 | +[ "$fulldomain" = "" ] && err "need fulldomain argument." && exit 2 |
| 297 | +[ "$token" = "" ] && [ "$command" != "list" ] && err "need token argument." && exit 2 |
| 298 | + |
| 299 | +# dbg info to the program tmp dir |
| 300 | +(( DEBUG>0)) && dbg "$fulldomain" "$command" |
| 301 | + |
| 302 | +# host check ending of dot |
| 303 | +[ "$host" != "" ] && host=$(check_end_dot "$host") |
| 304 | +# host fullname |
| 305 | +gname=$(check_end_dot "$host$fulldomain") |
| 306 | +#echo "gname: $gname" |
| 307 | + |
| 308 | +# activate google cloud account |
| 309 | +gcloud auth activate-service-account "$GCLOUD_ACCOUNT" --key-file="$GCLOUD_KEYFILE" --project="$GCLOUD_PROJECTID" |
| 310 | +stat=$? |
| 311 | +(( stat > 0 )) && err "gcloud activate account error" && exit 2 |
| 312 | + |
| 313 | +case "$command" in |
| 314 | + add) add_txt "$gname" "$token" |
| 315 | + ;; |
| 316 | + del) del_txt "$gname" "$token" |
| 317 | + ;; |
| 318 | + list) list_txt "$gname" |
| 319 | + ;; |
| 320 | + *) |
| 321 | + err "unknown command" |
| 322 | + exit 2 |
| 323 | + ;; |
| 324 | +esac |
| 325 | + |
| 326 | + |
| 327 | + |
0 commit comments