# -*- mode: shell-script -*- BIND_CONFIG_DIR=/etc/bind ## Generate bind config files in $cfgdir from YAML configuration $cfg bind:cfg:generate() { local cfg="$1" vars="$2" zone_file include_zone_statements all_domain \ domains expanded_domains zone_cfg ## __all__ domain is treated here like a normal domain expanded_domains="" while read-0 domain zone_cfg; do if ! [[ "$domain" =~ ^[a-z0-9_\(\),.-]+$ ]]; then err "Invalid domain '$domain' as key in ${WHITE}zones${NORMAL} records." return 1 fi domains=() bind:parse:expand-in "$domain" domains "$vars" || return 1 for domain in "${domains[@]}"; do expanded_domains=$(merge_yaml_str "$expanded_domains" \ "$(yaml_key_val_str "$domain" "$zone_cfg")") done done < <(e "$cfg" | shyaml key-values-0) ## __all__ domain is merged in other domain domain all_domain=$(e "$expanded_domains" | shyaml get-value "__all__") resolved_domains="" while read-0 domain zone_cfg; do if [ "$domain" == "__all__" ]; then continue fi resolved_domains=$(merge_yaml_str \ "$resolved_domains" \ "$(yaml_key_val_str "$domain" "$all_domain")" \ "$(yaml_key_val_str "$domain" "$zone_cfg")") done < <(e "$expanded_domains" | shyaml key-values-0) include_zone_statements= while read-0 domain zone_cfg; do info "Considering ${WHITE}$domain${NORMAL} zone configuration" zone_file="${BIND_CONFIG_DIR}/db.${domain}" zone_hash_file="${BIND_CONFIG_DIR}/.db.${domain}.hash" zone_def=$(bind:content:zone_definition \ "$domain" "$zone_cfg" "$vars") || return 1 zone_def_hash=$(H "$zone_def") ## We need to store the last serial as well as the last hash in ## the datastore to allow a full restore (will correct serials) ## without discontinuity of services. current_hash= current_serial= if [ -e "${SERVICE_DATASTORE}${zone_hash_file}" ]; then current_hash_content="$(cat "${SERVICE_DATASTORE}${zone_hash_file}")" || true current_hash=${current_hash_content% *} current_serial=${current_hash_content#* } echo " found a previous hash file with serial: $current_serial" >&2 fi if [ "$current_hash" == "$zone_def_hash" ]; then ## no need to change full serial echo " can use previous serial as zone definition seems to have not changed" >&2 serial=${current_serial} else echo " needs a new serial as zone definition seems to have changed" >&2 date=$(date +%Y%m%d) if [ "$date" == "${current_serial:0:8}" ]; then serial="$date$(printf "%02d" "$((${current_serial: -2:2} + 1))")" echo " increment serial to $serial" >&2 else serial="${date}00" echo " first definition for the current date" >&2 fi fi zone_def=$(e "$zone_def" | sed -r "s/%%SERIAL%%/$serial/g") if ! [ -e "${SERVICE_CONFIGSTORE}${zone_file}" ] || [ "$zone_def" != "$(cat "${SERVICE_CONFIGSTORE}${zone_file}")" ]; then echo " ${DARKYELLOW}writing${NORMAL} zone def file to config with serial $serial." >&2 e "${zone_def}" > "${SERVICE_CONFIGSTORE}${zone_file}" else echo " zone def already available in config with serial $serial." >&2 fi if ! [ -e "${SERVICE_DATASTORE}${zone_hash_file}" ] || [ "$zone_def_hash $serial" != "$(cat "${SERVICE_DATASTORE}${zone_hash_file}")" ]; then echo " ${DARKYELLOW}writing${NORMAL} zone file hash to data with serial $serial." >&2 e "$zone_def_hash $serial" > "${SERVICE_DATASTORE}${zone_hash_file}" else echo " zone def hash already available in data with serial $serial." >&2 fi include_zone_statements+=$( bind:content:include_zone_statement "$domain" "${zone_file}" )$'\n' config_hash=$(p0 "$config_hash" "$zone_def_hash" | md5_compat) || exit 1 done < <(e "$resolved_domains" | shyaml key-values-0) e "$include_zone_statements" > "${SERVICE_CONFIGSTORE}${BIND_CONFIG_DIR}/named.conf.local" } bind:content:include_zone_statement() { local domain="$1" zone_file="$2" cat <&2 echo " Please use a number of seconds or use one-character time units (IE: 1w, 1h30, 3600)" >&2 return 1 fi return 0 } bind:parse:expand-in() { local value="$1" aname="$2" vars="$3" if ! [[ "$value" =~ ^[a-z0-9_\(\)\ \$,.-]+$ ]]; then err "Invalid value '$value' to expand." return 1 fi declare -n cur="$aname" if [[ "$value" == *","* ]]; then value="${value//\(/\{}" value="${value//\)/\}}" value="${value// /\\ }" else value="\"$value\"" fi value="${value//\$/\$_vars_}" while read-0 label v; do local "_vars_$label"="$v" done < <(e "$vars" | shyaml key-values-0) eval "cur+=($value)" || { err "Couldn't eval '$value'." return 1 } } bind:content:zone_definition() { local domain="$1" zone_cfg="$2" vars_cfg="$3" \ vtype cache_file="$CACHEDIR/$FUNCNAME.cache.$(H "$@")" if [ -e "$cache_file" ]; then # debug "$FUNCNAME: cache hit ($*)" cat "$cache_file" return 0 fi ## Zone definition is created with %%SERIAL%% as placeholder in ## the serial number intended final emplacement. This allows to ## check the output's hash of this function to decide if we need ## to change the serial. zone_cfg=$(merge_yaml_str " \$ttl: 1w soa: email: dns@${domain} refresh: 8h retry: 4h expire: 1w ttl: 1d ns: _: dns " "$zone_cfg") || return 1 vars_cfg=$(merge_yaml_str " mydomain: \"$domain\" " "$vars_cfg") || return 1 while read-0 key subcfg; do # echo "KEY: $key" >&2 # echo "SUBCONFIG:" >&2 # echo "$subcfg" | prefix " | " >&2 declare -A decls ttl= if [[ "$key" == *" "* ]]; then ttl=${key#* } key=${key%% *} fi case "$key" in "\$"*) case "$key" in "\$ttl") bind:check:is_time ttl "$subcfg" || return 1 ;; *) echo "Unsupported variable '${key}'." >&2 return 1 esac decls[vars]+="${key^^} $subcfg"$'\n' ;; soa) soa_values="$(printf "%-12s ; %s" "%%SERIAL%%" "serial")"$'\n' for k in refresh retry expire ttl; do value=$(e "$subcfg" | shyaml get-value "$k" 2>/dev/null) || { err "Couldn't find any value for '$k' in '${domain}''s 'soa' config." return 1 } bind:check:is_time "soa.$k" "$value" || { err "Error while reading soa definition of domain '$domain'." return 1 } soa_values+="$(printf "%-12s ; %s" "$value" "$k")"$'\n' done email=$(e "$subcfg" | shyaml get-value "email" 2>/dev/null) || { err "Couldn't find any value for 'email' in '${domain}''s 'soa' config." return 1 } soa_values="$(e "$soa_values" | prefix " ")" decls[soa]+="$(printf "%-28s IN SOA %s. %s. (" "@" "${domain}" "${email/@/.}")"$'\n' decls[soa]+="${soa_values}"$'\n'")"$'\n' ;; ns|mx|name|spf|txt) while read-0 name name_cfg; do if ! [[ "$name" =~ ^[a-z0-9_\(\),.-]+$ ]]; then err "Invalid name '$name' in ${domain}'s $key records." return 1 fi names=() bind:parse:expand-in "$name" names "$vars_cfg" || return 1 ## values vtype=$(e "$name_cfg" | shyaml get-type) # echo TYPE: $vtype >&2 values=() case "$vtype" in "sequence") array_read-0 values < <(e "$name_cfg" | shyaml get-values-0) ;; "str") values=("$(e "$name_cfg" | shyaml get-value)") ;; *) err "Unsupported type '$vtype' of '$key' in $domain's zone definition." exit 1 ;; esac values_expanded=() for value in "${values[@]}"; do case "$key" in ns|name) if ! [[ "$value" =~ ^[a-z0-9_\$\(\),.-]+$ ]]; then err "Invalid value '$value' in ${domain}'s $key records for '$name'." return 1 fi ## Append in values_expanded bind:parse:expand-in "$value" values_expanded "$vars_cfg" || return 1 ;; mx) ## space is allowed (actually mandatory) if ! [[ "$value" =~ ^[a-z0-9_\$\(\),.\ -]+$ ]]; then err "Invalid value '$value' in ${domain}'s $key records for '$name'." return 1 fi ## Append in values_expanded bind:parse:expand-in "$value" values_expanded "$vars_cfg" || return 1 ;; spf|txt) ## No expansion for them values_expanded+=("\"$value\"") ;; esac done for name in "${names[@]}"; do [ "$name" == "_" ] && name="@" for value in "${values_expanded[@]}"; do rkey="$key" if [ "$key" == "name" ]; then if [[ "$value" =~ ^([0-9]{1,3}\.){3,3}[0-9]{1,3}$ ]]; then rkey="a" elif [[ "$value" =~ ^([a-z0-9-]+\.?)+$ ]]; then rkey="cname" elif [ -z "$value" ]; then err "Empty value for 'name' '$name', was '${values[@]}' before evaluation" echo " in '$domain' zone definition." >&2 return 1 else err "Unrecognised value for 'name': '$value'." echo " in '$domain' zone definition." >&2 return 1 fi fi if [ -z "$value" ]; then err "Empty value for 'name' '$name'." echo " in '$domain' zone definition." >&2 return 1 fi decls[$rkey]+=$(printf "%-15s %-12s IN %s %s" "$name" "$ttl" "${rkey^^}" "$value")$'\n' done done done < <(e "$subcfg" | shyaml key-values-0) ;; *) err "Unknown keyword '$key' in '$domain' zone definition." return 1 esac done < <(e "$zone_cfg" | shyaml key-values-0) for k in vars soa ns mx a cname spf txt; do [ -z "${decls[$k]}" ] && continue if [ "$k" != "vars" ]; then echo echo echo ";; $k" echo fi echo "${decls[$k]}" | grep -v "^$" | { case "$k" in a|ns|cname|spf|txt|vars) sort ;; soa|mx) cat ;; esac } done | tee "$cache_file" }