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.
485 lines
16 KiB
485 lines
16 KiB
#!/bin/bash
|
|
|
|
usage() {
|
|
PREFIX="miaou:usage" echo '<init>'
|
|
exit 0
|
|
}
|
|
|
|
yqm() {
|
|
#read only
|
|
yq "$1" "$EXPANDED_CONF"
|
|
}
|
|
|
|
yqmi() {
|
|
# for update
|
|
yq "$1" "$EXPANDED_CONF" -i
|
|
}
|
|
|
|
yqmt() {
|
|
# tabular
|
|
yq "$1" "$EXPANDED_CONF" -o t
|
|
}
|
|
|
|
compute_fqdn_middlepart() {
|
|
case "$1" in
|
|
prod)
|
|
local fqdn_middlepart="."
|
|
;;
|
|
beta)
|
|
local fqdn_middlepart=".beta."
|
|
;;
|
|
dev)
|
|
local fqdn_middlepart=".dev."
|
|
;;
|
|
*)
|
|
echowarn "unknown target <${target}>, please fix with correct value from {prod, beta, dev} and try again..."
|
|
exit 1
|
|
;;
|
|
esac
|
|
builtin echo "$fqdn_middlepart"
|
|
}
|
|
|
|
# archive_conf(FILE)
|
|
# save patch in archived folder of current file
|
|
function archive_conf() {
|
|
PREFIX="miaou:conf:archive"
|
|
|
|
file="$1"
|
|
filename=$(basename "$file")
|
|
mkdir -p "$MIAOU_CONFIGDIR/archived/$filename"
|
|
previous="$MIAOU_CONFIGDIR/archived/$filename/previous"
|
|
|
|
# shellcheck disable=SC2012
|
|
latest_patch=$(ls -1tr "$MIAOU_CONFIGDIR/archived/$filename/" | tail -n1)
|
|
|
|
if [[ -z "$latest_patch" ]]; then
|
|
echo -n "archiving first file <$file> ..."
|
|
cp "$file" "$previous"
|
|
PREFIX="" echoinfo OK
|
|
elif [[ "$file" -nt "$latest_patch" ]]; then
|
|
patchname="$MIAOU_CONFIGDIR/archived/$filename/$(date +%F_%T)"
|
|
if ! diff "$previous" "$file" >"$patchname"; then
|
|
echo -n "archiving patch <$patchname> ..."
|
|
cp "$file" "$previous"
|
|
PREFIX="" echoinfo OK
|
|
else
|
|
rm "$patchname"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
function archive_allconf() {
|
|
mkdir -p "$MIAOU_CONFIGDIR"
|
|
archive_conf "$CONF"
|
|
archive_conf "$DEFAULTS"
|
|
}
|
|
|
|
function check_expand_conf() {
|
|
if ! "$FORCE" && [ -f "$EXPANDED_CONF" ] && [ "$EXPANDED_CONF" -nt "$CONF" ] && [ "$EXPANDED_CONF" -nt "$DEFAULTS" ]; then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
function expand_conf() {
|
|
PREFIX="miaou:conf"
|
|
|
|
if [[ -f "$EXPANDED_CONF" ]]; then
|
|
current_target=$(grep -Es "^target:" /etc/miaou/defaults.yaml | cut -d ' ' -f2)
|
|
previous_target=$(grep -Es "^target:" "$EXPANDED_CONF" | cut -d ' ' -f2)
|
|
[[ "$current_target" != "$previous_target" ]] && echoerr "TARGET <$previous_target> mismatched <$current_target>" && exit 101
|
|
fi
|
|
|
|
# initialize expanded conf by merging default
|
|
# shellcheck disable=SC2016
|
|
yq eval-all '. as $item ireduce ({}; . * $item )' "$CONF" "$DEFAULTS" >"$EXPANDED_CONF"
|
|
|
|
# append unique container unless overridden
|
|
mapfile -t services_app_only < <(yqmt '.services.[].[] | has("container") | select ( . == false) | [(parent|key)+" " +key]')
|
|
|
|
for i in "${services_app_only[@]}"; do
|
|
read -r -a item <<<"$i"
|
|
domain=${item[0]}
|
|
subdomain=${item[1]}
|
|
app=$(yqm ".services.\"$domain\".\"$subdomain\".app")
|
|
container=$(get_container_for_domain_subdomain_app "$domain" "$subdomain" "$app")
|
|
yqmi ".services.\"$domain\".\"$subdomain\".container=\"$container\""
|
|
done
|
|
|
|
# append enabled=true unless overridden
|
|
mapfile -t services_app_only < <(yqmt '.services.[].[] | has("enabled") | select ( . == false) | [(parent|key)+" " +key] | unique ')
|
|
# echo "found <${#services_app_only[@]}> enabled services"
|
|
for i in "${services_app_only[@]}"; do
|
|
read -r -a item <<<"$i"
|
|
domain=${item[0]}
|
|
subdomain=${item[1]}
|
|
yqmi ".services.\"$domain\".\"$subdomain\".enabled=true"
|
|
done
|
|
|
|
# compute fqdn
|
|
target=$(yqm '.target')
|
|
fqdn_middlepart=$(compute_fqdn_middlepart "$target")
|
|
|
|
# write fqdn_middlepart
|
|
yqmi ".expanded.fqdn_middlepart = \"$fqdn_middlepart\""
|
|
|
|
# add monitored.containers section
|
|
yqmi '.expanded.monitored.containers = ([ .services[] | to_entries | .[] | .value | select (.enabled == true ) | .container ] | unique)'
|
|
|
|
# add monitored.hosts section
|
|
yqmi '.expanded.monitored.hosts = [( .services[][] | select (.enabled == true ) | {"domain": ( parent | key ), "subdomain": key, "fqdn": key + (parent | parent | parent | .expanded.fqdn_middlepart) + ( parent | key ), "container":.container, "port":.port, "app":.app })]'
|
|
|
|
# add services section
|
|
if [[ ${#services_app_only[@]} -gt 0 ]]; then
|
|
yqmi '.expanded.services = [( .services[][] | select (.enabled == true ) | {"domain": ( parent | key ), "subdomain": key, "fqdn": key + (parent | parent | parent | .expanded.fqdn_middlepart) + ( parent | key ), "container":.container, "port":.port, "app":.app, "name": .name // ""})]'
|
|
else
|
|
yqmi '.expanded.services = []'
|
|
fi
|
|
|
|
# add firewall section, bridge_subnet + mail_passthrough if any
|
|
bridge_subnet=$(lxc network get lxdbr0 ipv4.address)
|
|
yqmi ".firewall.bridge_subnet = \"$bridge_subnet\""
|
|
|
|
container_mail_passthrough=$(yqm ".firewall.container_mail_passthrough")
|
|
}
|
|
|
|
function build_routes() {
|
|
PREFIX="miaou:routes"
|
|
|
|
mapfile -t fqdns < <(yqm '.expanded.services[].fqdn')
|
|
echo "found <${#fqdns[@]}> fqdn"
|
|
raw_dnsmasq=''
|
|
for i in "${fqdns[@]}"; do
|
|
raw_dnsmasq+="address=/$i/$DMZ_IP\\n"
|
|
# append domains to conf
|
|
echo "re-routing any connection from <$i> to internal container <$DMZ_CONTAINER.lxd>"
|
|
done
|
|
|
|
builtin echo -e "$raw_dnsmasq" | lxc network set $BRIDGE raw.dnsmasq -
|
|
}
|
|
|
|
function build_dmz_reverseproxy() {
|
|
PREFIX="miaou:build:dmz"
|
|
echo -n "building configuration for nginx ... "
|
|
mkdir -p "$MIAOU_CONFIGDIR/nginx"
|
|
tera -t "$MIAOU_BASEDIR/templates/nginx/_default.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nginx/_default" &>/dev/null
|
|
|
|
tera -t "$MIAOU_BASEDIR/templates/nginx/hosts.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nginx/hosts" &>/dev/null
|
|
PREFIX="" echo OK
|
|
|
|
echo -n "pushing configuration to <$DMZ_CONTAINER> ... "
|
|
for f in "$MIAOU_CONFIGDIR"/nginx/*; do
|
|
lxc file push --uid=0 --gid=0 "$f" "$DMZ_CONTAINER/etc/nginx/sites-available/" &>/dev/null
|
|
done
|
|
PREFIX="" echo OK
|
|
|
|
cat <<EOF | PREFIX="miaou:build:dmz" lxc_exec "$DMZ_CONTAINER"
|
|
cd /etc/nginx/sites-enabled/
|
|
for i in ../sites-available/*; do
|
|
# echo dmz: enabling... \$i
|
|
ln -sf \$i
|
|
done
|
|
nginx -tq
|
|
systemctl restart nginx
|
|
EOF
|
|
echo "nginx reloaded successfully!"
|
|
}
|
|
|
|
function monit_show() {
|
|
PREFIX="monit:show"
|
|
: $PREFIX
|
|
|
|
readarray -t hosts < <(yqmt '.expanded.monitored.hosts[] | [ .container, .port, .fqdn, .app ]')
|
|
echo "================="
|
|
echo "${#hosts[@]} available hosts"
|
|
echo "================="
|
|
|
|
for host in "${hosts[@]}"; do
|
|
read -r -a item <<<"$host"
|
|
container=${item[0]}
|
|
port=${item[1]}
|
|
fqdn=${item[2]}
|
|
app=${item[3]}
|
|
|
|
[[ -n ${PREFIX:-} ]] && printf "${DARK}%25.25s${NC} " "${PREFIX}"
|
|
|
|
if curl -m $MAX_WAIT -I -4so /dev/null "http://$container:$port"; then
|
|
builtin echo -ne "${GREEN}✔${NC}"
|
|
else
|
|
builtin echo -ne "${RED}✘${NC}"
|
|
fi
|
|
printf "\t%10.10s\thttps://%-40s\thttp://%s\n" "$app" "$fqdn" "$container:$port"
|
|
done
|
|
|
|
}
|
|
|
|
function build_monit() {
|
|
|
|
# test whether monitored items actually run safely
|
|
PREFIX="monit:build"
|
|
echo -n "testing monitored hosts ..."
|
|
readarray -t hosts < <(yqmt '.expanded.monitored.hosts[] | [ .container, .port, .fqdn ]')
|
|
for host in "${hosts[@]}"; do
|
|
read -r -a item <<<"$host"
|
|
container=${item[0]}
|
|
port=${item[1]}
|
|
fqdn=${item[2]}
|
|
|
|
if ! (lxc exec "$container" -- ss -tln | grep -q "\(0.0.0.0\|*\):$port"); then
|
|
echoerr
|
|
echoerr "no HTTP server responds on <$container.lxd:$port>"
|
|
echoerr "please review configuration <miaou.yaml> for fqdn: $fqdn"
|
|
exit 2
|
|
fi
|
|
|
|
if ! curl_check_unsecure "https://$fqdn"; then
|
|
echoerr
|
|
echoerr "DMZ does not seem to dispatch <https://$fqdn> please review DMZ Nginx proxy"
|
|
exit 3
|
|
elif [[ "$target" != 'dev' ]] && ! curl_check "https://$fqdn"; then
|
|
PREFIX="" echo
|
|
echowarn "T=$target missing valid certificate for fqdn <https://$fqdn> please review DMZ certbot"
|
|
fi
|
|
|
|
done
|
|
PREFIX="" echo OK
|
|
|
|
# templates for monit
|
|
echo -n "copying templates for monit ..."
|
|
mkdir -p "$MIAOU_CONFIGDIR/monit"
|
|
tera -t "$MIAOU_BASEDIR/templates/monit/containers.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/monit/containers" >/dev/null
|
|
tera -t "$MIAOU_BASEDIR/templates/monit/hosts.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/monit/hosts" >/dev/null
|
|
PREFIX="" echo OK
|
|
}
|
|
|
|
# count_service_for_container(container: string)
|
|
# returns how many services run inside container according to expanded conf
|
|
function count_service_for_container() {
|
|
container_mail_passthrough="$1"
|
|
count=$(yqm ".expanded.services.[] | select(.container == \"$container_mail_passthrough\") | .fqdn" | wc -l)
|
|
builtin echo "$count"
|
|
}
|
|
|
|
function build_nftables() {
|
|
PREFIX="miaou:nftables:build"
|
|
mkdir -p "$MIAOU_CONFIGDIR/nftables.rules.d"
|
|
|
|
container_mail_passthrough=$(yqm '.firewall.container_mail_passthrough')
|
|
if [[ "$container_mail_passthrough" != null ]]; then
|
|
ip_mail_passthrough=$(lxc list "$container_mail_passthrough" -c4 -f csv | grep eth0 | cut -d ' ' -f1)
|
|
[[ -z "$ip_mail_passthrough" ]] && echoerr "container <$container_mail_passthrough> passthrough unknown ip!" && exit 55
|
|
echo "passthrough=$container_mail_passthrough/$ip_mail_passthrough"
|
|
|
|
count=$(count_service_for_container "$container_mail_passthrough")
|
|
[[ $count == 0 ]] && echowarn "no service detected => no passthrough, no change!"
|
|
[[ $count -gt 1 ]] && echoerr "count <$count> services detected on container <$container_mail_passthrough>, please disable some and leave only one service for safety!!!" && exit 56
|
|
|
|
ip_mail_passthrough=$ip_mail_passthrough tera -e --env-key env -t "$MIAOU_BASEDIR/templates/nftables/lxd.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" &>/dev/null
|
|
else
|
|
echo "no container passthrough"
|
|
tera -t "$MIAOU_BASEDIR/templates/nftables/lxd.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" &>/dev/null
|
|
fi
|
|
|
|
if ! diff -q "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" /etc/nftables.rules.d/lxd.table &>/dev/null; then
|
|
sudo_required "reloading nftables"
|
|
echo -n "reloading nftables..."
|
|
sudo cp "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" /etc/nftables.rules.d/lxd.table
|
|
sudo systemctl reload nftables
|
|
PREFIX="" echo DONE
|
|
fi
|
|
}
|
|
|
|
# check whether http server responds 200 OK, required <url>, ie: http://example.com:8001, https://example.com
|
|
function curl_check() {
|
|
arg1_required "$@"
|
|
# echo "curl $1"
|
|
curl -m $MAX_WAIT -sLI4 "$1" | grep -q "^HTTP.* 200"
|
|
}
|
|
|
|
# check whether https server responds 200 OK, even unsecured certificate (auto-signed in mode DEV)
|
|
function curl_check_unsecure() {
|
|
arg1_required "$@"
|
|
curl -m $MAX_WAIT -skLI4 "$1" | grep -q "^HTTP.* 200"
|
|
}
|
|
|
|
function get_dmz_ip() {
|
|
if ! container_running "$DMZ_CONTAINER"; then
|
|
echowarn "Container running dmz <$DMZ_CONTAINER> seems down"
|
|
echoerr "please \`lxc start $DMZ_CONTAINER\` or initialize first!"
|
|
exit 1
|
|
fi
|
|
|
|
dmz_ip=$(host "$DMZ_CONTAINER.lxd" | cut -d ' ' -f4)
|
|
if ! valid_ipv4 "$dmz_ip"; then
|
|
echowarn "dmz seems up but no valid ip <$dmz_ip> found!"
|
|
echoerr "please fix this networking issue, then retry..."
|
|
exit 1
|
|
else
|
|
builtin echo "$dmz_ip"
|
|
fi
|
|
}
|
|
|
|
function fetch_container_of_type() {
|
|
local type="$1"
|
|
readarray -t dmzs < <(yqm ".containers.[].[] | select(.==\"$type\") | parent | key")
|
|
case ${#dmzs[@]} in
|
|
0) : ;;
|
|
1) builtin echo "${dmzs[0]}" ;;
|
|
*) for d in "${dmzs[@]}"; do
|
|
builtin echo "$d"
|
|
done ;;
|
|
esac
|
|
}
|
|
|
|
function get_container_for_domain_subdomain_app() {
|
|
local domain="$1"
|
|
local subdomain="$2"
|
|
local app="$3"
|
|
readarray -t containers < <(fetch_container_of_type "$app")
|
|
case ${#containers[@]} in
|
|
0) echoerr "no container of type <$app> found amongst containers for $subdomain.$domain\nHINT : Please, either :\n1. define at least one container for recipe <$app>\n2. remove all services related to recipe <$app>" && exit 1 ;;
|
|
1) builtin echo "${containers[0]}" ;;
|
|
*)
|
|
for d in "${containers[@]}"; do
|
|
echowarn "container of type $app found in <$d>"
|
|
done
|
|
echoerr "multiple containers (${#containers[@]}) provided same app <$app>, therefore container is mandatory alongside $subdomain.$domain" && exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
function get_unique_container_dmz() {
|
|
readarray -t containers < <(fetch_container_of_type "dmz")
|
|
case ${#containers[@]} in
|
|
0) echoerr "no container of type <dmz> found amongst containers" && exit 1 ;;
|
|
1) builtin echo "${containers[0]}" ;;
|
|
|
|
*)
|
|
for d in "${containers[@]}"; do
|
|
echowarn "container of type dmz found in <$d>"
|
|
done
|
|
echoerr "multiple dmz (${#containers[@]}) are not allowed, please select only one " && exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
function prepare_dmz_container() {
|
|
"$MIAOU_BASEDIR"/recipes/dmz/install.sh "$DMZ_CONTAINER"
|
|
}
|
|
|
|
function check_resolv_conf() {
|
|
local bridge_gw resolver
|
|
|
|
bridge_gw=$(lxc network get lxdbr0 ipv4.address | cut -d'/' -f1)
|
|
PREFIX="resolver:check" echo "container bridge is <$bridge_gw>"
|
|
|
|
resolver=$(grep nameserver /etc/resolv.conf | head -n1 | cut -d ' ' -f2)
|
|
PREFIX="resolver:check" echo "first resolver is <$resolver>"
|
|
|
|
[[ "$bridge_gw" != "$resolver" ]] && return 21
|
|
return 0
|
|
}
|
|
|
|
function prepare_containers() {
|
|
PREFIX="miaou:prepare"
|
|
readarray -t containers < <(yqmt ".containers.[] | [ key, .[] ] ")
|
|
for i in "${containers[@]}"; do
|
|
read -r -a item <<<"$i"
|
|
container=${item[0]}
|
|
for ((j = 1; j < ${#item[@]}; j++)); do
|
|
service="${item[$j]}"
|
|
recipe_install="$MIAOU_BASEDIR/recipes/$service/install.sh"
|
|
if [[ -f "$recipe_install" ]]; then
|
|
echo "install [$service] onto container <$container>"
|
|
"$recipe_install" "$container"
|
|
else
|
|
echoerr "FAILURE, for container <$container>, install recipe [$service] not found!"
|
|
echoerr "please review configuration, mismatch recipe name maybe?"
|
|
exit 50
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
function build_services() {
|
|
PREFIX="miaou:build:services"
|
|
echo "building services..."
|
|
readarray -t services < <(yqmt '.expanded.services[] | [ .[] ]')
|
|
for i in "${services[@]}"; do
|
|
|
|
read -r -a item <<<"$i"
|
|
fqdn=${item[2]}
|
|
container=${item[3]}
|
|
port=${item[4]}
|
|
app=${item[5]}
|
|
name=${item[6]:-}
|
|
|
|
domain=${item[0]}
|
|
subdomain=${item[1]}
|
|
data=$(yqm ".services.\"$domain\".\"$subdomain\".data|explode(.)")
|
|
|
|
recipe="$MIAOU_BASEDIR/recipes/$app/crud.sh"
|
|
if [[ -f "$recipe" ]]; then
|
|
echo "read [$app:$name] onto container <$container>"
|
|
command="env MIAOU_BASEDIR=$MIAOU_BASEDIR MIAOU_CONFIGDIR=$MIAOU_CONFIGDIR $recipe --port $port --container $container --name $name --fqdn $fqdn"
|
|
[[ "$data" != "null" ]] && command+=" --data \"$data\""
|
|
if ! eval "$command -r" >/dev/null; then
|
|
echoinfo "CREATE RECIPE"
|
|
eval "$command -c"
|
|
echoinfo "CREATE RECIPE: OK"
|
|
fi
|
|
else
|
|
echowarn "for container <$container>, crud recipe [$app] not found!"
|
|
fi
|
|
done
|
|
}
|
|
|
|
### MAIN
|
|
|
|
. "$MIAOU_BASEDIR/lib/init.sh"
|
|
|
|
readonly CONF="/etc/miaou/miaou.yaml"
|
|
readonly DEFAULTS="/etc/miaou/defaults.yaml"
|
|
readonly EXPANDED_CONF="$MIAOU_CONFIGDIR/miaou.expanded.yaml"
|
|
readonly BRIDGE="lxdbr0"
|
|
readonly MAX_WAIT=3 # timeout in seconds
|
|
|
|
# shellcheck disable=SC2034
|
|
declare -a options=("$@")
|
|
|
|
FORCE=false
|
|
if containsElement options "-f" || containsElement options "--force"; then
|
|
FORCE=true
|
|
fi
|
|
|
|
if containsElement options "history"; then
|
|
echo "TODO: HISTORY"
|
|
exit 0
|
|
fi
|
|
|
|
if containsElement options "config"; then
|
|
editor /etc/miaou/miaou.yaml
|
|
if diff -q /etc/miaou/miaou.yaml "$HOME"/.config/miaou/archived/miaou.yaml/previous; then
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
if check_expand_conf; then
|
|
archive_allconf
|
|
expand_conf
|
|
check_resolv_conf
|
|
build_nftables
|
|
prepare_containers
|
|
|
|
DMZ_CONTAINER=$(get_unique_container_dmz)
|
|
readonly DMZ_CONTAINER
|
|
build_services
|
|
|
|
DMZ_IP=$(get_dmz_ip)
|
|
readonly DMZ_IP
|
|
build_dmz_reverseproxy
|
|
|
|
build_routes
|
|
build_monit
|
|
|
|
fi
|
|
monit_show
|