mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 18:34:31 +00:00
584 lines
17 KiB
Bash
584 lines
17 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# init-prole-dns.sh
|
|
# Sync prole.org host records into Samba AD DNS using samba-tool.
|
|
#
|
|
# Host source:
|
|
# 1) Postgres (preferred when available):
|
|
# SELECT host_name, ip_address FROM prole.hosts;
|
|
# 2) Fallback file:
|
|
# /etc/prole-dns-sync/hosts.txt (one hostname per line; FQDN or short)
|
|
#
|
|
# Commands:
|
|
# start - run sync now + install hourly cron job
|
|
# stop - remove cron job
|
|
# status - show last run time, last exit, and journal messages
|
|
# sync - run one sync pass
|
|
# flush - clear local state (forces "unknown" status); does NOT delete DNS records
|
|
# list|show|details - show configured hostnames and detected DC info
|
|
#
|
|
# Options:
|
|
# -db=HOST:PORT/DBNAME default: localhost:5432/prole-db
|
|
#
|
|
# Environment overrides (optional):
|
|
# AD_DNS_ZONE default: prole.org
|
|
# AD_DNS_SERVER default: auto-detect (hostname -f)
|
|
# AD_DNS_SERVER_IP default: auto-detect via getent hosts
|
|
# PUBLIC_DNS_SERVER default: first NS of prole.org from system resolv OR "ns1.name.com"
|
|
# DRY_RUN if set to 1, prints actions without changing AD DNS
|
|
|
|
PROG="init_prole_dns"
|
|
STATE_DIR="/var/lib/${PROG}"
|
|
STATE_FILE="${STATE_DIR}/state.env"
|
|
CRON_FILE="/etc/cron.d/${PROG}"
|
|
CONF_DIR="/etc/prole-dns-sync"
|
|
HOSTS_FILE="${CONF_DIR}/hosts.txt"
|
|
LOG_TAG="${PROG}"
|
|
|
|
AD_DNS_ZONE="${AD_DNS_ZONE:-prole.org}"
|
|
DRY_RUN="${DRY_RUN:-0}"
|
|
|
|
DEFAULT_DB_ENDPOINT="localhost:5432/prole-db"
|
|
DB_ENDPOINT="$DEFAULT_DB_ENDPOINT"
|
|
|
|
mkdir -p "$STATE_DIR" "$CONF_DIR"
|
|
chmod 700 "$STATE_DIR"
|
|
|
|
log() {
|
|
local msg="$*"
|
|
logger -t "$LOG_TAG" -- "$msg" 2>/dev/null || true
|
|
echo "$msg"
|
|
}
|
|
|
|
die() {
|
|
log "ERROR: $*"
|
|
exit 1
|
|
}
|
|
|
|
need_root() {
|
|
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Must run as root."
|
|
}
|
|
|
|
have_cmd() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
detect_dc_fqdn() {
|
|
local fqdn
|
|
fqdn="$(hostname -f 2>/dev/null || true)"
|
|
[[ -n "$fqdn" ]] || die "Unable to detect DC FQDN (hostname -f failed)."
|
|
echo "$fqdn"
|
|
}
|
|
|
|
detect_dc_ip() {
|
|
local dc_fqdn="$1"
|
|
local ip
|
|
ip="$(getent ahostsv4 "$dc_fqdn" 2>/dev/null | awk '{print $1; exit}' || true)"
|
|
[[ -n "$ip" ]] || die "Unable to detect DC IP for $dc_fqdn (getent ahostsv4 failed)."
|
|
echo "$ip"
|
|
}
|
|
|
|
detect_public_dns_server() {
|
|
local ns=""
|
|
if have_cmd dig; then
|
|
ns="$(dig +short NS "${AD_DNS_ZONE}" 2>/dev/null | head -n1 | sed 's/\.$//' || true)"
|
|
fi
|
|
[[ -n "$ns" ]] || ns="ns1.name.com"
|
|
echo "$ns"
|
|
}
|
|
|
|
# Parse DB endpoint of form host:port/dbname
|
|
parse_db_endpoint() {
|
|
local ep="$1"
|
|
local hostport="${ep%%/*}"
|
|
local dbname="${ep#*/}"
|
|
local host="${hostport%%:*}"
|
|
local port="${hostport#*:}"
|
|
|
|
if [[ "$hostport" == "$host" ]]; then
|
|
# no port specified
|
|
port="5432"
|
|
fi
|
|
[[ -n "$host" && -n "$port" && -n "$dbname" ]] || return 1
|
|
|
|
echo "$host|$port|$dbname"
|
|
}
|
|
|
|
psql_can_connect() {
|
|
have_cmd psql || return 1
|
|
local parsed
|
|
parsed="$(parse_db_endpoint "$DB_ENDPOINT")" || return 1
|
|
local host="${parsed%%|*}"; parsed="${parsed#*|}"
|
|
local port="${parsed%%|*}"; local db="${parsed##*|}"
|
|
|
|
# Allow interactive password prompting if needed:
|
|
# We DO NOT add -w (never prompt). We add a short statement timeout.
|
|
# If auth requires a password and none is cached, this command will prompt.
|
|
psql -h "$host" -p "$port" -d "$db" -v ON_ERROR_STOP=1 -At \
|
|
-c "SET statement_timeout='5s'; SELECT 1;" >/dev/null 2>&1
|
|
}
|
|
|
|
# Functions to check if a string is an IP address
|
|
is_ipv4() {
|
|
[[ $1 =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]
|
|
}
|
|
|
|
is_ipv6() {
|
|
[[ $1 == *:* ]]
|
|
}
|
|
|
|
is_ip() {
|
|
is_ipv4 "$1" || is_ipv6 "$1"
|
|
}
|
|
|
|
read_hosts_file() {
|
|
[[ -f "$HOSTS_FILE" ]] || die "Missing $HOSTS_FILE and DB not available. Create it with: hostname [TAB/SPACE] ip_or_target"
|
|
grep -vE '^\s*($|#)' "$HOSTS_FILE" | awk '{print $1 "|" $2}' || true
|
|
}
|
|
|
|
read_hosts_db() {
|
|
local parsed
|
|
parsed="$(parse_db_endpoint "$DB_ENDPOINT")" || die "Invalid -db endpoint '$DB_ENDPOINT' (expected host:port/dbname)"
|
|
local host="${parsed%%|*}"; parsed="${parsed#*|}"
|
|
local port="${parsed%%|*}"; local db="${parsed##*|}"
|
|
|
|
# This will prompt for password if required by libpq/pg_hba.
|
|
# Output format: host_name|ip_address
|
|
psql -h "$host" -p "$port" -d "$db" -v ON_ERROR_STOP=1 -At -F '|' \
|
|
-c "SELECT host_name, ip_address FROM prole.hosts ORDER BY host_name;"
|
|
}
|
|
|
|
normalize_host() {
|
|
local raw="$1"
|
|
raw="${raw%%#*}"
|
|
# Trim whitespace
|
|
raw="$(echo "$raw" | xargs)"
|
|
[[ -n "$raw" ]] || return 1
|
|
|
|
local fqdn="$raw"
|
|
# If it's a short name, append the zone
|
|
if [[ "$raw" != *.* ]]; then
|
|
fqdn="${raw}.${AD_DNS_ZONE}"
|
|
fi
|
|
|
|
# Validate it's within our zone
|
|
if [[ "$fqdn" != *".${AD_DNS_ZONE}" && "$fqdn" != "${AD_DNS_ZONE}" ]]; then
|
|
# We might be reading a line that has two columns, but normalize_host is called with only the first.
|
|
# If the user provided a FQDN outside the zone, it's an error for an A/CNAME record name in this script's context.
|
|
die "Host '$raw' is not within zone '${AD_DNS_ZONE}' (got fqdn '$fqdn')."
|
|
fi
|
|
|
|
local rel="${fqdn%.$AD_DNS_ZONE}"
|
|
rel="${rel%.}"
|
|
|
|
echo "$rel|$fqdn"
|
|
}
|
|
|
|
dig_records() {
|
|
local dns_server="$1"
|
|
local fqdn="$2"
|
|
local a_records aaaa_records
|
|
|
|
a_records="$(dig +short @"$dns_server" A "$fqdn" 2>/dev/null | tr '\n' ' ' | xargs || true)"
|
|
aaaa_records="$(dig +short @"$dns_server" AAAA "$fqdn" 2>/dev/null | tr '\n' ' ' | xargs || true)"
|
|
echo "$a_records|$aaaa_records"
|
|
}
|
|
|
|
samba_query_rr() {
|
|
local dc="$1" zone="$2" name="$3" rrtype="$4"
|
|
samba-tool dns query "$dc" "$zone" "$name" "$rrtype" --use-kerberos=required 2>/dev/null \
|
|
| awk '
|
|
BEGIN{inrec=0}
|
|
/Records:/ {inrec=1; next}
|
|
inrec && NF>=1 { print $0 }
|
|
' \
|
|
| tr '\n' ' ' | xargs || true
|
|
}
|
|
|
|
samba_add_rr() {
|
|
local dc="$1" zone="$2" name="$3" rrtype="$4" value="$5"
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
log "[DRY_RUN] samba-tool dns add $dc $zone $name $rrtype $value --use-kerberos=required"
|
|
return 0
|
|
fi
|
|
local out
|
|
if out=$(samba-tool dns add "$dc" "$zone" "$name" "$rrtype" "$value" --use-kerberos=required 2>&1); then
|
|
return 0
|
|
else
|
|
if [[ "$out" == *"WERR_DNS_ERROR_RECORD_ALREADY_EXISTS"* ]]; then
|
|
log "INFO: Record $name $rrtype $value exists"
|
|
return 0
|
|
fi
|
|
echo "$out" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
samba_update_rr() {
|
|
local dc="$1" zone="$2" name="$3" rrtype="$4" old="$5" new="$6"
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
log "[DRY_RUN] samba-tool dns update $dc $zone $name $rrtype $old $new --use-kerberos=required"
|
|
return 0
|
|
fi
|
|
local out
|
|
if out=$(samba-tool dns update "$dc" "$zone" "$name" "$rrtype" "$old" "$new" --use-kerberos=required 2>&1); then
|
|
return 0
|
|
else
|
|
if [[ "$out" == *"WERR_DNS_ERROR_RECORD_ALREADY_EXISTS"* ]]; then
|
|
log "INFO: Record $name $rrtype $new exists"
|
|
return 0
|
|
fi
|
|
echo "$out" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
sync_rr_set() {
|
|
local dc="$1" zone="$2" name_rel="$3" fqdn="$4" rrtype="$5" desired_values="$6"
|
|
[[ -n "$desired_values" ]] || return 0
|
|
|
|
local existing
|
|
existing="$(samba_query_rr "$dc" "$zone" "$name_rel" "$rrtype")"
|
|
|
|
for val in $desired_values; do
|
|
if [[ " $existing " == *" $val "* ]]; then
|
|
log "OK: $fqdn $rrtype already has $val"
|
|
else
|
|
local first_old
|
|
first_old="$(echo "$existing" | awk '{print $1}' || true)"
|
|
if [[ -n "$first_old" ]]; then
|
|
log "UPDATE: $fqdn $rrtype $first_old -> $val"
|
|
samba_update_rr "$dc" "$zone" "$name_rel" "$rrtype" "$first_old" "$val" || samba_add_rr "$dc" "$zone" "$name_rel" "$rrtype" "$val"
|
|
else
|
|
log "ADD: $fqdn $rrtype $val"
|
|
samba_add_rr "$dc" "$zone" "$name_rel" "$rrtype" "$val"
|
|
fi
|
|
fi
|
|
done
|
|
}
|
|
|
|
write_state() {
|
|
local key="$1" val="$2"
|
|
if [[ -f "$STATE_FILE" ]] && grep -qE "^${key}=" "$STATE_FILE"; then
|
|
sed -i "s|^${key}=.*|${key}=${val}|g" "$STATE_FILE"
|
|
else
|
|
echo "${key}=${val}" >> "$STATE_FILE"
|
|
fi
|
|
chmod 600 "$STATE_FILE"
|
|
}
|
|
|
|
load_state() {
|
|
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE" || true
|
|
}
|
|
|
|
install_cron() {
|
|
local script_path
|
|
script_path="$(readlink -f "$0")"
|
|
cat > "$CRON_FILE" <<EOF
|
|
# Managed by $PROG. Runs hourly.
|
|
SHELL=/bin/bash
|
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
0 * * * * root $script_path -db=$DB_ENDPOINT sync >/dev/null 2>&1
|
|
EOF
|
|
chmod 644 "$CRON_FILE"
|
|
log "Installed hourly cron job at $CRON_FILE"
|
|
}
|
|
|
|
remove_cron() {
|
|
if [[ -f "$CRON_FILE" ]]; then
|
|
rm -f "$CRON_FILE"
|
|
log "Removed cron job $CRON_FILE"
|
|
else
|
|
log "Cron job not present ($CRON_FILE)"
|
|
fi
|
|
}
|
|
|
|
choose_host_source() {
|
|
# Echo "db" if we can connect; otherwise "file"
|
|
if have_cmd psql; then
|
|
# We want to allow interactive prompting; connectivity test here should not block forever.
|
|
# We'll do a *non-interactive* quick check first: if it fails, fall back to file.
|
|
# (If you want forced DB prompting, run: sudo PSQL_FORCE_PROMPT=1 ... sync)
|
|
if psql_can_connect; then
|
|
echo "db"
|
|
return 0
|
|
fi
|
|
fi
|
|
echo "file"
|
|
}
|
|
|
|
cmd_list() {
|
|
local dc_fqdn dc_ip public_dns source
|
|
dc_fqdn="${AD_DNS_SERVER:-$(detect_dc_fqdn)}"
|
|
dc_ip="${AD_DNS_SERVER_IP:-$(detect_dc_ip "$dc_fqdn")}"
|
|
public_dns="${PUBLIC_DNS_SERVER:-$(detect_public_dns_server)}"
|
|
source="$(choose_host_source)"
|
|
|
|
echo "AD zone : $AD_DNS_ZONE"
|
|
echo "AD DC (server) : $dc_fqdn"
|
|
echo "AD DC IP : $dc_ip"
|
|
echo "Public resolver: $public_dns"
|
|
echo "DB endpoint : $DB_ENDPOINT"
|
|
echo "Hosts file : $HOSTS_FILE"
|
|
echo "Host source : $source"
|
|
echo
|
|
|
|
echo "Hostnames:"
|
|
if [[ "$source" == "db" ]]; then
|
|
read_hosts_db | while IFS='|' read -r host ip; do
|
|
[[ -n "${host// }" ]] || continue
|
|
local norm rel fqdn
|
|
norm="$(normalize_host "$host")" || continue
|
|
rel="${norm%%|*}"
|
|
fqdn="${norm##*|}"
|
|
if [[ -n "${ip// }" ]]; then
|
|
if is_ip "$ip"; then
|
|
echo " - $fqdn (name='$rel', ip=$ip)"
|
|
else
|
|
echo " - $fqdn (name='$rel', cname=$ip)"
|
|
fi
|
|
else
|
|
echo " - $fqdn (name='$rel', ip=<lookup>)"
|
|
fi
|
|
done
|
|
else
|
|
read_hosts_file | while IFS='|' read -r host ip; do
|
|
[[ -n "${host// }" ]] || continue
|
|
local norm rel fqdn
|
|
norm="$(normalize_host "$host")" || continue
|
|
rel="${norm%%|*}"
|
|
fqdn="${norm##*|}"
|
|
if [[ -n "${ip// }" ]]; then
|
|
if is_ip "$ip"; then
|
|
echo " - $fqdn (name='$rel', ip=$ip)"
|
|
else
|
|
echo " - $fqdn (name='$rel', cname=$ip)"
|
|
fi
|
|
else
|
|
echo " - $fqdn (name='$rel', ip=<lookup>)"
|
|
fi
|
|
done
|
|
fi
|
|
}
|
|
|
|
cmd_start() {
|
|
need_root
|
|
cmd_sync
|
|
install_cron
|
|
}
|
|
|
|
cmd_stop() {
|
|
need_root
|
|
remove_cron
|
|
}
|
|
|
|
cmd_flush() {
|
|
need_root
|
|
rm -f "$STATE_FILE"
|
|
log "Cleared local state ($STATE_FILE). DNS records were NOT changed."
|
|
}
|
|
|
|
cmd_status() {
|
|
load_state
|
|
echo "State file: $STATE_FILE"
|
|
echo " last_run_epoch : ${LAST_RUN_EPOCH:-<unknown>}"
|
|
if [[ -n "${LAST_RUN_EPOCH:-}" ]]; then
|
|
date -d "@${LAST_RUN_EPOCH}" +" last_run_time : %F %T %Z"
|
|
else
|
|
echo " last_run_time : <unknown>"
|
|
fi
|
|
echo " last_exit : ${LAST_EXIT:-<unknown>}"
|
|
echo " last_source : ${LAST_SOURCE:-<unknown>}"
|
|
echo " last_public_ns : ${LAST_PUBLIC_DNS:-<unknown>}"
|
|
echo " last_dc : ${LAST_DC:-<unknown>}"
|
|
echo " db_endpoint : ${DB_ENDPOINT}"
|
|
echo
|
|
echo "Recent logs:"
|
|
echo "----------------------------------------"
|
|
journalctl -t "$LOG_TAG" -n 50 --no-pager 2>/dev/null || \
|
|
echo "(journalctl tag '$LOG_TAG' not available; try: sudo journalctl -n 50 | grep $LOG_TAG)"
|
|
}
|
|
|
|
cmd_sync() {
|
|
need_root
|
|
have_cmd samba-tool || die "samba-tool not found (run this on the Samba AD DC)."
|
|
have_cmd dig || die "dig not found. Install bind9-dnsutils or dnsutils."
|
|
|
|
local dc_fqdn dc_ip public_dns source
|
|
dc_fqdn="${AD_DNS_SERVER:-$(detect_dc_fqdn)}"
|
|
dc_ip="${AD_DNS_SERVER_IP:-$(detect_dc_ip "$dc_fqdn")}"
|
|
public_dns="${PUBLIC_DNS_SERVER:-$(detect_public_dns_server)}"
|
|
source="$(choose_host_source)"
|
|
|
|
write_state "LAST_RUN_EPOCH" "$(date +%s)"
|
|
write_state "LAST_PUBLIC_DNS" "$public_dns"
|
|
write_state "LAST_DC" "$dc_fqdn"
|
|
write_state "LAST_SOURCE" "$source"
|
|
|
|
log "Starting sync: zone=${AD_DNS_ZONE}, dc=${dc_fqdn} (${dc_ip}), public_dns=${public_dns}, source=${source}, db=${DB_ENDPOINT}, dry_run=${DRY_RUN}"
|
|
|
|
# Verify Kerberos ticket if running as root via sudo (common case)
|
|
if [[ -z "${KRB5CCNAME:-}" ]]; then
|
|
if [[ -n "${SUDO_UID:-}" ]]; then
|
|
local user_candidate="FILE:/tmp/krb5cc_${SUDO_UID}"
|
|
if [[ -f "${user_candidate#FILE:}" ]]; then
|
|
# Try to use it, but if it's not readable by root (Operation not permitted),
|
|
# we might need to tell the user to kinit as root or fix permissions.
|
|
if [[ -r "${user_candidate#FILE:}" ]]; then
|
|
export KRB5CCNAME="$user_candidate"
|
|
log "INFO: Automatically using Kerberos cache from sudoer: $KRB5CCNAME"
|
|
else
|
|
log "WARN: Found Kerberos cache at ${user_candidate}, but it is not readable by root. Try: sudo kinit administrator@${AD_DNS_ZONE^^}"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# If KRB5CCNAME is set but not readable, it will fail later.
|
|
if [[ -n "${KRB5CCNAME:-}" && ! -r "${KRB5CCNAME#FILE:}" ]]; then
|
|
log "WARN: Kerberos cache ${KRB5CCNAME} is not readable by current user ($(id -u)). samba-tool will likely fail."
|
|
log "HINT: Try running 'kinit' as root, or use 'sudo kinit administrator@${AD_DNS_ZONE^^}'"
|
|
fi
|
|
|
|
# If KRB5CCNAME not set, check if default cache exists (for root kinit case)
|
|
if [[ -z "${KRB5CCNAME:-}" ]]; then
|
|
local default_cache="/tmp/krb5cc_$(id -u)"
|
|
if [[ ! -f "$default_cache" ]]; then
|
|
log "HINT: No Kerberos credentials found. If samba-tool fails, run: sudo -E KRB5CCNAME=/tmp/krb5cc_\$(id -u) $0 sync"
|
|
fi
|
|
fi
|
|
|
|
local exit_code=0
|
|
|
|
# Helper to process a single host entry
|
|
process_host() {
|
|
local host="$1" ip="$2"
|
|
[[ -n "${host// }" ]] || return 0
|
|
local norm rel fqdn
|
|
norm="$(normalize_host "$host")" || return 0
|
|
rel="${norm%%|*}"
|
|
fqdn="${norm##*|}"
|
|
|
|
# Skip zone apex
|
|
if [[ "$fqdn" == "$AD_DNS_ZONE" ]]; then
|
|
log "INFO: Skipping zone apex $fqdn"
|
|
return 0
|
|
fi
|
|
|
|
local a_vals="" aaaa_vals="" cname_vals=""
|
|
if [[ -n "${ip// }" ]]; then
|
|
if is_ip "$ip"; then
|
|
# If a single IP is provided, classify as v4/v6
|
|
if is_ipv6 "$ip"; then
|
|
aaaa_vals="$ip"
|
|
else
|
|
a_vals="$ip"
|
|
fi
|
|
else
|
|
# It's a hostname, use CNAME
|
|
cname_vals="$ip"
|
|
fi
|
|
else
|
|
# No IP in entry: resolve from public DNS
|
|
local recs
|
|
recs="$(dig_records "$public_dns" "$fqdn")"
|
|
a_vals="${recs%%|*}"
|
|
aaaa_vals="${recs##*|}"
|
|
if [[ -z "$a_vals" && -z "$aaaa_vals" ]]; then
|
|
log "WARN: No A/AAAA in public DNS for $fqdn (via $public_dns); skipping"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local ret=0
|
|
if [[ -n "$a_vals" ]]; then
|
|
if ! sync_rr_set "$dc_fqdn" "$AD_DNS_ZONE" "$rel" "$fqdn" "A" "$a_vals"; then ret=1; fi
|
|
fi
|
|
if [[ -n "$aaaa_vals" ]]; then
|
|
if ! sync_rr_set "$dc_fqdn" "$AD_DNS_ZONE" "$rel" "$fqdn" "AAAA" "$aaaa_vals"; then ret=1; fi
|
|
fi
|
|
if [[ -n "$cname_vals" ]]; then
|
|
# Samba-tool CNAME values should ideally be FQDN with trailing dot for safety,
|
|
# but let's see how it handles it. The issue says "samba-tool dns add .. cname"
|
|
if ! sync_rr_set "$dc_fqdn" "$AD_DNS_ZONE" "$rel" "$fqdn" "CNAME" "$cname_vals"; then ret=1; fi
|
|
fi
|
|
return $ret
|
|
}
|
|
|
|
if [[ "$source" == "db" ]]; then
|
|
while IFS='|' read -r host ip; do
|
|
if ! process_host "$host" "$ip"; then exit_code=1; fi
|
|
done < <(read_hosts_db)
|
|
else
|
|
while IFS='|' read -r host ip; do
|
|
if ! process_host "$host" "$ip"; then exit_code=1; fi
|
|
done < <(read_hosts_file)
|
|
fi
|
|
|
|
write_state "LAST_EXIT" "$exit_code"
|
|
if [[ "$exit_code" -eq 0 ]]; then
|
|
log "Sync complete: success"
|
|
else
|
|
log "Sync complete: errors occurred"
|
|
fi
|
|
return "$exit_code"
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 [-db=host:port/dbname] <command>
|
|
|
|
Commands:
|
|
start Run sync now and install hourly cron job
|
|
stop Remove hourly cron job
|
|
status Show last run time, exit status, and recent logs
|
|
sync Run one sync pass now
|
|
flush Clear local state file (does not change DNS records)
|
|
list Show host list + DC info (alias: show, details)
|
|
show Alias for list
|
|
details Alias for list
|
|
|
|
DB option:
|
|
-db=HOST:PORT/DBNAME default: ${DEFAULT_DB_ENDPOINT}
|
|
|
|
Host list sources:
|
|
1) If psql exists and DB is reachable:
|
|
SELECT host_name, ip_address FROM prole.hosts;
|
|
(psql may prompt for password interactively)
|
|
2) Else: $HOSTS_FILE
|
|
|
|
Examples:
|
|
sudo $0 sync
|
|
sudo kinit administrator@PROLE.ORG && sudo $0 sync
|
|
sudo -E $0 sync (preserves Kerberos ticket environment)
|
|
sudo DRY_RUN=1 $0 sync
|
|
sudo $0 start
|
|
sudo $0 status
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
# Parse -db=... and keep remaining args
|
|
local args=()
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
-db=*) DB_ENDPOINT="${a#-db=}" ;;
|
|
*) args+=("$a") ;;
|
|
esac
|
|
done
|
|
|
|
local cmd="${args[0]:-}"
|
|
case "$cmd" in
|
|
start) cmd_start ;;
|
|
stop) cmd_stop ;;
|
|
status) cmd_status ;;
|
|
sync) cmd_sync ;;
|
|
flush) cmd_flush ;;
|
|
list|show|details) cmd_list ;;
|
|
""|-h|--help|help) usage ;;
|
|
*) die "Unknown command: $cmd (try --help)" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@" |