You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
13 KiB

  1. # -*- mode: shell-script -*-
  2. BIND_CONFIG_DIR=/etc/bind
  3. ## Generate bind config files in $cfgdir from YAML configuration $cfg
  4. bind:cfg:generate() {
  5. local cfg="$1" vars="$2" zone_file include_zone_statements all_domain \
  6. domains expanded_domains zone_cfg
  7. ## __all__ domain is treated here like a normal domain
  8. expanded_domains=""
  9. while read-0 domain zone_cfg; do
  10. if ! [[ "$domain" =~ ^[a-z0-9_\(\),.-]+$ ]]; then
  11. err "Invalid domain '$domain' as key in ${WHITE}zones${NORMAL} records."
  12. return 1
  13. fi
  14. domains=()
  15. bind:parse:expand-in "$domain" domains "$vars" || return 1
  16. for domain in "${domains[@]}"; do
  17. expanded_domains=$(merge_yaml_str "$expanded_domains" \
  18. "$(yaml_key_val_str "$domain" "$zone_cfg")")
  19. done
  20. done < <(e "$cfg" | shyaml key-values-0)
  21. ## __all__ domain is merged in other domain domain
  22. all_domain=$(e "$expanded_domains" | shyaml get-value "__all__")
  23. resolved_domains=""
  24. while read-0 domain zone_cfg; do
  25. if [ "$domain" == "__all__" ]; then
  26. continue
  27. fi
  28. resolved_domains=$(merge_yaml_str \
  29. "$resolved_domains" \
  30. "$(yaml_key_val_str "$domain" "$all_domain")" \
  31. "$(yaml_key_val_str "$domain" "$zone_cfg")")
  32. done < <(e "$expanded_domains" | shyaml key-values-0)
  33. include_zone_statements=
  34. while read-0 domain zone_cfg; do
  35. info "Considering ${WHITE}$domain${NORMAL} zone configuration"
  36. zone_file="${BIND_CONFIG_DIR}/db.${domain}"
  37. zone_hash_file="${BIND_CONFIG_DIR}/.db.${domain}.hash"
  38. zone_def=$(bind:content:zone_definition \
  39. "$domain" "$zone_cfg" "$vars") || return 1
  40. zone_def_hash=$(H "$zone_def")
  41. ## We need to store the last serial as well as the last hash in
  42. ## the datastore to allow a full restore (will correct serials)
  43. ## without discontinuity of services.
  44. current_hash=
  45. current_serial=
  46. if [ -e "${SERVICE_DATASTORE}${zone_hash_file}" ]; then
  47. current_hash_content="$(cat "${SERVICE_DATASTORE}${zone_hash_file}")" || true
  48. current_hash=${current_hash_content% *}
  49. current_serial=${current_hash_content#* }
  50. echo " found a previous hash file with serial: $current_serial" >&2
  51. fi
  52. if [ "$current_hash" == "$zone_def_hash" ]; then ## no need to change full serial
  53. echo " can use previous serial as zone definition seems to have not changed" >&2
  54. serial=${current_serial}
  55. else
  56. echo " needs a new serial as zone definition seems to have changed" >&2
  57. date=$(date +%Y%m%d)
  58. if [ "$date" == "${current_serial:0:8}" ]; then
  59. serial="$date$(printf "%02d" "$((${current_serial: -2:2} + 1))")"
  60. echo " increment serial to $serial" >&2
  61. else
  62. serial="${date}00"
  63. echo " first definition for the current date" >&2
  64. fi
  65. fi
  66. zone_def=$(e "$zone_def" | sed -r "s/%%SERIAL%%/$serial/g")
  67. if ! [ -e "${SERVICE_CONFIGSTORE}${zone_file}" ] ||
  68. [ "$zone_def" != "$(cat "${SERVICE_CONFIGSTORE}${zone_file}")" ]; then
  69. echo " ${DARKYELLOW}writing${NORMAL} zone def file to config with serial $serial." >&2
  70. e "${zone_def}" > "${SERVICE_CONFIGSTORE}${zone_file}"
  71. else
  72. echo " zone def already available in config with serial $serial." >&2
  73. fi
  74. if ! [ -e "${SERVICE_DATASTORE}${zone_hash_file}" ] ||
  75. [ "$zone_def_hash $serial" != "$(cat "${SERVICE_DATASTORE}${zone_hash_file}")" ]; then
  76. echo " ${DARKYELLOW}writing${NORMAL} zone file hash to data with serial $serial." >&2
  77. e "$zone_def_hash $serial" > "${SERVICE_DATASTORE}${zone_hash_file}"
  78. else
  79. echo " zone def hash already available in data with serial $serial." >&2
  80. fi
  81. include_zone_statements+=$(
  82. bind:content:include_zone_statement "$domain" "${zone_file}"
  83. )$'\n'
  84. config_hash=$(p0 "$config_hash" "$zone_def_hash" | md5_compat) || exit 1
  85. done < <(e "$resolved_domains" | shyaml key-values-0)
  86. e "$include_zone_statements" > "${SERVICE_CONFIGSTORE}${BIND_CONFIG_DIR}/named.conf.local"
  87. }
  88. bind:content:include_zone_statement() {
  89. local domain="$1" zone_file="$2"
  90. cat <<EOF
  91. zone "${domain}" {
  92. type master;
  93. notify yes;
  94. file "${zone_file}";
  95. };
  96. EOF
  97. }
  98. bind:check:is_time() {
  99. local label="$1" value="$2"
  100. if ! [[ "$value" =~ ^[0-9smhdw]+$ ]]; then
  101. err "Invalid value for $label: '$value'." >&2
  102. echo " Please use a number of seconds or use one-character time units (IE: 1w, 1h30, 3600)" >&2
  103. return 1
  104. fi
  105. return 0
  106. }
  107. bind:parse:expand-in() {
  108. local value="$1" aname="$2" vars="$3"
  109. if ! [[ "$value" =~ ^[a-z0-9_\(\)\ \$,.-]+$ ]]; then
  110. err "Invalid value '$value' to expand."
  111. return 1
  112. fi
  113. declare -n cur="$aname"
  114. if [[ "$value" == *","* ]]; then
  115. value="${value//\(/\{}"
  116. value="${value//\)/\}}"
  117. value="${value// /\\ }"
  118. else
  119. value="\"$value\""
  120. fi
  121. value="${value//\$/\$_vars_}"
  122. while read-0 label v; do
  123. local "_vars_$label"="$v"
  124. done < <(e "$vars" | shyaml key-values-0)
  125. eval "cur+=($value)" || {
  126. err "Couldn't eval '$value'."
  127. return 1
  128. }
  129. }
  130. bind:content:zone_definition() {
  131. local domain="$1" zone_cfg="$2" vars_cfg="$3" \
  132. vtype cache_file="$CACHEDIR/$FUNCNAME.cache.$(H "$@")"
  133. if [ -e "$cache_file" ]; then
  134. # debug "$FUNCNAME: cache hit ($*)"
  135. cat "$cache_file"
  136. return 0
  137. fi
  138. ## Zone definition is created with %%SERIAL%% as placeholder in
  139. ## the serial number intended final emplacement. This allows to
  140. ## check the output's hash of this function to decide if we need
  141. ## to change the serial.
  142. zone_cfg=$(merge_yaml_str "
  143. \$ttl: 1w
  144. soa:
  145. email: dns@${domain}
  146. refresh: 8h
  147. retry: 4h
  148. expire: 1w
  149. ttl: 1d
  150. ns:
  151. _: dns
  152. " "$zone_cfg") || return 1
  153. vars_cfg=$(merge_yaml_str "
  154. mydomain: \"$domain\"
  155. " "$vars_cfg") || return 1
  156. while read-0 key subcfg; do
  157. # echo "KEY: $key" >&2
  158. # echo "SUBCONFIG:" >&2
  159. # echo "$subcfg" | prefix " | " >&2
  160. declare -A decls
  161. ttl=
  162. if [[ "$key" == *" "* ]]; then
  163. ttl=${key#* }
  164. key=${key%% *}
  165. fi
  166. case "$key" in
  167. "\$"*)
  168. case "$key" in
  169. "\$ttl")
  170. bind:check:is_time ttl "$subcfg" || return 1
  171. ;;
  172. *)
  173. echo "Unsupported variable '${key}'." >&2
  174. return 1
  175. esac
  176. decls[vars]+="${key^^} $subcfg"$'\n'
  177. ;;
  178. soa)
  179. soa_values="$(printf "%-12s ; %s" "%%SERIAL%%" "serial")"$'\n'
  180. for k in refresh retry expire ttl; do
  181. value=$(e "$subcfg" | shyaml get-value "$k" 2>/dev/null) || {
  182. err "Couldn't find any value for '$k' in '${domain}''s 'soa' config."
  183. return 1
  184. }
  185. bind:check:is_time "soa.$k" "$value" || {
  186. err "Error while reading soa definition of domain '$domain'."
  187. return 1
  188. }
  189. soa_values+="$(printf "%-12s ; %s" "$value" "$k")"$'\n'
  190. done
  191. email=$(e "$subcfg" | shyaml get-value "email" 2>/dev/null) || {
  192. err "Couldn't find any value for 'email' in '${domain}''s 'soa' config."
  193. return 1
  194. }
  195. soa_values="$(e "$soa_values" | prefix " ")"
  196. decls[soa]+="$(printf "%-28s IN SOA %s. %s. (" "@" "${domain}" "${email/@/.}")"$'\n'
  197. decls[soa]+="${soa_values}"$'\n'")"$'\n'
  198. ;;
  199. ns|mx|name|spf|txt)
  200. while read-0 name name_cfg; do
  201. if ! [[ "$name" =~ ^[a-z0-9_\(\),.-]+$ ]]; then
  202. err "Invalid name '$name' in ${domain}'s $key records."
  203. return 1
  204. fi
  205. names=()
  206. bind:parse:expand-in "$name" names "$vars_cfg" || return 1
  207. ## values
  208. vtype=$(e "$name_cfg" | shyaml get-type)
  209. # echo TYPE: $vtype >&2
  210. values=()
  211. case "$vtype" in
  212. "sequence")
  213. array_read-0 values < <(e "$name_cfg" | shyaml get-values-0)
  214. ;;
  215. "str")
  216. values=("$(e "$name_cfg" | shyaml get-value)")
  217. ;;
  218. *)
  219. err "Unsupported type '$vtype' of '$key' in $domain's zone definition."
  220. exit 1
  221. ;;
  222. esac
  223. values_expanded=()
  224. for value in "${values[@]}"; do
  225. case "$key" in
  226. ns|name)
  227. if ! [[ "$value" =~ ^[a-z0-9_\$\(\),.-]+$ ]]; then
  228. err "Invalid value '$value' in ${domain}'s $key records for '$name'."
  229. return 1
  230. fi
  231. ## Append in values_expanded
  232. bind:parse:expand-in "$value" values_expanded "$vars_cfg" || return 1
  233. ;;
  234. mx)
  235. ## space is allowed (actually mandatory)
  236. if ! [[ "$value" =~ ^[a-z0-9_\$\(\),.\ -]+$ ]]; then
  237. err "Invalid value '$value' in ${domain}'s $key records for '$name'."
  238. return 1
  239. fi
  240. ## Append in values_expanded
  241. bind:parse:expand-in "$value" values_expanded "$vars_cfg" || return 1
  242. ;;
  243. spf|txt)
  244. ## No expansion for them
  245. values_expanded+=("\"$value\"")
  246. ;;
  247. esac
  248. done
  249. for name in "${names[@]}"; do
  250. [ "$name" == "_" ] && name="@"
  251. for value in "${values_expanded[@]}"; do
  252. rkey="$key"
  253. if [ "$key" == "name" ]; then
  254. if [[ "$value" =~ ^([0-9]{1,3}\.){3,3}[0-9]{1,3}$ ]]; then
  255. rkey="a"
  256. elif [[ "$value" =~ ^([a-z0-9-]+\.?)+$ ]]; then
  257. rkey="cname"
  258. elif [ -z "$value" ]; then
  259. err "Empty value for 'name' '$name', was '${values[@]}' before evaluation"
  260. echo " in '$domain' zone definition." >&2
  261. return 1
  262. else
  263. err "Unrecognised value for 'name': '$value'."
  264. echo " in '$domain' zone definition." >&2
  265. return 1
  266. fi
  267. fi
  268. if [ -z "$value" ]; then
  269. err "Empty value for 'name' '$name'."
  270. echo " in '$domain' zone definition." >&2
  271. return 1
  272. fi
  273. decls[$rkey]+=$(printf "%-15s %-12s IN %s %s" "$name" "$ttl" "${rkey^^}" "$value")$'\n'
  274. done
  275. done
  276. done < <(e "$subcfg" | shyaml key-values-0)
  277. ;;
  278. *)
  279. err "Unknown keyword '$key' in '$domain' zone definition."
  280. return 1
  281. esac
  282. done < <(e "$zone_cfg" | shyaml key-values-0)
  283. for k in vars soa ns mx a cname spf txt; do
  284. [ -z "${decls[$k]}" ] && continue
  285. if [ "$k" != "vars" ]; then
  286. echo
  287. echo
  288. echo ";; $k"
  289. echo
  290. fi
  291. echo "${decls[$k]}" | grep -v "^$" | {
  292. case "$k" in
  293. a|ns|cname|spf|txt|vars)
  294. sort
  295. ;;
  296. soa|mx)
  297. cat
  298. ;;
  299. esac
  300. }
  301. done | tee "$cache_file"
  302. }