dns_hetznercloud.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. #!/usr/bin/env sh
  2. # shellcheck disable=SC2034
  3. dns_hetznercloud_info='Hetzner Cloud DNS
  4. Site: Hetzner.com
  5. Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hetznercloud
  6. Options:
  7. HETZNER_TOKEN API token for the Hetzner Cloud DNS API
  8. Optional:
  9. HETZNER_TTL Custom TTL for new TXT rrsets (default 120)
  10. HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1)
  11. HETZNER_MAX_ATTEMPTS Number of 1s polls to wait for async actions (default 120)
  12. Issues: github.com/acmesh-official/acme.sh/issues
  13. '
  14. HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1"
  15. HETZNERCLOUD_TTL_DEFAULT=120
  16. HETZNER_MAX_ATTEMPTS_DEFAULT=120
  17. ######## Public functions #####################
  18. dns_hetznercloud_add() {
  19. fulldomain="$(_idn "${1}")"
  20. txtvalue="${2}"
  21. _info "Using Hetzner Cloud DNS API to add record"
  22. if ! _hetznercloud_init; then
  23. return 1
  24. fi
  25. if ! _hetznercloud_prepare_zone "${fulldomain}"; then
  26. _err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
  27. return 1
  28. fi
  29. if ! _hetznercloud_get_rrset; then
  30. return 1
  31. fi
  32. if [ "${_hetznercloud_last_http_code}" = "200" ]; then
  33. if _hetznercloud_rrset_contains_value "${txtvalue}"; then
  34. _info "TXT record already present; nothing to do."
  35. return 0
  36. fi
  37. elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
  38. _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
  39. return 1
  40. fi
  41. add_payload="$(_hetznercloud_build_add_payload "${txtvalue}")"
  42. if [ -z "${add_payload}" ]; then
  43. _err "Failed to build request payload."
  44. return 1
  45. fi
  46. if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_add}" "${add_payload}"; then
  47. return 1
  48. fi
  49. case "${_hetznercloud_last_http_code}" in
  50. 200 | 201 | 202 | 204)
  51. if ! _hetznercloud_handle_action_response "TXT record add"; then
  52. return 1
  53. fi
  54. _info "Hetzner Cloud TXT record added."
  55. return 0
  56. ;;
  57. 401 | 403)
  58. _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
  59. _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
  60. return 1
  61. ;;
  62. 409 | 422)
  63. _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the add_records request" "${_hetznercloud_last_http_code}"
  64. return 1
  65. ;;
  66. *)
  67. _hetznercloud_log_http_error "Hetzner Cloud DNS add_records request failed" "${_hetznercloud_last_http_code}"
  68. return 1
  69. ;;
  70. esac
  71. }
  72. dns_hetznercloud_rm() {
  73. fulldomain="$(_idn "${1}")"
  74. txtvalue="${2}"
  75. _info "Using Hetzner Cloud DNS API to remove record"
  76. if ! _hetznercloud_init; then
  77. return 1
  78. fi
  79. if ! _hetznercloud_prepare_zone "${fulldomain}"; then
  80. _err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
  81. return 1
  82. fi
  83. if ! _hetznercloud_get_rrset; then
  84. return 1
  85. fi
  86. if [ "${_hetznercloud_last_http_code}" = "404" ]; then
  87. _info "TXT rrset does not exist; nothing to remove."
  88. return 0
  89. fi
  90. if [ "${_hetznercloud_last_http_code}" != "200" ]; then
  91. _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
  92. return 1
  93. fi
  94. if _hetznercloud_rrset_contains_value "${txtvalue}"; then
  95. remove_payload="$(_hetznercloud_build_remove_payload "${txtvalue}")"
  96. if [ -z "${remove_payload}" ]; then
  97. _err "Failed to build remove_records payload."
  98. return 1
  99. fi
  100. if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_remove}" "${remove_payload}"; then
  101. return 1
  102. fi
  103. case "${_hetznercloud_last_http_code}" in
  104. 200 | 201 | 202 | 204)
  105. if ! _hetznercloud_handle_action_response "TXT record remove"; then
  106. return 1
  107. fi
  108. _info "Hetzner Cloud TXT record removed."
  109. return 0
  110. ;;
  111. 401 | 403)
  112. _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
  113. _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
  114. return 1
  115. ;;
  116. 404)
  117. _info "TXT rrset already absent after remove action."
  118. return 0
  119. ;;
  120. 409 | 422)
  121. _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the remove_records request" "${_hetznercloud_last_http_code}"
  122. return 1
  123. ;;
  124. *)
  125. _hetznercloud_log_http_error "Hetzner Cloud DNS remove_records request failed" "${_hetznercloud_last_http_code}"
  126. return 1
  127. ;;
  128. esac
  129. else
  130. _info "TXT value not present; nothing to remove."
  131. return 0
  132. fi
  133. }
  134. #################### Private functions ##################################
  135. _hetznercloud_init() {
  136. HETZNER_TOKEN="${HETZNER_TOKEN:-$(_readaccountconf_mutable HETZNER_TOKEN)}"
  137. if [ -z "${HETZNER_TOKEN}" ]; then
  138. _err "The environment variable HETZNER_TOKEN must be set for the Hetzner Cloud DNS API."
  139. return 1
  140. fi
  141. HETZNER_TOKEN=$(echo "${HETZNER_TOKEN}" | tr -d '"')
  142. _saveaccountconf_mutable HETZNER_TOKEN "${HETZNER_TOKEN}"
  143. HETZNER_API="${HETZNER_API:-$(_readaccountconf_mutable HETZNER_API)}"
  144. if [ -z "${HETZNER_API}" ]; then
  145. HETZNER_API="${HETZNERCLOUD_API_DEFAULT}"
  146. fi
  147. _saveaccountconf_mutable HETZNER_API "${HETZNER_API}"
  148. HETZNER_TTL="${HETZNER_TTL:-$(_readaccountconf_mutable HETZNER_TTL)}"
  149. if [ -z "${HETZNER_TTL}" ]; then
  150. HETZNER_TTL="${HETZNERCLOUD_TTL_DEFAULT}"
  151. fi
  152. ttl_check=$(printf "%s" "${HETZNER_TTL}" | tr -d '0-9')
  153. if [ -n "${ttl_check}" ]; then
  154. _err "HETZNER_TTL must be an integer value."
  155. return 1
  156. fi
  157. _saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}"
  158. HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS:-$(_readaccountconf_mutable HETZNER_MAX_ATTEMPTS)}"
  159. if [ -z "${HETZNER_MAX_ATTEMPTS}" ]; then
  160. HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS_DEFAULT}"
  161. fi
  162. attempts_check=$(printf "%s" "${HETZNER_MAX_ATTEMPTS}" | tr -d '0-9')
  163. if [ -n "${attempts_check}" ]; then
  164. _err "HETZNER_MAX_ATTEMPTS must be an integer value."
  165. return 1
  166. fi
  167. _saveaccountconf_mutable HETZNER_MAX_ATTEMPTS "${HETZNER_MAX_ATTEMPTS}"
  168. return 0
  169. }
  170. _hetznercloud_prepare_zone() {
  171. _hetznercloud_zone_id=""
  172. _hetznercloud_zone_name=""
  173. _hetznercloud_zone_name_lc=""
  174. _hetznercloud_rr_name=""
  175. _hetznercloud_rrset_path=""
  176. _hetznercloud_rrset_action_add=""
  177. _hetznercloud_rrset_action_remove=""
  178. fulldomain_lc=$(printf "%s" "${1}" | sed 's/\.$//' | _lower_case)
  179. i=2
  180. p=1
  181. while true; do
  182. candidate=$(printf "%s" "${fulldomain_lc}" | cut -d . -f "${i}"-100)
  183. if [ -z "${candidate}" ]; then
  184. return 1
  185. fi
  186. if _hetznercloud_get_zone_by_candidate "${candidate}"; then
  187. zone_name_lc="${_hetznercloud_zone_name_lc}"
  188. if [ "${fulldomain_lc}" = "${zone_name_lc}" ]; then
  189. _hetznercloud_rr_name="@"
  190. else
  191. suffix=".${zone_name_lc}"
  192. if _endswith "${fulldomain_lc}" "${suffix}"; then
  193. _hetznercloud_rr_name="${fulldomain_lc%"${suffix}"}"
  194. else
  195. _hetznercloud_rr_name="${fulldomain_lc}"
  196. fi
  197. fi
  198. _hetznercloud_rrset_path=$(printf "%s" "${_hetznercloud_rr_name}" | _url_encode)
  199. _hetznercloud_rrset_action_add="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/add_records"
  200. _hetznercloud_rrset_action_remove="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/remove_records"
  201. return 0
  202. fi
  203. p=${i}
  204. i=$(_math "${i}" + 1)
  205. done
  206. }
  207. _hetznercloud_get_zone_by_candidate() {
  208. candidate="${1}"
  209. zone_key=$(printf "%s" "${candidate}" | sed 's/[^A-Za-z0-9]/_/g')
  210. zone_conf_key="HETZNERCLOUD_ZONE_ID_for_${zone_key}"
  211. cached_zone_id=$(_readdomainconf "${zone_conf_key}")
  212. if [ -n "${cached_zone_id}" ]; then
  213. if _hetznercloud_api GET "/zones/${cached_zone_id}"; then
  214. if [ "${_hetznercloud_last_http_code}" = "200" ]; then
  215. zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
  216. if _hetznercloud_parse_zone_fields "${zone_data}"; then
  217. zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
  218. if [ "${zone_name_lc}" = "${candidate}" ]; then
  219. return 0
  220. fi
  221. fi
  222. elif [ "${_hetznercloud_last_http_code}" = "404" ]; then
  223. _cleardomainconf "${zone_conf_key}"
  224. fi
  225. else
  226. return 1
  227. fi
  228. fi
  229. if _hetznercloud_api GET "/zones/${candidate}"; then
  230. if [ "${_hetznercloud_last_http_code}" = "200" ]; then
  231. zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
  232. if _hetznercloud_parse_zone_fields "${zone_data}"; then
  233. zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
  234. if [ "${zone_name_lc}" = "${candidate}" ]; then
  235. _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
  236. return 0
  237. fi
  238. fi
  239. elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
  240. _hetznercloud_log_http_error "Hetzner Cloud zone lookup failed" "${_hetznercloud_last_http_code}"
  241. return 1
  242. fi
  243. else
  244. return 1
  245. fi
  246. encoded_candidate=$(printf "%s" "${candidate}" | _url_encode)
  247. if ! _hetznercloud_api GET "/zones?name=${encoded_candidate}"; then
  248. return 1
  249. fi
  250. if [ "${_hetznercloud_last_http_code}" != "200" ]; then
  251. if [ "${_hetznercloud_last_http_code}" = "404" ]; then
  252. return 1
  253. fi
  254. _hetznercloud_log_http_error "Hetzner Cloud zone search failed" "${_hetznercloud_last_http_code}"
  255. return 1
  256. fi
  257. zone_data=$(_hetznercloud_extract_zone_from_list "${response}" "${candidate}")
  258. if [ -z "${zone_data}" ]; then
  259. return 1
  260. fi
  261. if ! _hetznercloud_parse_zone_fields "${zone_data}"; then
  262. return 1
  263. fi
  264. _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
  265. return 0
  266. }
  267. _hetznercloud_parse_zone_fields() {
  268. zone_json="${1}"
  269. if [ -z "${zone_json}" ]; then
  270. return 1
  271. fi
  272. normalized=$(printf "%s" "${zone_json}" | _normalizeJson)
  273. zone_id=$(printf "%s" "${normalized}" | _egrep_o '"id":[^,}]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')
  274. zone_name=$(printf "%s" "${normalized}" | _egrep_o '"name":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
  275. if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then
  276. return 1
  277. fi
  278. zone_name_trimmed=$(printf "%s" "${zone_name}" | sed 's/\.$//')
  279. if zone_name_ascii=$(_idn "${zone_name_trimmed}"); then
  280. zone_name="${zone_name_ascii}"
  281. else
  282. zone_name="${zone_name_trimmed}"
  283. fi
  284. _hetznercloud_zone_id="${zone_id}"
  285. _hetznercloud_zone_name="${zone_name}"
  286. _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case)
  287. return 0
  288. }
  289. _hetznercloud_extract_zone_from_list() {
  290. list_response=$(printf "%s" "${1}" | _normalizeJson)
  291. candidate="${2}"
  292. escaped_candidate=$(_hetznercloud_escape_regex "${candidate}")
  293. printf "%s" "${list_response}" | _egrep_o "{[^{}]*\"name\":\"${escaped_candidate}\"[^{}]*}" | _head_n 1
  294. }
  295. _hetznercloud_escape_regex() {
  296. printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/\./\\./g' | sed 's/-/\\-/g'
  297. }
  298. _hetznercloud_get_rrset() {
  299. if [ -z "${_hetznercloud_zone_id}" ] || [ -z "${_hetznercloud_rrset_path}" ]; then
  300. return 1
  301. fi
  302. if ! _hetznercloud_api GET "/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT"; then
  303. return 1
  304. fi
  305. return 0
  306. }
  307. _hetznercloud_rrset_contains_value() {
  308. wanted_value="${1}"
  309. normalized=$(printf "%s" "${response}" | _normalizeJson)
  310. escaped_value=$(_hetznercloud_escape_value "${wanted_value}")
  311. search_pattern="\"value\":\"\\\\\"${escaped_value}\\\\\"\""
  312. if _contains "${normalized}" "${search_pattern}"; then
  313. return 0
  314. fi
  315. return 1
  316. }
  317. _hetznercloud_build_add_payload() {
  318. value="${1}"
  319. escaped_value=$(_hetznercloud_escape_value "${value}")
  320. printf '{"ttl":%s,"records":[{"value":"\\"%s\\""}]}' "${HETZNER_TTL}" "${escaped_value}"
  321. }
  322. _hetznercloud_build_remove_payload() {
  323. value="${1}"
  324. escaped_value=$(_hetznercloud_escape_value "${value}")
  325. printf '{"records":[{"value":"\\"%s\\""}]}' "${escaped_value}"
  326. }
  327. _hetznercloud_escape_value() {
  328. printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g'
  329. }
  330. _hetznercloud_error_message() {
  331. if [ -z "${response}" ]; then
  332. return 1
  333. fi
  334. message=$(printf "%s" "${response}" | _normalizeJson | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
  335. if [ -n "${message}" ]; then
  336. printf "%s" "${message}"
  337. return 0
  338. fi
  339. return 1
  340. }
  341. _hetznercloud_log_http_error() {
  342. context="${1}"
  343. code="${2}"
  344. message="$(_hetznercloud_error_message)"
  345. if [ -n "${context}" ]; then
  346. if [ -n "${message}" ]; then
  347. _err "${context} (HTTP ${code}): ${message}"
  348. else
  349. _err "${context} (HTTP ${code})"
  350. fi
  351. else
  352. if [ -n "${message}" ]; then
  353. _err "Hetzner Cloud DNS API error (HTTP ${code}): ${message}"
  354. else
  355. _err "Hetzner Cloud DNS API error (HTTP ${code})"
  356. fi
  357. fi
  358. }
  359. _hetznercloud_api() {
  360. method="${1}"
  361. ep="${2}"
  362. data="${3}"
  363. retried="${4}"
  364. if [ -z "${method}" ]; then
  365. method="GET"
  366. fi
  367. if ! _startswith "${ep}" "/"; then
  368. ep="/${ep}"
  369. fi
  370. url="${HETZNER_API}${ep}"
  371. export _H1="Authorization: Bearer ${HETZNER_TOKEN}"
  372. export _H2="Accept: application/json"
  373. export _H3=""
  374. export _H4=""
  375. export _H5=""
  376. : >"${HTTP_HEADER}"
  377. if [ "${method}" = "GET" ]; then
  378. response="$(_get "${url}")"
  379. else
  380. if [ -z "${data}" ]; then
  381. data="{}"
  382. fi
  383. response="$(_post "${data}" "${url}" "" "${method}" "application/json")"
  384. fi
  385. ret="${?}"
  386. _hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
  387. if [ "${ret}" != "0" ]; then
  388. return 1
  389. fi
  390. if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then
  391. retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r')
  392. if [ -z "${retry_after}" ]; then
  393. retry_after=1
  394. fi
  395. _info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds."
  396. _sleep "${retry_after}"
  397. if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then
  398. return 1
  399. fi
  400. return 0
  401. fi
  402. return 0
  403. }
  404. _hetznercloud_handle_action_response() {
  405. context="${1}"
  406. if [ -z "${response}" ]; then
  407. return 0
  408. fi
  409. normalized=$(printf "%s" "${response}" | _normalizeJson)
  410. failed_message=""
  411. if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then
  412. if [ -n "${failed_message}" ]; then
  413. _err "Hetzner Cloud DNS ${context} failed: ${failed_message}"
  414. else
  415. _err "Hetzner Cloud DNS ${context} failed."
  416. fi
  417. return 1
  418. fi
  419. action_ids=""
  420. if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then
  421. for action_id in ${action_ids}; do
  422. if [ -z "${action_id}" ]; then
  423. continue
  424. fi
  425. if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then
  426. return 1
  427. fi
  428. done
  429. fi
  430. return 0
  431. }
  432. _hetznercloud_extract_failed_action_message() {
  433. normalized="${1}"
  434. failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]')
  435. if [ -z "${failed_section}" ]; then
  436. return 1
  437. fi
  438. if _contains "${failed_section}" '"failed_actions":[]'; then
  439. return 1
  440. fi
  441. message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
  442. if [ -n "${message}" ]; then
  443. printf "%s" "${message}"
  444. else
  445. printf "%s" "${failed_section}"
  446. fi
  447. return 0
  448. }
  449. _hetznercloud_extract_action_ids() {
  450. normalized="${1}"
  451. actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]')
  452. if [ -z "${actions_section}" ]; then
  453. return 1
  454. fi
  455. action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ')
  456. action_ids=$(printf "%s" "${action_ids}" | tr -s ' ')
  457. action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//')
  458. if [ -z "${action_ids}" ]; then
  459. return 1
  460. fi
  461. printf "%s" "${action_ids}"
  462. return 0
  463. }
  464. _hetznercloud_wait_for_action() {
  465. action_id="${1}"
  466. context="${2}"
  467. attempts="0"
  468. while true; do
  469. if ! _hetznercloud_api GET "/actions/${action_id}"; then
  470. return 1
  471. fi
  472. if [ "${_hetznercloud_last_http_code}" != "200" ]; then
  473. _hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}"
  474. return 1
  475. fi
  476. normalized=$(printf "%s" "${response}" | _normalizeJson)
  477. action_status=$(_hetznercloud_action_status_from_normalized "${normalized}")
  478. if [ -z "${action_status}" ]; then
  479. _err "Hetzner Cloud DNS ${context} action ${action_id} returned no status."
  480. return 1
  481. fi
  482. if [ "${action_status}" = "success" ]; then
  483. return 0
  484. fi
  485. if [ "${action_status}" = "error" ]; then
  486. if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then
  487. _err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}"
  488. else
  489. _err "Hetzner Cloud DNS ${context} action ${action_id} failed."
  490. fi
  491. return 1
  492. fi
  493. attempts=$(_math "${attempts}" + 1)
  494. if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then
  495. _err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts."
  496. return 1
  497. fi
  498. _sleep 1
  499. done
  500. }
  501. _hetznercloud_action_status_from_normalized() {
  502. normalized="${1}"
  503. status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
  504. printf "%s" "${status}"
  505. }
  506. _hetznercloud_action_error_from_normalized() {
  507. normalized="${1}"
  508. error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}')
  509. if [ -z "${error_section}" ]; then
  510. return 1
  511. fi
  512. message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
  513. if [ -n "${message}" ]; then
  514. printf "%s" "${message}"
  515. return 0
  516. fi
  517. code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
  518. if [ -n "${code}" ]; then
  519. printf "%s" "${code}"
  520. return 0
  521. fi
  522. return 1
  523. }