Prepare KarlDNS production deployment

This commit is contained in:
StasonJatham committed 2026-05-21 17:23:08 +02:00
1 parent 5a90bd9d87
commit ce5b4e0ff2
101 files changed
+3364 -2018

No files matched your search

+13 -6
View File
@@ -6,15 +6,18 @@ import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT / "router_dyndns" / "assets" / "bootstrap-subset.css"
DEFAULT_OUTPUT = ROOT / "karldns" / "assets" / "bootstrap-subset.css"
SCAN_PATHS = [
ROOT / "router_dyndns" / "web",
ROOT / "router_dyndns" / "routers",
ROOT / "karldns" / "web",
ROOT / "karldns" / "routers",
]
CSS = r"""
:root{--bs-body-color:#000;--bs-body-bg:#fff;--bs-border-color:#e5e5e5;--bs-secondary-color:#737373}[data-bs-theme="dark"]{--bs-body-color:#f5f5f5;--bs-body-bg:#0f0f0f;--bs-border-color:rgba(255,255,255,.16);--bs-secondary-color:#a3a3a3}html{-webkit-text-size-adjust:100%}body{margin:0}button,input,select{margin:0;font:inherit}button,select{text-transform:none}button:not(:disabled),[type="button"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}a{color:inherit}img,svg{vertical-align:middle}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}pre{margin:0}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:12px;padding-left:12px}.row{display:flex;flex-wrap:wrap;margin-right:calc(-.5 * var(--bs-gutter-x,0));margin-left:calc(-.5 * var(--bs-gutter-x,0));margin-top:calc(-1 * var(--bs-gutter-y,0))}.row>*{width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x,0) * .5);padding-left:calc(var(--bs-gutter-x,0) * .5);margin-top:var(--bs-gutter-y,0)}.col-6{flex:0 0 auto;width:50%}.col-12{flex:0 0 auto;width:100%}@media (min-width:768px){.col-md-6{flex:0 0 auto;width:50%}.align-items-md-center{align-items:center!important}.flex-md-row{flex-direction:row!important}}@media (min-width:992px){.col-lg-3{flex:0 0 auto;width:25%}.col-lg-5{flex:0 0 auto;width:41.666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.333333%}.flex-lg-row{flex-direction:row!important}}@media (min-width:1200px){.col-xl-3{flex:0 0 auto;width:25%}}.g-3{--bs-gutter-x:16px;--bs-gutter-y:16px}.g-4{--bs-gutter-x:24px;--bs-gutter-y:24px}.d-flex{display:flex!important}.d-grid{display:grid!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-wrap{flex-wrap:wrap!important}.flex-shrink-0{flex-shrink:0!important}.align-items-center{align-items:center!important}.align-items-start{align-items:flex-start!important}.align-items-stretch{align-items:stretch!important}.align-self-start{align-self:flex-start!important}.justify-content-between{justify-content:space-between!important}.justify-content-center{justify-content:center!important}.justify-content-end{justify-content:flex-end!important}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.gap-sm-3{gap:16px!important}}.gap-2{gap:8px!important}.gap-3{gap:16px!important}.gap-4{gap:24px!important}.m-0{margin:0!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.ms-auto{margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-3{margin-top:16px!important;margin-bottom:16px!important}.mt-1{margin-top:4px!important}.mt-2{margin-top:8px!important}.mt-3{margin-top:16px!important}.mt-4{margin-top:24px!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:4px!important}.mb-2{margin-bottom:8px!important}.mb-3{margin-bottom:16px!important}.mb-4{margin-bottom:24px!important}.p-0{padding:0!important}.p-4{padding:24px!important}.px-0{padding-right:0!important;padding-left:0!important}.px-2{padding-right:8px!important;padding-left:8px!important}.px-3{padding-right:16px!important;padding-left:16px!important}.py-2{padding-top:8px!important;padding-bottom:8px!important}.w-100{width:100%!important}.h-100{height:100%!important}.text-center{text-align:center!important}.text-start{text-align:left!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-secondary{color:var(--bs-secondary-color)!important}.text-danger{color:var(--rp-danger)!important}.small{font-size:.875em!important}.h5{font-size:1.25rem}.opacity-75{opacity:.75!important}.rounded{border-radius:12px!important}.rounded-0{border-radius:0!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:9999px!important}.border{border:1px solid var(--bs-border-color)!important}.border-top{border-top:1px solid var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-end-0{border-right:0!important}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:5px;padding-bottom:5px;margin-right:16px;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-text{padding-top:8px;padding-bottom:8px}.nav-link{display:block;padding:8px;text-decoration:none}.btn{--bs-btn-color:#000;--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-btn-color);--bs-btn-hover-bg:var(--bs-btn-bg);--bs-btn-hover-border-color:var(--bs-btn-border-color);--bs-btn-active-color:var(--bs-btn-hover-color);--bs-btn-active-bg:var(--bs-btn-hover-bg);--bs-btn-active-border-color:var(--bs-btn-hover-border-color);color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border:1px solid var(--bLine truncated
""".strip()
""".strip().replace(
".col-lg-3{flex:0 0 auto;width:25%}",
".col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.333333%}",
)
SUPPORTED_DYNAMIC_CLASSES = {"active", "disabled", "page-item", "page-link"}
@@ -44,14 +47,18 @@ def parse_args() -> argparse.Namespace:
def main() -> int:
args = parse_args()
output = args.output if args.output.is_absolute() else ROOT / args.output
app_styles = (ROOT / "router_dyndns" / "web" / "styles.py").read_text()
app_styles = (ROOT / "karldns" / "web" / "styles.py").read_text()
supported = _class_names_from_css(CSS) | _class_names_from_css(app_styles) | SUPPORTED_DYNAMIC_CLASSES
unsupported = sorted(_class_names_from_templates() - supported)
if unsupported:
unsupported_text = "\n".join(f"- {class_name}" for class_name in unsupported)
raise SystemExit(f"Unsupported Bootstrap-style classes in templates:\n{unsupported_text}")
output.write_text(CSS + "\n")
print(f"Wrote {output.relative_to(ROOT)} ({output.stat().st_size} bytes)")
try:
display_path = output.relative_to(ROOT)
except ValueError:
display_path = output
print(f"Wrote {display_path} ({output.stat().st_size} bytes)")
return 0
+71 -9
View File
@@ -7,11 +7,13 @@ KARLDNS_INSTALL_MODE="${KARLDNS_INSTALL_MODE:-auto}"
KARLDNS_UPDATE_URL="${KARLDNS_UPDATE_URL:-}"
KARLDNS_HOSTNAME="${KARLDNS_HOSTNAME:-}"
KARLDNS_MANAGEMENT_URL="${KARLDNS_MANAGEMENT_URL:-}"
KARLDNS_ACTIVATION_URL="${KARLDNS_ACTIVATION_URL:-}"
KARLDNS_RAW_BASE_URL="${KARLDNS_RAW_BASE_URL:-https://raw.githubusercontent.com/StasonJatham/karldns.de/main/scripts}"
KARLDNS_IPV4="${KARLDNS_IPV4:-auto}"
KARLDNS_IPV6="${KARLDNS_IPV6:-auto}"
KARLDNS_SKIP_CRON="${KARLDNS_SKIP_CRON:-0}"
KARLDNS_SKIP_FIRST_UPDATE="${KARLDNS_SKIP_FIRST_UPDATE:-0}"
KARLDNS_REUSED_CONFIG=0
usage() {
cat <<'EOF'
@@ -28,6 +30,7 @@ Options:
--update-url URL Existing private update URL
--hostname NAME Hostname to show after install
--dashboard-url URL Private dashboard URL to show after install
--activation-url URL Private registration URL for pending hostnames
--ipv4 VALUE auto, off, or a static IPv4. Default: auto
--ipv6 VALUE auto, off, or a static IPv6. Default: auto
--no-cron Install files only, do not configure cron
@@ -49,6 +52,7 @@ while [ "$#" -gt 0 ]; do
--update-url) KARLDNS_UPDATE_URL="${2:?missing value for --update-url}"; shift 2 ;;
--hostname) KARLDNS_HOSTNAME="${2:?missing value for --hostname}"; shift 2 ;;
--dashboard-url|--management-url) KARLDNS_MANAGEMENT_URL="${2:?missing value for --dashboard-url}"; shift 2 ;;
--activation-url) KARLDNS_ACTIVATION_URL="${2:?missing value for --activation-url}"; shift 2 ;;
--ipv4) KARLDNS_IPV4="${2:?missing value for --ipv4}"; shift 2 ;;
--ipv6) KARLDNS_IPV6="${2:?missing value for --ipv6}"; shift 2 ;;
--no-cron) KARLDNS_SKIP_CRON=1; shift ;;
@@ -81,6 +85,7 @@ quote_sh() {
download_updater() {
target_path="$1"
tmp_path="$(mktemp)"
mkdir -p "$(dirname "$target_path")"
if ! "$curl_bin" -fsS "$KARLDNS_RAW_BASE_URL/karldns-update.sh" -o "$tmp_path"; then
rm -f "$tmp_path"
exit 1
@@ -89,6 +94,18 @@ download_updater() {
mv "$tmp_path" "$target_path"
}
load_existing_config() {
if [ -n "$KARLDNS_UPDATE_URL" ]; then
return
fi
config_file="$1"
if [ -r "$config_file" ]; then
# shellcheck disable=SC1090
. "$config_file"
KARLDNS_REUSED_CONFIG=1
fi
}
cron_schedule() {
if [ "$KARLDNS_INTERVAL_MINUTES" = "60" ]; then
printf '0 * * * *'
@@ -113,6 +130,44 @@ update_crontab() {
printf '%s\n' "$marker_end"
}
install_system_cron() {
schedule="$1"
command="$2"
if [ -d /etc/cron.d ]; then
cron_file="/etc/cron.d/karldns-updater"
cat > "$cron_file" <<EOF
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$schedule root $command
EOF
chmod 644 "$cron_file"
return
fi
if [ -d /etc/crontabs ]; then
cron_file="/etc/crontabs/root"
tmp_file="$(mktemp)"
update_crontab "$cron_file" "$schedule" "$command" > "$tmp_file"
cat "$tmp_file" > "$cron_file"
rm -f "$tmp_file"
chmod 600 "$cron_file" 2>/dev/null || true
return
fi
if command -v crontab >/dev/null 2>&1; then
cron_file="$(mktemp)"
crontab -l > "$cron_file" 2>/dev/null || true
update_crontab "$cron_file" "$schedule" "$command" > "$cron_file.new"
mv "$cron_file.new" "$cron_file"
crontab "$cron_file"
rm -f "$cron_file"
return
fi
echo "KarlDNS installer: cron is not available. Install cron or re-run with --no-cron." >&2
exit 1
}
write_config() {
config_file="$1"
cache_file="$2"
@@ -123,6 +178,7 @@ write_config() {
printf 'KARLDNS_UPDATE_URL=%s\n' "$(quote_sh "$KARLDNS_UPDATE_URL")"
printf 'KARLDNS_HOSTNAME=%s\n' "$(quote_sh "$KARLDNS_HOSTNAME")"
printf 'KARLDNS_MANAGEMENT_URL=%s\n' "$(quote_sh "$KARLDNS_MANAGEMENT_URL")"
printf 'KARLDNS_ACTIVATION_URL=%s\n' "$(quote_sh "$KARLDNS_ACTIVATION_URL")"
printf 'KARLDNS_CACHE_FILE=%s\n' "$(quote_sh "$cache_file")"
printf 'KARLDNS_LOCK_FILE=%s\n' "$(quote_sh "$lock_file")"
printf 'KARLDNS_IPV4=%s\n' "$(quote_sh "$KARLDNS_IPV4")"
@@ -161,7 +217,6 @@ install_system() {
cache_dir="/var/cache/karldns"
config_file="${config_dir}/updater.env"
updater_path="/usr/local/bin/karldns-update"
cron_file="/etc/cron.d/karldns-updater"
mkdir -p "$config_dir" "$cache_dir"
download_updater "$updater_path"
@@ -169,12 +224,7 @@ install_system() {
write_config "$config_file" "$cache_dir/last-ip" "/run/karldns-updater.lock"
if [ "$KARLDNS_SKIP_CRON" = "0" ]; then
cat > "$cron_file" <<EOF
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$(cron_schedule) root KARLDNS_CONFIG=$(quote_sh "$config_file") $(quote_sh "$updater_path") >/dev/null 2>&1
EOF
chmod 644 "$cron_file"
install_system_cron "$(cron_schedule)" "KARLDNS_CONFIG=$(quote_sh "$config_file") $(quote_sh "$updater_path") >/dev/null 2>&1"
fi
}
@@ -206,12 +256,19 @@ if [ "$KARLDNS_INSTALL_MODE" = "system" ] && [ "$(id -u)" -ne 0 ]; then
exit 1
fi
if [ "$KARLDNS_INSTALL_MODE" = "system" ]; then
load_existing_config /etc/karldns/updater.env
else
load_existing_config "${XDG_CONFIG_HOME:-$HOME/.config}/karldns/updater.env"
fi
if [ -z "$KARLDNS_UPDATE_URL" ]; then
echo "KarlDNS: creating a new hostname at $KARLDNS_BASE_URL ..."
response="$("$curl_bin" -fsS --max-time 30 -H 'content-type: application/json' -d '{}' "$KARLDNS_BASE_URL/api/v1/hostnames/magic")"
response="$("$curl_bin" -fsS --max-time 30 -X POST "$KARLDNS_BASE_URL/api/v1/hostnames")"
KARLDNS_UPDATE_URL="$(printf '%s' "$response" | json_field update_url)"
KARLDNS_HOSTNAME="$(printf '%s' "$response" | json_field hostname)"
KARLDNS_MANAGEMENT_URL="$(printf '%s' "$response" | json_field management_url)"
KARLDNS_MANAGEMENT_URL="$(printf '%s' "$response" | json_field dashboard_url)"
KARLDNS_ACTIVATION_URL="$(printf '%s' "$response" | json_field registration_url)"
fi
if [ -z "$KARLDNS_UPDATE_URL" ]; then
@@ -222,6 +279,11 @@ case "$KARLDNS_UPDATE_URL" in
*"<ipaddr>"*|*"<ip6addr>"*) KARLDNS_UPDATE_URL="${KARLDNS_UPDATE_URL%%\?*}" ;;
esac
if [ -n "$KARLDNS_ACTIVATION_URL" ] && [ "$KARLDNS_REUSED_CONFIG" = "0" ]; then
echo "KarlDNS: registering hostname ..."
"$curl_bin" -fsS --max-time 30 -X POST "$KARLDNS_ACTIVATION_URL" >/dev/null
fi
if [ "$KARLDNS_INSTALL_MODE" = "system" ]; then
install_system
else
+2 -3
View File
@@ -13,7 +13,6 @@ KARLDNS_BASE_URL="${KARLDNS_BASE_URL:-https://karldns.de}"
KARLDNS_LOCK_FILE="${KARLDNS_LOCK_FILE:-/tmp/karldns-updater.lock}"
KARLDNS_CACHE_FILE="${KARLDNS_CACHE_FILE:-${XDG_CACHE_HOME:-$HOME/.cache}/karldns/last-ip}"
KARLDNS_CURL="${KARLDNS_CURL:-curl}"
KARLDNS_IP_URL="${KARLDNS_IP_URL:-$KARLDNS_BASE_URL/ip}"
KARLDNS_IPV4="${KARLDNS_IPV4:-auto}"
KARLDNS_IPV6="${KARLDNS_IPV6:-auto}"
case "$KARLDNS_UPDATE_URL" in
@@ -39,12 +38,12 @@ fi
ipv4=""
ipv6=""
if [ "$KARLDNS_IPV4" = "auto" ]; then
ipv4="$("$KARLDNS_CURL" -4 -fsS --max-time 15 "$KARLDNS_IP_URL" 2>/dev/null | tr -d '[:space:]' || true)"
ipv4=""
elif [ "$KARLDNS_IPV4" != "off" ]; then
ipv4="$KARLDNS_IPV4"
fi
if [ "$KARLDNS_IPV6" = "auto" ]; then
ipv6="$("$KARLDNS_CURL" -6 -fsS --max-time 15 "$KARLDNS_IP_URL" 2>/dev/null | tr -d '[:space:]' || true)"
ipv6=""
elif [ "$KARLDNS_IPV6" != "off" ]; then
ipv6="$KARLDNS_IPV6"
fi
+29
View File
@@ -48,6 +48,33 @@ remove_user() {
fi
}
remove_marked_crontab_file() {
cron_file="$1"
[ -f "$cron_file" ] || return 0
tmp_file="$(mktemp)"
awk '
$0 == "# karldns-update start" {skip = 1; next}
$0 == "# karldns-update end" {skip = 0; next}
skip != 1 {print}
' "$cron_file" > "$tmp_file"
cat "$tmp_file" > "$cron_file"
rm -f "$tmp_file"
}
remove_marked_root_crontab() {
if ! command -v crontab >/dev/null 2>&1; then
return 0
fi
tmp_file="$(mktemp)"
crontab -l 2>/dev/null | awk '
$0 == "# karldns-update start" {skip = 1; next}
$0 == "# karldns-update end" {skip = 0; next}
skip != 1 {print}
' > "$tmp_file" || true
crontab "$tmp_file"
rm -f "$tmp_file"
}
remove_system() {
if [ "$(id -u)" -ne 0 ]; then
echo "System uninstall needs root. Re-run with sudo or set --mode user." >&2
@@ -55,6 +82,8 @@ remove_system() {
fi
rm -f /etc/cron.d/karldns-updater /usr/local/bin/karldns-update
remove_marked_crontab_file /etc/crontabs/root
remove_marked_root_crontab
if [ "$KARLDNS_DELETE_CONFIG" = "1" ]; then
rm -rf /etc/karldns /var/cache/karldns
fi