mirror of
git://f0xx.org/ac/ac-deploy
synced 2026-07-29 10:18:15 +03:00
load_secrets_lab quotes values safely; compose reads MAIL_FROM via read_secret. Co-authored-by: Cursor <cursoragent@cursor.com>
263 lines
6.7 KiB
Bash
263 lines
6.7 KiB
Bash
# shellcheck shell=sh
|
|
# Shared helpers for cast cluster scripts.
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date -Iseconds 2>/dev/null || date)" "$*"
|
|
}
|
|
|
|
die() {
|
|
log "ERROR: $*"
|
|
if [ -n "${JOURNAL_LAST_ACTION:-}" ]; then
|
|
journal_record NOK "$JOURNAL_LAST_ACTION" "$*"
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
cluster_root() {
|
|
if [ -n "${CAST_CLUSTER_ROOT:-}" ]; then
|
|
printf '%s\n' "$CAST_CLUSTER_ROOT"
|
|
return
|
|
fi
|
|
if [ -f /shared/cluster/cluster.env ]; then
|
|
printf '/shared/cluster\n'
|
|
return
|
|
fi
|
|
if [ -f "$(dirname "$0")/../cluster.env" ]; then
|
|
cd "$(dirname "$0")/.." && pwd
|
|
return
|
|
fi
|
|
die "cannot locate cluster root (is /shared mounted?)"
|
|
}
|
|
|
|
load_cluster_env() {
|
|
ROOT="$(cluster_root)"
|
|
ENV_FILE="${1:-$ROOT/cluster.env}"
|
|
[ -f "$ENV_FILE" ] || die "missing $ENV_FILE"
|
|
# shellcheck disable=SC1090
|
|
. "$ENV_FILE"
|
|
CAST_CLUSTER_ROOT="$ROOT"
|
|
FE_NFS="${FE_NFS_SERVER}:${FE_NFS_EXPORT_PATH}"
|
|
FSTAB_NFS_LINE="${FE_NFS} ${SHARED_MOUNT} nfs rw,_netdev,nofail,noatime,nodiratime 0 0"
|
|
ALL_CAST_HOSTS="${CAST01_HOST} ${CAST02_HOST} ${CAST03_HOST}"
|
|
JOURNAL_DIR="${SHARED_MOUNT}/journal"
|
|
JOURNAL_TSV="${JOURNAL_DIR}/${CLUSTER_NAME}.journal.tsv"
|
|
JOURNAL_SUMMARY="${JOURNAL_DIR}/${CLUSTER_NAME}.summary.md"
|
|
}
|
|
|
|
journal_init() {
|
|
load_cluster_env 2>/dev/null || return 0
|
|
mkdir -p "$JOURNAL_DIR"
|
|
if [ ! -f "$JOURNAL_TSV" ]; then
|
|
printf 'timestamp_utc\thost\tactor\tphase\taction\tstatus\tdetail\n' > "$JOURNAL_TSV"
|
|
fi
|
|
}
|
|
|
|
journal_record() {
|
|
_status="$1"
|
|
_action="$2"
|
|
_detail="${3:-}"
|
|
_phase="${JOURNAL_PHASE:-}"
|
|
journal_init
|
|
_ts="$(date -u -Iseconds 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
_host="$(host_short 2>/dev/null || echo unknown)"
|
|
_actor="${JOURNAL_ACTOR:-script}"
|
|
_line="${_ts} ${_host} ${_actor} ${_phase} ${_action} ${_status} ${_detail}"
|
|
printf '%s\n' "$_line" >> "$JOURNAL_TSV"
|
|
log "journal ${_status} ${_action}${_detail:+ — ${_detail}}"
|
|
if [ -x "${CAST_CLUSTER_ROOT}/scripts/journal-rebuild-summary.sh" ] 2>/dev/null; then
|
|
sh "${CAST_CLUSTER_ROOT}/scripts/journal-rebuild-summary.sh" 2>/dev/null || true
|
|
elif [ -f "${CAST_CLUSTER_ROOT}/scripts/journal-rebuild-summary.sh" ]; then
|
|
sh "${CAST_CLUSTER_ROOT}/scripts/journal-rebuild-summary.sh" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
journal_start() {
|
|
JOURNAL_LAST_ACTION="$1"
|
|
journal_record START "$1" "${2:-}"
|
|
}
|
|
|
|
journal_ok() {
|
|
journal_record OK "$1" "${2:-}"
|
|
JOURNAL_LAST_ACTION=""
|
|
}
|
|
|
|
journal_nok() {
|
|
journal_record NOK "$1" "${2:-}"
|
|
JOURNAL_LAST_ACTION=""
|
|
}
|
|
|
|
journal_warn() {
|
|
journal_record WARN "$1" "${2:-}"
|
|
}
|
|
|
|
ensure_shared_mounted() {
|
|
load_cluster_env
|
|
if mount | grep -q " on ${SHARED_MOUNT} "; then
|
|
journal_init
|
|
return 0
|
|
fi
|
|
if [ -f /etc/cast-cluster.env ]; then
|
|
# shellcheck disable=SC1091
|
|
. /etc/cast-cluster.env
|
|
FE_NFS="${FE_NFS_SERVER}:${FE_NFS_EXPORT_PATH}"
|
|
FSTAB_NFS_LINE="${FE_NFS} ${SHARED_MOUNT} nfs rw,_netdev,nofail,noatime,nodiratime 0 0"
|
|
fi
|
|
log "mounting ${SHARED_MOUNT} from ${FE_NFS}"
|
|
journal_start nfs_mount "from ${FE_NFS}"
|
|
mkdir -p "$SHARED_MOUNT"
|
|
grep -v "${FE_NFS_EXPORT_PATH}" /etc/fstab > /var/tmp/cast-fstab.new 2>/dev/null || cp /etc/fstab /var/tmp/cast-fstab.new
|
|
echo "$FSTAB_NFS_LINE" >> /var/tmp/cast-fstab.new
|
|
mv /var/tmp/cast-fstab.new /etc/fstab
|
|
rc-update add nfsmount boot 2>/dev/null || true
|
|
mount "$SHARED_MOUNT" 2>/dev/null || mount -a
|
|
if mount | grep -q " on ${SHARED_MOUNT} "; then
|
|
load_cluster_env
|
|
journal_ok nfs_mount "${SHARED_MOUNT}"
|
|
else
|
|
journal_nok nfs_mount "mount failed"
|
|
die "NFS mount failed: ${SHARED_MOUNT}"
|
|
fi
|
|
}
|
|
|
|
run_script() {
|
|
_script="$1"
|
|
_action="$(basename "$_script" .sh)"
|
|
journal_start "$_action" "$_script"
|
|
if sh "$_script"; then
|
|
journal_ok "$_action" "$_script"
|
|
return 0
|
|
fi
|
|
journal_nok "$_action" "$_script"
|
|
return 1
|
|
}
|
|
|
|
host_short() {
|
|
hostname -s 2>/dev/null || hostname
|
|
}
|
|
|
|
is_primary_node() {
|
|
[ "$(host_short)" = "$CAST01_HOST" ]
|
|
}
|
|
|
|
is_replica_node() {
|
|
H="$(host_short)"
|
|
[ "$H" = "$CAST02_HOST" ] || [ "$H" = "$CAST03_HOST" ]
|
|
}
|
|
|
|
wait_for_tcp() {
|
|
_host="$1"
|
|
_port="$2"
|
|
_timeout="${3:-300}"
|
|
_i=0
|
|
while [ "$_i" -lt "$_timeout" ]; do
|
|
if nc -z "$_host" "$_port" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
_i=$((_i + 1))
|
|
sleep 1
|
|
done
|
|
die "timeout waiting for ${_host}:${_port}"
|
|
}
|
|
|
|
wait_for_primary_db() {
|
|
load_cluster_env
|
|
CREDS="${CAST_CLUSTER_ROOT}/credentials.txt"
|
|
APP_PASS="$(awk '/^\[mariadb_app\]/{f=1;next} /^\[/{f=0} f&&/^password=/{print substr($0,10); exit}' "$CREDS")"
|
|
_i=0
|
|
while [ "$_i" -lt 300 ]; do
|
|
if mariadb -u androidcast -p"$APP_PASS" -h"$PRIMARY_DB_HOST" -N -e 'SELECT 1' >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
_i=$((_i + 1))
|
|
sleep 2
|
|
done
|
|
die "primary DB not reachable at ${PRIMARY_DB_HOST}"
|
|
}
|
|
|
|
read_cred() {
|
|
_section="$1"
|
|
_key="$2"
|
|
_file="${CAST_CLUSTER_ROOT}/credentials.txt"
|
|
awk -v s="[$_section]" -v k="$_key=" '
|
|
$0 == s { f=1; next }
|
|
/^\[/ { f=0 }
|
|
f && index($0, k) == 1 { print substr($0, length(k)+1); exit }
|
|
' "$_file"
|
|
}
|
|
|
|
# Optional NFS secrets (not in git): secrets.lab.env
|
|
load_secrets_lab() {
|
|
_file="${CAST_CLUSTER_ROOT}/secrets.lab.env"
|
|
[ -f "$_file" ] || return 0
|
|
_tmp="/tmp/cast-secrets-load-$$"
|
|
: > "$_tmp"
|
|
while IFS= read -r _line || [ -n "$_line" ]; do
|
|
case "$_line" in
|
|
''|\#*) continue ;;
|
|
*=*)
|
|
_k="${_line%%=*}"
|
|
_v="${_line#*=}"
|
|
case "$_v" in
|
|
\"*) _v="${_v#\"}"; _v="${_v%\"}" ;;
|
|
esac
|
|
case "$_k" in
|
|
MAIL_*|MSMTP_*|SMTP_*|AUTH_*|GITEA_*|WG_*|RSSH_*|URL_SHORTENER_*)
|
|
_v_escaped="$(printf '%s' "$_v" | sed "s/'/'\\\\''/g")"
|
|
printf "%s='%s'\n" "$_k" "$_v_escaped" >> "$_tmp"
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
done < "$_file"
|
|
# shellcheck disable=SC1090
|
|
. "$_tmp"
|
|
rm -f "$_tmp"
|
|
}
|
|
|
|
# RFC5322 From — secrets without angle brackets break Gmail SMTP (shell ate <email>).
|
|
normalize_mail_from() {
|
|
_in="$1"
|
|
_smtp_user="${2:-}"
|
|
case "$_in" in
|
|
*'<'*'>'*) printf '%s' "$_in"; return ;;
|
|
esac
|
|
case "$_in" in
|
|
*@*)
|
|
_email="$(printf '%s' "$_in" | awk '{print $NF}')"
|
|
_name="$(printf '%s' "$_in" | sed "s/ ${_email}\$//")"
|
|
if [ -n "$_name" ]; then
|
|
printf '%s <%s>' "$_name" "$_email"
|
|
else
|
|
printf '%s' "$_email"
|
|
fi
|
|
return
|
|
;;
|
|
esac
|
|
if [ -n "$_smtp_user" ] && printf '%s' "$_smtp_user" | grep -q '@'; then
|
|
printf 'Android Cast Lab <%s>' "$_smtp_user"
|
|
return
|
|
fi
|
|
printf '%s' "$_in"
|
|
}
|
|
|
|
read_secret() {
|
|
_key="$1"
|
|
_file="${CAST_CLUSTER_ROOT}/secrets.lab.env"
|
|
[ -f "$_file" ] || return 1
|
|
awk -v k="$_key" '
|
|
$0 ~ "^" k "=" {
|
|
sub("^" k "=", "")
|
|
sub(/^"/, "")
|
|
sub(/"$/, "")
|
|
print
|
|
exit
|
|
}
|
|
' "$_file"
|
|
}
|
|
|
|
ssh_node() {
|
|
_host="$1"
|
|
shift
|
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=no "${SSH_USER}@${_host}" "$@"
|
|
}
|