320 lines
8.9 KiB
Bash
320 lines
8.9 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
###############################################################################
|
|
# Configuration
|
|
###############################################################################
|
|
|
|
# Space separated host IPs. Include the master IP if the master node also needs
|
|
# rmdc-watchdog-node installed.
|
|
INSTALL_HOSTS="192.168.3.31 192.168.3.32 192.168.3.33"
|
|
|
|
# Absolute binary path on the master node. Remote nodes will receive the binary
|
|
# at the same absolute path.
|
|
RMDC_WATCHDOG_NODE_BIN="/root/wdd/rmdc-watchdog-node"
|
|
|
|
# Replace WDD_BOOTSTRAP_PSK in the systemd service.
|
|
WDD_BOOTSTRAP_PSK="hO8dKlh9hSXZ25Bv9xsySxDe2rh7XikV"
|
|
|
|
# Main NIC name. The script reads each node's IPv4 address on this NIC and uses
|
|
# it as WDD_LISTEN_IP.
|
|
MAIN_NIC="eno3"
|
|
|
|
# Master node private IP. This replaces WDD_BOOTSTRAP_ALLOW_IPS.
|
|
MASTER_IP="192.168.3.31"
|
|
|
|
# Systemd service install path on every node.
|
|
SERVICE_PATH="/usr/lib/systemd/system/rmdc-watchdog-node.service"
|
|
|
|
# SSH settings for remote nodes.
|
|
SSH_PORT="22"
|
|
SSH_CONNECT_TIMEOUT_SEC="10"
|
|
SSH_CONTROL_PERSIST="10m"
|
|
|
|
###############################################################################
|
|
# Implementation
|
|
###############################################################################
|
|
|
|
SERVICE_NAME="$(basename "$SERVICE_PATH")"
|
|
REMOTE_TMP_DIR="/tmp/rmdc-watchdog-node-deploy"
|
|
REMOTE_SERVICE_TMP="${REMOTE_TMP_DIR}/${SERVICE_NAME}"
|
|
SSH_CONTROL_DIR=""
|
|
SSH_CONTROL_PATH=""
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
|
|
}
|
|
|
|
die() {
|
|
printf 'error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
shell_quote() {
|
|
printf "'"
|
|
printf '%s' "${1-}" | sed "s/'/'\\\\''/g"
|
|
printf "'"
|
|
}
|
|
|
|
require_command() {
|
|
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
|
}
|
|
|
|
validate_config() {
|
|
[[ "$(id -u)" == "0" ]] || die "this script must run as root"
|
|
[[ "$#" == "0" ]] || die "this script does not accept command line arguments; edit the configuration section instead"
|
|
[[ -n "$INSTALL_HOSTS" ]] || die "INSTALL_HOSTS is empty"
|
|
[[ -f "$RMDC_WATCHDOG_NODE_BIN" ]] || die "binary not found: $RMDC_WATCHDOG_NODE_BIN"
|
|
[[ -n "$WDD_BOOTSTRAP_PSK" && "$WDD_BOOTSTRAP_PSK" != "change-me" ]] || die "WDD_BOOTSTRAP_PSK must be configured"
|
|
[[ -n "$MAIN_NIC" ]] || die "MAIN_NIC must be configured"
|
|
[[ -n "$MASTER_IP" ]] || die "MASTER_IP must be configured"
|
|
[[ "$SERVICE_PATH" = /* ]] || die "SERVICE_PATH must be an absolute path"
|
|
[[ "$RMDC_WATCHDOG_NODE_BIN" = /* ]] || die "RMDC_WATCHDOG_NODE_BIN must be an absolute path"
|
|
|
|
require_command awk
|
|
require_command basename
|
|
require_command dirname
|
|
require_command install
|
|
require_command ip
|
|
require_command mktemp
|
|
require_command sed
|
|
require_command systemctl
|
|
}
|
|
|
|
get_local_ipv4s() {
|
|
{
|
|
hostname -I 2>/dev/null | tr ' ' '\n' || true
|
|
ip -o -4 addr show 2>/dev/null | awk '{split($4, a, "/"); print a[1]}' || true
|
|
} | awk 'NF' | sort -u
|
|
}
|
|
|
|
is_local_host() {
|
|
local host="$1"
|
|
|
|
case "$host" in
|
|
localhost|127.0.0.1|::1)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
get_local_ipv4s | awk -v host="$host" '$0 == host {found = 1} END {exit found ? 0 : 1}'
|
|
}
|
|
|
|
local_nic_ip() {
|
|
ip -o -4 addr show dev "$MAIN_NIC" scope global |
|
|
awk '{split($4, a, "/"); print a[1]; exit}'
|
|
}
|
|
|
|
remote_sh() {
|
|
local host="$1"
|
|
local command="$2"
|
|
|
|
ssh \
|
|
-p "$SSH_PORT" \
|
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT_SEC" \
|
|
-o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null \
|
|
-o LogLevel=ERROR \
|
|
-o ControlMaster=auto \
|
|
-o ControlPersist="$SSH_CONTROL_PERSIST" \
|
|
-o ControlPath="$SSH_CONTROL_PATH" \
|
|
"root@${host}" \
|
|
"bash -lc $(shell_quote "$command")"
|
|
}
|
|
|
|
remote_scp() {
|
|
local source_path="$1"
|
|
local host="$2"
|
|
local target_path="$3"
|
|
|
|
scp \
|
|
-P "$SSH_PORT" \
|
|
-o ConnectTimeout="$SSH_CONNECT_TIMEOUT_SEC" \
|
|
-o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null \
|
|
-o LogLevel=ERROR \
|
|
-o ControlMaster=auto \
|
|
-o ControlPersist="$SSH_CONTROL_PERSIST" \
|
|
-o ControlPath="$SSH_CONTROL_PATH" \
|
|
"$source_path" \
|
|
"root@${host}:${target_path}"
|
|
}
|
|
|
|
cleanup_ssh_control() {
|
|
local host
|
|
|
|
if [[ -n "${SSH_CONTROL_DIR:-}" && -d "$SSH_CONTROL_DIR" ]]; then
|
|
for host in $INSTALL_HOSTS; do
|
|
if ! is_local_host "$host"; then
|
|
ssh \
|
|
-p "$SSH_PORT" \
|
|
-o StrictHostKeyChecking=no \
|
|
-o UserKnownHostsFile=/dev/null \
|
|
-o LogLevel=ERROR \
|
|
-o ControlPath="$SSH_CONTROL_PATH" \
|
|
-O exit \
|
|
"root@${host}" >/dev/null 2>&1 || true
|
|
fi
|
|
done
|
|
rm -rf "$SSH_CONTROL_DIR"
|
|
fi
|
|
}
|
|
|
|
remote_nic_ip() {
|
|
local host="$1"
|
|
remote_sh "$host" "ip -o -4 addr show dev $(shell_quote "$MAIN_NIC") scope global | awk '{split(\$4, a, \"/\"); print a[1]; exit}'"
|
|
}
|
|
|
|
render_service() {
|
|
local listen_ip="$1"
|
|
local output_path="$2"
|
|
local working_dir
|
|
|
|
working_dir="$(dirname "$RMDC_WATCHDOG_NODE_BIN")"
|
|
|
|
cat > "$output_path" <<EOF
|
|
[Unit]
|
|
Description=RMDC Watchdog Node Daemon
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
Group=root
|
|
WorkingDirectory=${working_dir}
|
|
ExecStart=${RMDC_WATCHDOG_NODE_BIN}
|
|
Restart=on-failure
|
|
RestartSec=3
|
|
|
|
Environment="WDD_LISTEN_IP=${listen_ip}"
|
|
Environment="WDD_BOOTSTRAP_TIMEOUT_SEC=300"
|
|
Environment="WDD_BOOTSTRAP_ALLOW_IPS=${MASTER_IP}"
|
|
Environment="WDD_BOOTSTRAP_RE_ALLOW=false"
|
|
Environment="WDD_RUNTIME_MAX_CONCURRENT=4"
|
|
Environment="WDD_RUNTIME_QUEUE_CAPACITY=128"
|
|
Environment="WDD_RUNTIME_TASK_TIMEOUT_SEC=1800"
|
|
Environment="WDD_SECURITY_REQUIRE_ROOT=true"
|
|
Environment="WDD_SECURITY_TOTP_WINDOW=1"
|
|
Environment="WDD_BOOTSTRAP_PSK=${WDD_BOOTSTRAP_PSK}"
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
}
|
|
|
|
install_local() {
|
|
local host="$1"
|
|
local listen_ip
|
|
local rendered_service
|
|
|
|
listen_ip="$(local_nic_ip)"
|
|
[[ -n "$listen_ip" ]] || die "failed to get local IPv4 address from NIC ${MAIN_NIC}"
|
|
|
|
rendered_service="$(mktemp)"
|
|
render_service "$listen_ip" "$rendered_service"
|
|
|
|
log "${host}: installing local service with WDD_LISTEN_IP=${listen_ip}"
|
|
chmod 0755 "$RMDC_WATCHDOG_NODE_BIN"
|
|
chown root:root "$RMDC_WATCHDOG_NODE_BIN"
|
|
install -d -m 0755 "$(dirname "$SERVICE_PATH")"
|
|
install -m 0644 -o root -g root "$rendered_service" "$SERVICE_PATH"
|
|
rm -f "$rendered_service"
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable "$SERVICE_NAME"
|
|
systemctl restart "$SERVICE_NAME"
|
|
systemctl is-active --quiet "$SERVICE_NAME"
|
|
}
|
|
|
|
install_remote() {
|
|
local host="$1"
|
|
local listen_ip
|
|
local rendered_service
|
|
local binary_dir
|
|
local service_dir
|
|
|
|
listen_ip="$(remote_nic_ip "$host")"
|
|
[[ -n "$listen_ip" ]] || die "${host}: failed to get IPv4 address from NIC ${MAIN_NIC}"
|
|
|
|
rendered_service="$(mktemp)"
|
|
render_service "$listen_ip" "$rendered_service"
|
|
|
|
binary_dir="$(dirname "$RMDC_WATCHDOG_NODE_BIN")"
|
|
service_dir="$(dirname "$SERVICE_PATH")"
|
|
|
|
log "${host}: preparing remote directories"
|
|
remote_sh "$host" "mkdir -p $(shell_quote "$binary_dir") $(shell_quote "$service_dir") $(shell_quote "$REMOTE_TMP_DIR")"
|
|
|
|
log "${host}: uploading binary to ${RMDC_WATCHDOG_NODE_BIN}"
|
|
remote_scp "$RMDC_WATCHDOG_NODE_BIN" "$host" "$RMDC_WATCHDOG_NODE_BIN"
|
|
|
|
log "${host}: uploading service with WDD_LISTEN_IP=${listen_ip}"
|
|
remote_scp "$rendered_service" "$host" "$REMOTE_SERVICE_TMP"
|
|
rm -f "$rendered_service"
|
|
|
|
log "${host}: reloading and restarting ${SERVICE_NAME}"
|
|
remote_sh "$host" "set -euo pipefail
|
|
chmod 0755 $(shell_quote "$RMDC_WATCHDOG_NODE_BIN")
|
|
chown root:root $(shell_quote "$RMDC_WATCHDOG_NODE_BIN")
|
|
install -m 0644 -o root -g root $(shell_quote "$REMOTE_SERVICE_TMP") $(shell_quote "$SERVICE_PATH")
|
|
systemctl daemon-reload
|
|
systemctl enable $(shell_quote "$SERVICE_NAME")
|
|
systemctl restart $(shell_quote "$SERVICE_NAME")
|
|
systemctl is-active --quiet $(shell_quote "$SERVICE_NAME")
|
|
rm -f $(shell_quote "$REMOTE_SERVICE_TMP")"
|
|
}
|
|
|
|
print_failure_diagnostics() {
|
|
local host="$1"
|
|
|
|
if is_local_host "$host"; then
|
|
systemctl --no-pager --full status "$SERVICE_NAME" || true
|
|
journalctl -u "$SERVICE_NAME" -n 80 --no-pager || true
|
|
else
|
|
remote_sh "$host" "systemctl --no-pager --full status $(shell_quote "$SERVICE_NAME") || true; journalctl -u $(shell_quote "$SERVICE_NAME") -n 80 --no-pager || true" || true
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
validate_config "$@"
|
|
|
|
local has_remote="false"
|
|
local host
|
|
for host in $INSTALL_HOSTS; do
|
|
if ! is_local_host "$host"; then
|
|
has_remote="true"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ "$has_remote" == "true" ]]; then
|
|
require_command ssh
|
|
require_command scp
|
|
SSH_CONTROL_DIR="$(mktemp -d -t rmdc-watchdog-node-ssh.XXXXXX)"
|
|
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/%r@%h:%p"
|
|
trap cleanup_ssh_control EXIT
|
|
fi
|
|
|
|
for host in $INSTALL_HOSTS; do
|
|
log "${host}: deployment started"
|
|
if is_local_host "$host"; then
|
|
if ! install_local "$host"; then
|
|
print_failure_diagnostics "$host"
|
|
die "${host}: deployment failed"
|
|
fi
|
|
else
|
|
if ! install_remote "$host"; then
|
|
print_failure_diagnostics "$host"
|
|
die "${host}: deployment failed"
|
|
fi
|
|
fi
|
|
log "${host}: deployment finished"
|
|
done
|
|
|
|
log "all deployments finished"
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
main "$@"
|
|
fi
|