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.
 
 

331 lines
13 KiB

# -*- 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 <<EOF
zone "${domain}" {
type master;
notify yes;
file "${zone_file}";
};
EOF
}
bind:check:is_time() {
local label="$1" value="$2"
if ! [[ "$value" =~ ^[0-9smhdw]+$ ]]; then
err "Invalid value for $label: '$value'." >&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"
}