-
-
Save jaytaylor/6273175 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash | |
## | |
# @author Jay Taylor [@jtaylor] | |
# @date 2013-08-15 | |
# | |
# @description CloudFlare management script. | |
# | |
# Path ENV VAR override. | |
if test -z "${CF_PATH:-}"; then | |
CF_PATH="${HOME}/.cloudflare" | |
fi | |
# Ensure base path exists. | |
set -e | |
mkdir -p "${CF_PATH}" || ( echo "error: failed to create CF_PATH=${CF_PATH}" 1>&2 ; exit 1 ) | |
set +e | |
## | |
# Begin Configuration | |
# | |
# NB: Hard-code these configuration values or place them in the corresponding file. | |
tkn=$(test -r "${CF_PATH}/token" && cat "${CF_PATH}/token" || echo '<YOUR_API_TOKEN_HERE>') | |
email=$(test -r "${CF_PATH}/email" && cat "${CF_PATH}/email" || echo '<YOUR_EMAIL_ADDRESS_HERE>') | |
zone=$(test -r "${CF_PATH}/zone" && cat "${CF_PATH}/zone" || echo '<YOUR_ZONE_HERE>') | |
ttl=$(test -r "${CF_PATH}/ttl" && cat "${CF_PATH}/ttl" || echo '<DEFAULT_TTL_SECONDS_HERE>') | |
# "service_mode" [applies to A/AAAA/CNAME] | |
# Status of CloudFlare Proxy, 1 = orange cloud, 0 = grey cloud. | |
cfProxy=0 | |
# | |
# End Configuration | |
## | |
zid=$(test -r "${HOME}/.cloudflare/zid" && cat "${HOME}/.cloudflare/zid" || echo '') | |
# Attempt zone-id lookup if it is absent. | |
if test -z "${zid}"; then | |
response=$( | |
curl \ | |
--fail \ | |
--silent \ | |
--show-error \ | |
--compressed \ | |
-H "X-Auth-Email: ${email}" \ | |
-H "X-Auth-Key: ${tkn}" \ | |
"https://api.cloudflare.com/client/v4/zones?name=${zone}" | |
) | |
rc=$? | |
if test $rc -ne 0; then | |
echo "error: failed to lookup zone-id for domain name \"${zone}\", curl exit code=${rc}" 1>&2 | |
exit $rc | |
else | |
zid=$( | |
echo "${response}" | python -c 'import json, sys | |
data = json.loads(sys.argv[1] if len(sys.argv)>1 else sys.stdin.read()) | |
print(data.get("result", [{}])[0].get("id", "") if len(data.get("result", [])) > 0 else "")' | |
) | |
if test -z "${zid}"; then | |
echo "error: no zone-id found matching domain name \"${zone}\"" 1>&2 | |
exit 1 | |
else | |
echo "${zid}" > "${HOME}/.cloudflare/zid" | |
fi | |
fi | |
fi | |
if test -z "$1" || test "$1" = '-h' || test "$1" = '--help'; then | |
echo "usage: $0 [ACTION] [additionalParameters?].. | |
ACTION - one of \"create\", \"read\", \"update\", \"delete\", or \"id\"" 1>&2 | |
exit 1 | |
fi | |
action=$1 | |
# Action aliases. | |
if test "${action}" = 'add' || test "${action}" = '+'; then action='create'; fi | |
if test "${action}" = 'edit' || test "${action}" = 'modify' || test "${action}" = '~'; then action='update'; fi | |
if test "${action}" = 'remove' || test "${action}" = 'rm' || test "${action}" = 'erase' || test "${action}" = '-'; then action='delete'; fi | |
if test "${action}" = 'list'; then action='read'; fi | |
# Validate action. | |
test "${action}" != 'create' && \ | |
test "${action}" != 'read' && \ | |
test "${action}" != 'update' && \ | |
test "${action}" != 'delete' && \ | |
test "${action}" != 'id' && \ | |
echo "error: unrecognized action \"${action}\" (see -h or --help), operation aborted" 1>&2 && exit 1 || true | |
# Translate requested action to CloudFlare's name for the action. | |
if test "${action}" = 'create'; then a='rec_new'; fi | |
if test "${action}" = 'read' || test "${action}" = 'id'; then a='rec_load_all'; fi | |
if test "${action}" = 'update'; then a='rec_edit'; fi | |
if test "${action}" = 'delete'; then a='rec_delete'; fi | |
if test "${action}" = 'id'; then | |
test -z "$2" && echo 'error: missing required parameter: search query' 1>&2 && exit 1 || true | |
results='' | |
page=1 | |
while true; do | |
searchQuery=$2 | |
response=$( | |
curl \ | |
--fail \ | |
--silent \ | |
--show-error \ | |
--compressed \ | |
-H "X-Auth-Email: ${email}" \ | |
-H "X-Auth-Key: ${tkn}" \ | |
-H 'Content-Type: application/json' \ | |
"https://api.cloudflare.com/client/v4/zones/${zid}/dns_records?page=${page}&per_page=100" | |
) | |
rc=$? | |
if test $rc -ne 0; then | |
echo "error: cloudflare dns record query failed on page=${page}, curl exit code=${rc}" 1>&2 | |
exit $rc | |
fi | |
result=$( | |
echo "${response}" | python -c 'import json, sys | |
data = json.loads(sys.argv[1] if len(sys.argv)>1 else sys.stdin.read()) | |
#sys.stderr.write("%s" % data) | |
for record in data.get("result", []): | |
if record["type"].upper() in ("CNAME", "A", "TXT"): | |
print("{0} {1} {2}".format(record["id"], record["name"], record["content"]))' \ | |
| grep "${searchQuery}" | |
) | |
results=$(echo -e -n "${results}\n${result}" | grep -v '^$') | |
numRecords=$( | |
echo "${response}" | python -c 'import json, sys | |
data = json.loads(sys.argv[1] if len(sys.argv)>1 else sys.stdin.read()) | |
sys.stdout.write("%s" % (len(data.get("result", [])),))' | |
) | |
if test ${numRecords} -lt 100; then | |
break | |
fi | |
page=$(($page + 1)) | |
done | |
test -z "${results}" && echo "error: no results found for search query \"${searchQuery}\"" 1>&2 && exit 1 || true | |
test $(echo "${results}" | wc -l) -gt 1 && echo -e "error: too many results found for search query \"${searchQuery}\":\n${results}" 1>&2 && exit 1 || true | |
echo "${results}" | cut -d' ' -f1 | |
exit 0 | |
fi | |
if test "${action}" = 'read'; then | |
filter=$2 | |
page=1 | |
out='id\tname\tcontent | |
--\t----\t------- | |
' | |
while true; do | |
response=$( | |
curl \ | |
--fail \ | |
--silent \ | |
--show-error \ | |
--compressed \ | |
-H "X-Auth-Email: ${email}" \ | |
-H "X-Auth-Key: ${tkn}" \ | |
-H 'Content-Type: application/json' \ | |
"https://api.cloudflare.com/client/v4/zones/${zid}/dns_records?page=${page}&per_page=100" | |
) | |
rc=$? | |
if test $rc -ne 0; then | |
echo "error: cloudflare dns record query failed on page=${page}, curl exit code=${rc}" 1>&2 | |
exit $rc | |
fi | |
out="${out} | |
$(echo "${response}" | python -c 'import json, sys | |
data = json.loads(sys.argv[1] if len(sys.argv)>1 else sys.stdin.read()) | |
for record in data.get("result", []): | |
if record["type"].upper() in ("CNAME", "A", "TXT"): | |
print("{0}\t{1}\t{2}".format(record["id"], record["name"], record["content"]))' \ | |
| grep "$(test -n "${filter}" && echo "${filter}" || echo '.*')")" | |
numRecords=$( | |
echo "${response}" | python -c 'import json, sys | |
data = json.loads(sys.argv[1] if len(sys.argv)>1 else sys.stdin.read()) | |
sys.stdout.write("%s" % (len(data.get("result", [])),))' | |
) | |
if test ${numRecords} -lt 100; then | |
break | |
fi | |
page=$(($page + 1)) | |
done | |
column -t <<< "$(echo -e "${out}")" | |
fi | |
if test "${action}" = 'create'; then | |
method='POST' | |
elif test "${action}" = 'update'; then | |
method='PUT' | |
elif test "${action}" = 'delete'; then | |
method='DELETE' | |
fi | |
apiUrl="https://api.cloudflare.com/client/v4/zones/${zid}/dns_records" | |
httpJsonData='{' | |
if test "${action}" = 'create' || test "${action}" = 'update'; then | |
test -z "$2" && echo 'error: missing required parameter: subdomain name' 1>&2 && exit 1 || true | |
httpJsonData="${httpJsonData}\"name\": \"$2\"" | |
test -z "$3" && echo 'error: missing required parameter: ip or cname hostname' 1>&2 && exit 1 || true | |
if test -z "$4"; then | |
if test -n "$(echo "$3" | grep '^[0-9\.]\+$')"; then | |
httpJsonData="${httpJsonData}, \"type\": \"A\"" | |
else | |
httpJsonData="${httpJsonData}, \"type\": \"CNAME\"" | |
fi | |
else | |
httpJsonData="${httpJsonData}, \"type\": \"$4\"" | |
fi | |
httpJsonData="${httpJsonData}, \"content\": \"$3\", \"ttl\": ${ttl}" | |
fi | |
if test "${action}" = 'update'; then | |
test -z "$2" && echo 'error: missing required parameter: subdomain name' && exit 1 || true | |
test -z "$3" && echo 'error: missing required parameter: ip-address or target domain name' && exit 1 || true | |
if test -n "$4"; then | |
recordId="$4" | |
else | |
specifier=$2 | |
echo "info: attempting to resolve record id for \"${specifier}\"" 1>&2 | |
recordId=$("$0" id "${specifier}") | |
rc=$? | |
test $rc -ne 0 && echo "error: id resolution failed for specifier \"${specifier}\"" 1>&2 && exit 1 || true | |
fi | |
apiUrl="${apiUrl}/${recordId}" | |
httpJsonData="${httpJsonData}, \"id\": \"${recordId}\"" | |
fi | |
httpJsonData="${httpJsonData}}" | |
if test "${action}" = 'delete'; then | |
test -z "$2" && echo 'error: missing required parameter: record specifier (id, or name to resolve to id)' 1>&2 && exit 1 || true | |
# Test if specifier | |
specifier=$2 | |
if test -z "$(echo "${specifier}" | grep '^[0-9a-f]\+')"; then | |
echo "info: attempting to resolve record id for \"${specifier}\"" | |
recordId=$("$0" id "${specifier}") | |
rc=$? | |
test $rc -ne 0 && echo "error: id resolution failed for specifier \"${specifier}\"" 1>&2 && exit 1 || true | |
fi | |
apiUrl="${apiUrl}/${recordId}" | |
httpJsonData='' | |
fi | |
if test "${action}" = 'create' || test "${action}" = 'update' || test "${action}" = 'delete'; then | |
curl \ | |
--silent \ | |
--show-error \ | |
--fail \ | |
--compressed \ | |
-X "${method}" \ | |
-H 'Content-Type: application/json' \ | |
-H "X-Auth-Email: ${email}" \ | |
-H "X-Auth-Key: ${tkn}" \ | |
--data "${httpJsonData}" \ | |
"${apiUrl}" | |
rc=$? | |
echo '' | |
if test $rc -ne 0; then | |
echo "error: failed to ${action} record, curl return code=${rc}" 1>&2 | |
exit $rc | |
fi | |
fi | |
exit 0 |
@cf-b Thanks for the helpful suggestion.
As a sidenote: I guess GitHub doesn't send email notifications for gists, because I just saw this now -- 14 months after you posted the message.
Hi Jay,
CloudFlare will migrate to a new API version in November 2016 (https://www.cloudflare.com/migrating-to-v4/). Do you intend to update your script to support the new API?
- Daniel
It now uses the V4 API. Thank you for your help @dgehri!
What is the proper way to use delete?
./cf.sh delete demo
curl: (22) The requested URL returned error: 400 Bad Request
error: failed to delete record, curl return code=22
Just found out, you probably have a typo, change that to:
--- cf.sh.orig 2016-08-26 17:53:26.544435110 +0200
+++ cf.sh 2016-08-26 23:31:41.496494641 +0200
@@ -245,7 +245,7 @@
test -z "$2" && echo 'error: missing required parameter: record specifier (id, or name to resolve to id)' 1>&2 && exit 1 || true
# Test if specifier
specifier=$2
- if test -z "$(echo "${specifier}" | grep '^[0-9a-f]+')"; then
- if test -n "$(echo "${specifier}" | grep '^[0-9a-f]+')"; then
echo "info: attempting to resolve record id for "${specifier}""
recordId=$("$0" id "${specifier}")
rc=$?
Have you considered putting more detailed help output into this? Would be nice to have some examples; eg, in my use case I want to take ngrok
output and assign the temporarily assigned domain to a CloudFlare name alias. It'd be neat to see this kind of example in a help.
the description is nothing https://gist.github.com/ylluminate @ylluminate @mitjax how to run simple add a cname against this like
demo.example.co cnam kkjbjlknllmlklmkbknl ? is it working script?
You may want to modify your script to support DNS record listing returns with more than 180 DNS records (the maximum limit per API response). Multiple calls are necessary, specifying an offset to paginate through the records. Here's an example of this in PHP: