mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-12 05:45:27 +00:00
The installer was trying to write binaries to /opt/pulse/sensor-proxy/bin/ before creating the directory structure, causing 'No such file or directory' errors on fresh installs. Moved directory creation for INSTALL_ROOT and bin/ to before binary installation section (before line 657), ensuring directories exist before use. Related to cleanup implementation testing.
2469 lines
90 KiB
Bash
Executable file
2469 lines
90 KiB
Bash
Executable file
#!/bin/bash
|
|
|
|
# install-sensor-proxy.sh - Installs pulse-sensor-proxy on Proxmox host for secure temperature monitoring
|
|
# Supports --uninstall [--purge] to remove the proxy and cleanup resources.
|
|
# This script is idempotent and can be safely re-run
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
print_info() {
|
|
if [ "$QUIET" != true ]; then
|
|
echo -e "${GREEN}[INFO]${NC} $1"
|
|
fi
|
|
}
|
|
|
|
print_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
print_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
print_success() {
|
|
echo -e "${GREEN}✓${NC} $1"
|
|
}
|
|
|
|
configure_local_authorized_key() {
|
|
local auth_line=$1
|
|
local auth_keys_file="/root/.ssh/authorized_keys"
|
|
local tmp_auth
|
|
|
|
tmp_auth=$(mktemp)
|
|
mkdir -p /root/.ssh
|
|
touch "$tmp_auth"
|
|
|
|
if [[ -f "$auth_keys_file" ]]; then
|
|
grep -vF '# pulse-managed-key' "$auth_keys_file" >"$tmp_auth" 2>/dev/null || true
|
|
chmod --reference="$auth_keys_file" "$tmp_auth" 2>/dev/null || chmod 600 "$tmp_auth"
|
|
chown --reference="$auth_keys_file" "$tmp_auth" 2>/dev/null || true
|
|
else
|
|
chmod 600 "$tmp_auth"
|
|
fi
|
|
|
|
echo "${auth_line}" >>"$tmp_auth"
|
|
if mv "$tmp_auth" "$auth_keys_file"; then
|
|
if [ "$QUIET" != true ]; then
|
|
print_success "SSH key configured on localhost"
|
|
fi
|
|
else
|
|
rm -f "$tmp_auth"
|
|
print_warn "Failed to configure SSH key on localhost"
|
|
print_info "Add this line manually to /root/.ssh/authorized_keys:"
|
|
print_info " ${auth_line}"
|
|
fi
|
|
}
|
|
|
|
# Helper function to safely update allowed_nodes section in config file
|
|
# Removes any existing allowed_nodes section before adding new one to prevent duplicates
|
|
update_allowed_nodes() {
|
|
local config_file="/etc/pulse-sensor-proxy/config.yaml"
|
|
local comment_line="$1"
|
|
shift
|
|
local nodes=("$@")
|
|
|
|
# Create temp file with unique prefix to avoid conflicts
|
|
local tmp_config
|
|
tmp_config=$(mktemp /tmp/pulse-config.XXXXXXXXXX)
|
|
|
|
# Ensure temp file is cleaned up on exit (success or failure)
|
|
trap "rm -f '$tmp_config'" RETURN
|
|
|
|
# Remove any existing allowed_nodes section (including the YAML key and all list items)
|
|
# This handles multi-line allowed_nodes blocks and associated comment headers
|
|
if [[ -f "$config_file" ]]; then
|
|
awk '
|
|
# When we hit allowed_nodes, mark section start and clear pending comments
|
|
/^allowed_nodes:/ {
|
|
in_section=1
|
|
delete pending_lines
|
|
pending_count=0
|
|
next
|
|
}
|
|
|
|
# Inside section: skip all content until we hit a non-indented, non-comment line
|
|
in_section && /^[^ \t#]/ {
|
|
in_section=0
|
|
# Fall through to print this line
|
|
}
|
|
in_section {
|
|
next
|
|
}
|
|
|
|
# Outside section: buffer comment lines (they might belong to allowed_nodes)
|
|
!in_section && /^[ \t]*#/ {
|
|
pending_lines[pending_count++] = $0
|
|
next
|
|
}
|
|
|
|
# Outside section: non-comment line - flush pending comments and print
|
|
!in_section {
|
|
for (i = 0; i < pending_count; i++) {
|
|
print pending_lines[i]
|
|
}
|
|
delete pending_lines
|
|
pending_count = 0
|
|
print
|
|
}
|
|
|
|
# At end of file, flush any remaining pending comments
|
|
END {
|
|
for (i = 0; i < pending_count; i++) {
|
|
print pending_lines[i]
|
|
}
|
|
}
|
|
' "$config_file" > "$tmp_config"
|
|
|
|
# Preserve file permissions (but defer ownership change until after all writes)
|
|
chmod --reference="$config_file" "$tmp_config" 2>/dev/null || chmod 0644 "$tmp_config"
|
|
else
|
|
touch "$tmp_config"
|
|
chmod 0644 "$tmp_config"
|
|
fi
|
|
|
|
# Append new allowed_nodes section
|
|
{
|
|
echo ""
|
|
echo "# ${comment_line}"
|
|
echo "# These nodes are allowed to request temperature data when cluster IPC validation is unavailable"
|
|
echo "allowed_nodes:"
|
|
for node in "${nodes[@]}"; do
|
|
echo " - $node"
|
|
done
|
|
} >> "$tmp_config"
|
|
|
|
# Replace original file
|
|
if ! mv "$tmp_config" "$config_file"; then
|
|
echo "ERROR: Failed to update $config_file - check permissions" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Ensure proper permissions
|
|
chmod 0644 "$config_file" 2>/dev/null || true
|
|
chown pulse-sensor-proxy:pulse-sensor-proxy "$config_file" 2>/dev/null || true
|
|
}
|
|
|
|
# Installation root - writable location that works on read-only /usr systems
|
|
INSTALL_ROOT="/opt/pulse/sensor-proxy"
|
|
|
|
# Binaries and scripts (in writable location)
|
|
BINARY_PATH="${INSTALL_ROOT}/bin/pulse-sensor-proxy"
|
|
WRAPPER_SCRIPT="${INSTALL_ROOT}/bin/pulse-sensor-wrapper.sh"
|
|
CLEANUP_SCRIPT_PATH="${INSTALL_ROOT}/bin/pulse-sensor-cleanup.sh"
|
|
SELFHEAL_SCRIPT="${INSTALL_ROOT}/bin/pulse-sensor-proxy-selfheal.sh"
|
|
STORED_INSTALLER="${INSTALL_ROOT}/install-sensor-proxy.sh"
|
|
|
|
# System configuration (standard locations)
|
|
SERVICE_PATH="/etc/systemd/system/pulse-sensor-proxy.service"
|
|
RUNTIME_DIR="/run/pulse-sensor-proxy"
|
|
SOCKET_PATH="${RUNTIME_DIR}/pulse-sensor-proxy.sock"
|
|
WORK_DIR="/var/lib/pulse-sensor-proxy"
|
|
SSH_DIR="${WORK_DIR}/ssh"
|
|
CONFIG_DIR="/etc/pulse-sensor-proxy"
|
|
CTID_FILE="${CONFIG_DIR}/ctid"
|
|
CLEANUP_PATH_UNIT="/etc/systemd/system/pulse-sensor-cleanup.path"
|
|
CLEANUP_SERVICE_UNIT="/etc/systemd/system/pulse-sensor-cleanup.service"
|
|
CLEANUP_REQUEST_PATH="${WORK_DIR}/cleanup-request.json"
|
|
SERVICE_USER="pulse-sensor-proxy"
|
|
LOG_DIR="/var/log/pulse/sensor-proxy"
|
|
SELFHEAL_SERVICE_UNIT="/etc/systemd/system/pulse-sensor-proxy-selfheal.service"
|
|
SELFHEAL_TIMER_UNIT="/etc/systemd/system/pulse-sensor-proxy-selfheal.timer"
|
|
SCRIPT_SOURCE="$(readlink -f "${BASH_SOURCE[0]:-$0}" 2>/dev/null || printf '%s' "${BASH_SOURCE[0]:-$0}")"
|
|
GITHUB_REPO="rcourtman/Pulse"
|
|
LATEST_RELEASE_TAG=""
|
|
REQUESTED_VERSION=""
|
|
INSTALLER_CACHE_REASON=""
|
|
DEFER_SOCKET_VERIFICATION=false
|
|
|
|
cleanup_local_authorized_keys() {
|
|
local auth_keys_file="/root/.ssh/authorized_keys"
|
|
if [[ ! -f "$auth_keys_file" ]]; then
|
|
return
|
|
fi
|
|
|
|
if grep -q '# pulse-\(managed\|proxy\)-key$' "$auth_keys_file"; then
|
|
if sed -i -e '/# pulse-managed-key$/d' -e '/# pulse-proxy-key$/d' "$auth_keys_file"; then
|
|
print_info "Removed Pulse SSH keys from ${auth_keys_file}"
|
|
else
|
|
print_warn "Failed to clean Pulse SSH keys from ${auth_keys_file}"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cleanup_cluster_authorized_keys_manual() {
|
|
local nodes=()
|
|
if command -v pvecm >/dev/null 2>&1; then
|
|
while IFS= read -r node_ip; do
|
|
[[ -n "$node_ip" ]] && nodes+=("$node_ip")
|
|
done < <(pvecm status 2>/dev/null | awk '/0x[0-9a-f]+.*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print $3}' || true)
|
|
fi
|
|
|
|
if [[ ${#nodes[@]} -eq 0 ]]; then
|
|
cleanup_local_authorized_keys
|
|
return
|
|
fi
|
|
|
|
local local_ips
|
|
local_ips="$(hostname -I 2>/dev/null || echo "")"
|
|
local local_hostnames
|
|
local_hostnames="$(hostname 2>/dev/null || echo "") $(hostname -f 2>/dev/null || echo "")"
|
|
|
|
for node_ip in "${nodes[@]}"; do
|
|
local is_local=false
|
|
for local_ip in $local_ips; do
|
|
if [[ "$node_ip" == "$local_ip" ]]; then
|
|
is_local=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ " $local_hostnames " == *" $node_ip "* ]]; then
|
|
is_local=true
|
|
fi
|
|
|
|
if [[ "$node_ip" == "127.0.0.1" || "$node_ip" == "localhost" ]]; then
|
|
is_local=true
|
|
fi
|
|
|
|
if [[ "$is_local" == true ]]; then
|
|
cleanup_local_authorized_keys
|
|
continue
|
|
fi
|
|
|
|
print_info "Removing Pulse SSH keys from node ${node_ip}"
|
|
if ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
|
"sed -i -e '/# pulse-managed-key\$/d' -e '/# pulse-proxy-key\$/d' /root/.ssh/authorized_keys" 2>/dev/null; then
|
|
print_info " SSH keys cleaned on ${node_ip}"
|
|
else
|
|
print_warn " Unable to clean Pulse SSH keys on ${node_ip}"
|
|
fi
|
|
done
|
|
|
|
cleanup_local_authorized_keys
|
|
}
|
|
|
|
determine_installer_ref() {
|
|
if [[ -n "$REQUESTED_VERSION" && "$REQUESTED_VERSION" != "latest" && "$REQUESTED_VERSION" != "main" ]]; then
|
|
printf '%s' "$REQUESTED_VERSION"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "$LATEST_RELEASE_TAG" ]]; then
|
|
printf '%s' "$LATEST_RELEASE_TAG"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$REQUESTED_VERSION" == "main" ]]; then
|
|
printf 'main'
|
|
return 0
|
|
fi
|
|
|
|
printf 'main'
|
|
}
|
|
|
|
cache_installer_for_self_heal() {
|
|
INSTALLER_CACHE_REASON=""
|
|
install -d "$SHARE_DIR"
|
|
|
|
local source_issue=""
|
|
if [[ -n "$SCRIPT_SOURCE" && -f "$SCRIPT_SOURCE" ]]; then
|
|
if install -m 0755 "$SCRIPT_SOURCE" "$STORED_INSTALLER"; then
|
|
return 0
|
|
fi
|
|
source_issue="failed to copy ${SCRIPT_SOURCE}"
|
|
else
|
|
source_issue="no readable source"
|
|
fi
|
|
|
|
local repo="${GITHUB_REPO:-rcourtman/Pulse}"
|
|
local ref
|
|
ref="$(determine_installer_ref)"
|
|
[[ -z "$ref" ]] && ref="main"
|
|
|
|
local candidate_urls=()
|
|
if [[ "$ref" != "main" ]]; then
|
|
candidate_urls+=("https://github.com/${repo}/releases/download/${ref}/install-sensor-proxy.sh")
|
|
fi
|
|
candidate_urls+=("https://raw.githubusercontent.com/${repo}/${ref}/scripts/install-sensor-proxy.sh")
|
|
|
|
local tmp_file
|
|
tmp_file=$(mktemp)
|
|
local tmp_err
|
|
tmp_err=$(mktemp)
|
|
local last_error=""
|
|
|
|
for url in "${candidate_urls[@]}"; do
|
|
if curl --fail --silent --location --connect-timeout 10 --max-time 60 "$url" -o "$tmp_file" 2>"$tmp_err"; then
|
|
if install -m 0755 "$tmp_file" "$STORED_INSTALLER"; then
|
|
rm -f "$tmp_file" "$tmp_err"
|
|
if [[ "$QUIET" != true ]]; then
|
|
print_info "Cached installer script for self-heal from ${url}"
|
|
fi
|
|
return 0
|
|
fi
|
|
last_error="failed to write cached installer to ${STORED_INSTALLER}"
|
|
break
|
|
fi
|
|
|
|
if [[ -s "$tmp_err" ]]; then
|
|
last_error="$(cat "$tmp_err")"
|
|
else
|
|
last_error="HTTP error"
|
|
fi
|
|
: >"$tmp_err"
|
|
done
|
|
|
|
rm -f "$tmp_file" "$tmp_err"
|
|
|
|
if [[ -n "$source_issue" && -n "$last_error" ]]; then
|
|
INSTALLER_CACHE_REASON="${source_issue}; download failed (${last_error})"
|
|
elif [[ -n "$source_issue" ]]; then
|
|
INSTALLER_CACHE_REASON="$source_issue"
|
|
elif [[ -n "$last_error" ]]; then
|
|
INSTALLER_CACHE_REASON="download failed (${last_error})"
|
|
else
|
|
INSTALLER_CACHE_REASON="unknown failure"
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
perform_uninstall() {
|
|
print_info "Starting pulse-sensor-proxy uninstall..."
|
|
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
print_info "Stopping pulse-sensor-proxy service"
|
|
systemctl stop pulse-sensor-proxy 2>/dev/null || true
|
|
print_info "Disabling pulse-sensor-proxy service"
|
|
systemctl disable pulse-sensor-proxy 2>/dev/null || true
|
|
|
|
print_info "Stopping cleanup path watcher"
|
|
systemctl stop pulse-sensor-cleanup.path 2>/dev/null || true
|
|
systemctl disable pulse-sensor-cleanup.path 2>/dev/null || true
|
|
systemctl stop pulse-sensor-cleanup.service 2>/dev/null || true
|
|
systemctl disable pulse-sensor-cleanup.service 2>/dev/null || true
|
|
else
|
|
print_warn "systemctl not available; skipping service disable"
|
|
fi
|
|
|
|
if [[ -x "$CLEANUP_SCRIPT_PATH" ]]; then
|
|
print_info "Invoking cleanup script to remove Pulse SSH keys"
|
|
mkdir -p "$WORK_DIR"
|
|
cat > "$CLEANUP_REQUEST_PATH" <<'EOF'
|
|
{"host":""}
|
|
EOF
|
|
if "$CLEANUP_SCRIPT_PATH"; then
|
|
print_success "Cleanup script removed Pulse SSH keys"
|
|
else
|
|
print_warn "Cleanup script reported errors; attempting manual cleanup"
|
|
cleanup_cluster_authorized_keys_manual
|
|
fi
|
|
rm -f "$CLEANUP_REQUEST_PATH"
|
|
else
|
|
cleanup_cluster_authorized_keys_manual
|
|
fi
|
|
|
|
if [[ -f "$BINARY_PATH" ]]; then
|
|
rm -f "$BINARY_PATH"
|
|
print_success "Removed binary ${BINARY_PATH}"
|
|
else
|
|
print_info "Binary already absent at ${BINARY_PATH}"
|
|
fi
|
|
|
|
if [[ -f "$SERVICE_PATH" ]]; then
|
|
rm -f "$SERVICE_PATH"
|
|
print_success "Removed service unit ${SERVICE_PATH}"
|
|
fi
|
|
|
|
if [[ -f "$CLEANUP_PATH_UNIT" ]]; then
|
|
rm -f "$CLEANUP_PATH_UNIT"
|
|
print_success "Removed cleanup path unit ${CLEANUP_PATH_UNIT}"
|
|
fi
|
|
|
|
if [[ -f "$CLEANUP_SERVICE_UNIT" ]]; then
|
|
rm -f "$CLEANUP_SERVICE_UNIT"
|
|
print_success "Removed cleanup service unit ${CLEANUP_SERVICE_UNIT}"
|
|
fi
|
|
|
|
if [[ -f "$SELFHEAL_TIMER_UNIT" ]]; then
|
|
systemctl stop pulse-sensor-proxy-selfheal.timer 2>/dev/null || true
|
|
systemctl disable pulse-sensor-proxy-selfheal.timer 2>/dev/null || true
|
|
rm -f "$SELFHEAL_TIMER_UNIT"
|
|
print_success "Removed self-heal timer ${SELFHEAL_TIMER_UNIT}"
|
|
fi
|
|
|
|
if [[ -f "$SELFHEAL_SERVICE_UNIT" ]]; then
|
|
systemctl stop pulse-sensor-proxy-selfheal.service 2>/dev/null || true
|
|
systemctl disable pulse-sensor-proxy-selfheal.service 2>/dev/null || true
|
|
rm -f "$SELFHEAL_SERVICE_UNIT"
|
|
print_success "Removed self-heal service ${SELFHEAL_SERVICE_UNIT}"
|
|
fi
|
|
|
|
if [[ -f "$SELFHEAL_SCRIPT" ]]; then
|
|
rm -f "$SELFHEAL_SCRIPT"
|
|
print_success "Removed self-heal helper ${SELFHEAL_SCRIPT}"
|
|
fi
|
|
|
|
if [[ -f "$STORED_INSTALLER" ]]; then
|
|
rm -f "$STORED_INSTALLER"
|
|
print_success "Removed cached installer ${STORED_INSTALLER}"
|
|
fi
|
|
|
|
if [[ -f "$CTID_FILE" ]]; then
|
|
rm -f "$CTID_FILE"
|
|
fi
|
|
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
fi
|
|
|
|
rm -f "$CLEANUP_SCRIPT_PATH" "$CLEANUP_REQUEST_PATH" 2>/dev/null || true
|
|
rm -f "$SOCKET_PATH" 2>/dev/null || true
|
|
rm -rf "$RUNTIME_DIR" 2>/dev/null || true
|
|
|
|
# Always remove HTTP secrets and TLS material (security best practice)
|
|
if [[ -f "/etc/pulse-sensor-proxy/.http-auth-token" ]]; then
|
|
rm -f "/etc/pulse-sensor-proxy/.http-auth-token"
|
|
print_success "Removed HTTP auth token"
|
|
fi
|
|
if [[ -d "/etc/pulse-sensor-proxy/tls" ]]; then
|
|
rm -rf "/etc/pulse-sensor-proxy/tls"
|
|
print_success "Removed TLS certificates"
|
|
fi
|
|
|
|
# Check for and remove LXC bind mounts on any containers
|
|
if command -v pct >/dev/null 2>&1; then
|
|
print_info "Checking for LXC bind mounts..."
|
|
# Find all containers with pulse-sensor-proxy bind mounts
|
|
for ctid in $(pct list | awk 'NR>1 {print $1}'); do
|
|
if grep -q "pulse-sensor-proxy" /etc/pve/lxc/${ctid}.conf 2>/dev/null; then
|
|
CONTAINER_NAME=$(pct list | awk -v id="$ctid" '$1==id {print $3}')
|
|
print_info "Found bind mount in container $ctid ($CONTAINER_NAME)"
|
|
if sed -i '/pulse-sensor-proxy/d' /etc/pve/lxc/${ctid}.conf 2>/dev/null; then
|
|
print_success "Removed bind mount from container $ctid ($CONTAINER_NAME)"
|
|
print_warn "Container restart required for change to take effect"
|
|
else
|
|
print_warn "Failed to remove bind mount from container $ctid"
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ "$PURGE" == true ]]; then
|
|
print_info "Purging Pulse sensor proxy state"
|
|
rm -rf "$WORK_DIR" "$CONFIG_DIR" 2>/dev/null || true
|
|
if [[ -d "$LOG_DIR" ]]; then
|
|
print_info "Removing log directory ${LOG_DIR}"
|
|
fi
|
|
rm -rf "$LOG_DIR" 2>/dev/null || true
|
|
|
|
if id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
|
if userdel --remove "$SERVICE_USER" 2>/dev/null; then
|
|
print_success "Removed service user ${SERVICE_USER}"
|
|
elif userdel "$SERVICE_USER" 2>/dev/null; then
|
|
print_success "Removed service user ${SERVICE_USER}"
|
|
else
|
|
print_warn "Failed to remove service user ${SERVICE_USER}"
|
|
fi
|
|
fi
|
|
|
|
if getent group "$SERVICE_USER" >/dev/null 2>&1; then
|
|
if groupdel "$SERVICE_USER" 2>/dev/null; then
|
|
print_success "Removed service group ${SERVICE_USER}"
|
|
else
|
|
print_warn "Failed to remove service group ${SERVICE_USER}"
|
|
fi
|
|
fi
|
|
else
|
|
if [[ -d "$WORK_DIR" ]]; then
|
|
print_info "Preserving data directory ${WORK_DIR} (use --purge to remove)"
|
|
fi
|
|
if [[ -d "$CONFIG_DIR" ]]; then
|
|
print_info "Preserving config directory ${CONFIG_DIR} (use --purge to remove)"
|
|
fi
|
|
if [[ -d "$LOG_DIR" ]]; then
|
|
print_info "Preserving log directory ${LOG_DIR} (use --purge to remove)"
|
|
fi
|
|
fi
|
|
|
|
print_success "pulse-sensor-proxy uninstall complete"
|
|
}
|
|
|
|
# Parse arguments first to check for standalone mode
|
|
CTID=""
|
|
VERSION="latest"
|
|
LOCAL_BINARY=""
|
|
QUIET=false
|
|
PULSE_SERVER=""
|
|
STANDALONE=false
|
|
HTTP_MODE=false
|
|
HTTP_ADDR=":8443"
|
|
FALLBACK_BASE="${PULSE_SENSOR_PROXY_FALLBACK_URL:-}"
|
|
SKIP_RESTART=false
|
|
UNINSTALL=false
|
|
PURGE=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--ctid)
|
|
CTID="$2"
|
|
shift 2
|
|
;;
|
|
--version)
|
|
VERSION="$2"
|
|
shift 2
|
|
;;
|
|
--local-binary)
|
|
LOCAL_BINARY="$2"
|
|
shift 2
|
|
;;
|
|
--pulse-server)
|
|
PULSE_SERVER="$2"
|
|
shift 2
|
|
;;
|
|
--quiet)
|
|
QUIET=true
|
|
shift
|
|
;;
|
|
--standalone)
|
|
STANDALONE=true
|
|
shift
|
|
;;
|
|
--http-mode)
|
|
HTTP_MODE=true
|
|
shift
|
|
;;
|
|
--http-addr)
|
|
HTTP_ADDR="$2"
|
|
shift 2
|
|
;;
|
|
--skip-restart)
|
|
SKIP_RESTART=true
|
|
shift
|
|
;;
|
|
--uninstall)
|
|
UNINSTALL=true
|
|
shift
|
|
;;
|
|
--purge)
|
|
PURGE=true
|
|
shift
|
|
;;
|
|
*)
|
|
print_error "Unknown option: $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$PURGE" == true && "$UNINSTALL" != true ]]; then
|
|
print_warn "--purge is only valid together with --uninstall; ignoring"
|
|
PURGE=false
|
|
fi
|
|
|
|
if [[ "$UNINSTALL" == true ]]; then
|
|
perform_uninstall
|
|
exit 0
|
|
fi
|
|
|
|
REQUESTED_VERSION="${VERSION:-latest}"
|
|
|
|
# If --pulse-server was provided, use it as the fallback base
|
|
if [[ -n "$PULSE_SERVER" ]]; then
|
|
FALLBACK_BASE="${PULSE_SERVER}/api/install/pulse-sensor-proxy"
|
|
fi
|
|
|
|
# Preflight checks
|
|
if [[ $EUID -ne 0 ]]; then
|
|
print_error "This script must be run as root"
|
|
print_error "Use: sudo $0 $*"
|
|
exit 1
|
|
fi
|
|
|
|
# Check required commands
|
|
REQUIRED_CMDS="curl openssl systemctl useradd groupadd install chmod chown mkdir"
|
|
if [[ "$HTTP_MODE" == true ]]; then
|
|
REQUIRED_CMDS="$REQUIRED_CMDS hostname awk"
|
|
fi
|
|
if [[ "$STANDALONE" == false ]]; then
|
|
REQUIRED_CMDS="$REQUIRED_CMDS pvecm"
|
|
fi
|
|
|
|
for cmd in $REQUIRED_CMDS; do
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
print_error "Required command not found: $cmd"
|
|
print_error "Please install it and try again"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Check if running on Proxmox host (only required for LXC mode)
|
|
if [[ "$STANDALONE" == false ]]; then
|
|
if ! command -v pvecm >/dev/null 2>&1; then
|
|
print_error "This script must be run on a Proxmox VE host"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Validate arguments based on mode
|
|
if [[ "$STANDALONE" == false ]]; then
|
|
if [[ -z "$CTID" ]]; then
|
|
print_error "Missing required argument: --ctid <container-id>"
|
|
echo "Usage: $0 --ctid <container-id> [--pulse-server <url>] [--version <version>] [--local-binary <path>]"
|
|
echo " Or: $0 --standalone [--pulse-server <url>] [--version <version>] [--local-binary <path>]"
|
|
echo " Or: $0 --uninstall [--purge]"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify container exists
|
|
if ! pct status "$CTID" >/dev/null 2>&1; then
|
|
print_error "Container $CTID does not exist"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$STANDALONE" == true ]]; then
|
|
print_info "Installing pulse-sensor-proxy for standalone/Docker deployment"
|
|
else
|
|
print_info "Installing pulse-sensor-proxy for container $CTID"
|
|
fi
|
|
|
|
# Create dedicated service account if it doesn't exist
|
|
if ! id -u pulse-sensor-proxy >/dev/null 2>&1; then
|
|
print_info "Creating pulse-sensor-proxy service account..."
|
|
useradd --system --user-group --no-create-home --shell /usr/sbin/nologin pulse-sensor-proxy
|
|
print_info "Service account created"
|
|
else
|
|
print_info "Service account pulse-sensor-proxy already exists"
|
|
fi
|
|
|
|
# Add pulse-sensor-proxy user to www-data group for Proxmox IPC access (pvecm commands)
|
|
if ! groups pulse-sensor-proxy | grep -q '\bwww-data\b'; then
|
|
print_info "Adding pulse-sensor-proxy to www-data group for Proxmox IPC access..."
|
|
usermod -aG www-data pulse-sensor-proxy
|
|
fi
|
|
|
|
# Create installation directories before binary installation (handles fresh installs and upgrades)
|
|
print_info "Setting up installation directories..."
|
|
install -d -o root -g root -m 0755 "${INSTALL_ROOT}"
|
|
install -d -o root -g root -m 0755 "${INSTALL_ROOT}/bin"
|
|
|
|
# Install binary - either from local file or download from GitHub
|
|
if [[ -n "$LOCAL_BINARY" ]]; then
|
|
# Use local binary for testing
|
|
print_info "Using local binary: $LOCAL_BINARY"
|
|
if [[ ! -f "$LOCAL_BINARY" ]]; then
|
|
print_error "Local binary not found: $LOCAL_BINARY"
|
|
exit 1
|
|
fi
|
|
cp "$LOCAL_BINARY" "$BINARY_PATH"
|
|
chmod +x "$BINARY_PATH"
|
|
print_info "Binary installed to $BINARY_PATH"
|
|
else
|
|
# Detect architecture
|
|
ARCH=$(uname -m)
|
|
case $ARCH in
|
|
x86_64)
|
|
BINARY_NAME="pulse-sensor-proxy-linux-amd64"
|
|
ARCH_LABEL="linux-amd64"
|
|
;;
|
|
aarch64|arm64)
|
|
BINARY_NAME="pulse-sensor-proxy-linux-arm64"
|
|
ARCH_LABEL="linux-arm64"
|
|
;;
|
|
armv7l|armhf)
|
|
BINARY_NAME="pulse-sensor-proxy-linux-armv7"
|
|
ARCH_LABEL="linux-armv7"
|
|
;;
|
|
armv6l)
|
|
BINARY_NAME="pulse-sensor-proxy-linux-armv6"
|
|
ARCH_LABEL="linux-armv6"
|
|
;;
|
|
i386|i686)
|
|
BINARY_NAME="pulse-sensor-proxy-linux-386"
|
|
ARCH_LABEL="linux-386"
|
|
;;
|
|
*)
|
|
print_error "Unsupported architecture: $ARCH"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
DOWNLOAD_SUCCESS=false
|
|
ATTEMPTED_SOURCES=()
|
|
|
|
fetch_latest_release_tag() {
|
|
local api_url="https://api.github.com/repos/$GITHUB_REPO/releases?per_page=25"
|
|
local tmp_err
|
|
tmp_err=$(mktemp)
|
|
local response
|
|
response=$(curl --fail --silent --location --connect-timeout 10 --max-time 30 "$api_url" 2>"$tmp_err")
|
|
local status=$?
|
|
if [[ $status -ne 0 ]]; then
|
|
if [[ -s "$tmp_err" ]]; then
|
|
print_warn "Failed to resolve latest GitHub release: $(cat "$tmp_err")"
|
|
else
|
|
print_warn "Failed to resolve latest GitHub release (HTTP $status)"
|
|
fi
|
|
rm -f "$tmp_err"
|
|
return 1
|
|
fi
|
|
rm -f "$tmp_err"
|
|
|
|
local tag=""
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
if ! tag=$(printf '%s' "$response" | python3 -c '
|
|
import json
|
|
import sys
|
|
|
|
binary_name = sys.argv[1]
|
|
arch_label = sys.argv[2]
|
|
tar_suffix = arch_label or ""
|
|
|
|
def has_sensor_assets(tag, assets):
|
|
names = {asset.get("name") for asset in assets if isinstance(asset, dict) and asset.get("name")}
|
|
if binary_name and binary_name in names:
|
|
return True
|
|
if tar_suffix:
|
|
tarball = f"pulse-{tag}-{tar_suffix}.tar.gz"
|
|
if tarball in names or f"{tarball}.sha256" in names:
|
|
return True
|
|
universal = f"pulse-{tag}.tar.gz"
|
|
if universal in names or f"{universal}.sha256" in names:
|
|
return True
|
|
return False
|
|
|
|
try:
|
|
releases = json.load(sys.stdin)
|
|
except json.JSONDecodeError:
|
|
sys.exit(1)
|
|
|
|
for release in releases:
|
|
tag_name = (release.get("tag_name") or "").strip()
|
|
if not tag_name or tag_name.startswith("helm-chart"):
|
|
continue
|
|
assets = release.get("assets") or []
|
|
if has_sensor_assets(tag_name, assets):
|
|
sys.stdout.write(tag_name)
|
|
sys.exit(0)
|
|
|
|
sys.exit(0)
|
|
' "$BINARY_NAME" "$ARCH_LABEL"); then
|
|
print_warn "Failed to parse GitHub releases via python3; falling back to heuristic tag detection"
|
|
tag=""
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$tag" ]]; then
|
|
tag=$(printf '%s\n' "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4 | grep -Ev '^helm-chart-' | head -n 1 || true)
|
|
fi
|
|
|
|
if [[ -n "$tag" ]]; then
|
|
tag="${tag%%$'\n'*}"
|
|
fi
|
|
|
|
if [[ -z "$tag" ]]; then
|
|
print_warn "Could not determine latest GitHub release for pulse-sensor-proxy"
|
|
return 1
|
|
fi
|
|
LATEST_RELEASE_TAG="$tag"
|
|
return 0
|
|
}
|
|
|
|
attempt_github_asset_or_tarball() {
|
|
local tag="$1"
|
|
[[ -z "$tag" ]] && return 1
|
|
|
|
local asset_url="https://github.com/$GITHUB_REPO/releases/download/${tag}/${BINARY_NAME}"
|
|
ATTEMPTED_SOURCES+=("GitHub release asset ${tag}")
|
|
print_info "Downloading $BINARY_NAME from GitHub release ${tag}..."
|
|
local tmp_err
|
|
tmp_err=$(mktemp)
|
|
if curl --fail --silent --location --connect-timeout 10 --max-time 120 "$asset_url" -o "$BINARY_PATH.tmp" 2>"$tmp_err"; then
|
|
rm -f "$tmp_err"
|
|
DOWNLOAD_SUCCESS=true
|
|
return 0
|
|
fi
|
|
|
|
local asset_error=""
|
|
if [[ -s "$tmp_err" ]]; then
|
|
asset_error="$(cat "$tmp_err")"
|
|
fi
|
|
rm -f "$tmp_err"
|
|
rm -f "$BINARY_PATH.tmp" 2>/dev/null || true
|
|
|
|
local tarball_name="pulse-${tag}-linux-${ARCH_LABEL#linux-}.tar.gz"
|
|
local tarball_url="https://github.com/$GITHUB_REPO/releases/download/${tag}/${tarball_name}"
|
|
ATTEMPTED_SOURCES+=("GitHub release tarball ${tarball_name}")
|
|
print_info "Downloading ${tarball_name} to extract pulse-sensor-proxy..."
|
|
tmp_err=$(mktemp)
|
|
local tarball_tmp
|
|
tarball_tmp=$(mktemp)
|
|
if curl --fail --silent --location --connect-timeout 10 --max-time 240 "$tarball_url" -o "$tarball_tmp" 2>"$tmp_err"; then
|
|
if tar -tzf "$tarball_tmp" >/dev/null 2>&1 && tar -xzf "$tarball_tmp" -C "$(dirname "$tarball_tmp")" ./bin/pulse-sensor-proxy >/dev/null 2>&1; then
|
|
mv "$(dirname "$tarball_tmp")/bin/pulse-sensor-proxy" "$BINARY_PATH.tmp"
|
|
rm -f "$tarball_tmp" "$tmp_err"
|
|
DOWNLOAD_SUCCESS=true
|
|
return 0
|
|
else
|
|
print_warn "Release tarball did not contain expected ./bin/pulse-sensor-proxy"
|
|
fi
|
|
else
|
|
if [[ -s "$tmp_err" ]]; then
|
|
print_warn "Tarball download failed: $(cat "$tmp_err")"
|
|
else
|
|
print_warn "Tarball download failed (HTTP error)"
|
|
fi
|
|
fi
|
|
rm -f "$tarball_tmp" "$tmp_err"
|
|
if [[ -n "$asset_error" ]]; then
|
|
print_warn "GitHub release asset error: $asset_error"
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
if [[ "$REQUESTED_VERSION" == "latest" || "$REQUESTED_VERSION" == "main" || -z "$REQUESTED_VERSION" ]]; then
|
|
if fetch_latest_release_tag; then
|
|
attempt_github_asset_or_tarball "$LATEST_RELEASE_TAG" || true
|
|
fi
|
|
else
|
|
attempt_github_asset_or_tarball "$REQUESTED_VERSION" || true
|
|
fi
|
|
|
|
if [[ "$DOWNLOAD_SUCCESS" != true ]] && [[ -n "$FALLBACK_BASE" ]]; then
|
|
fallback_url="$FALLBACK_BASE"
|
|
if [[ "$fallback_url" == *"?"* ]]; then
|
|
fallback_url="$fallback_url"
|
|
elif [[ "$fallback_url" == *"pulse-sensor-proxy-"* ]]; then
|
|
fallback_url="${fallback_url}"
|
|
else
|
|
fallback_url="${fallback_url%/}?arch=${ARCH_LABEL}"
|
|
fi
|
|
|
|
ATTEMPTED_SOURCES+=("Fallback ${fallback_url}")
|
|
print_info "Downloading $BINARY_NAME from fallback source..."
|
|
fallback_err=$(mktemp)
|
|
if curl --fail --silent --location --connect-timeout 10 --max-time 120 "$fallback_url" -o "$BINARY_PATH.tmp" 2>"$fallback_err"; then
|
|
rm -f "$fallback_err"
|
|
DOWNLOAD_SUCCESS=true
|
|
else
|
|
if [[ -s "$fallback_err" ]]; then
|
|
print_error "Fallback download failed: $(cat "$fallback_err")"
|
|
else
|
|
print_error "Fallback download failed (HTTP error)"
|
|
fi
|
|
rm -f "$fallback_err"
|
|
rm -f "$BINARY_PATH.tmp" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$DOWNLOAD_SUCCESS" != true ]] && [[ -n "$CTID" ]] && command -v pct >/dev/null 2>&1; then
|
|
pull_targets=(
|
|
"/opt/pulse/bin/${BINARY_NAME}"
|
|
"/opt/pulse/bin/pulse-sensor-proxy"
|
|
)
|
|
for src in "${pull_targets[@]}"; do
|
|
tmp_pull=$(mktemp)
|
|
if pct pull "$CTID" "$src" "$tmp_pull" >/dev/null 2>&1; then
|
|
mv "$tmp_pull" "$BINARY_PATH.tmp"
|
|
print_info "Copied pulse-sensor-proxy binary from container $CTID ($src)"
|
|
DOWNLOAD_SUCCESS=true
|
|
break
|
|
fi
|
|
rm -f "$tmp_pull"
|
|
done
|
|
fi
|
|
|
|
if [[ "$DOWNLOAD_SUCCESS" != true ]]; then
|
|
print_error "Unable to download pulse-sensor-proxy binary."
|
|
if [[ ${#ATTEMPTED_SOURCES[@]} -gt 0 ]]; then
|
|
print_error "Sources attempted:"
|
|
for src in "${ATTEMPTED_SOURCES[@]}"; do
|
|
print_error " - $src"
|
|
done
|
|
fi
|
|
print_error "Publish a GitHub release with binary assets or ensure a Pulse server is reachable."
|
|
exit 1
|
|
fi
|
|
|
|
chmod +x "$BINARY_PATH.tmp"
|
|
mv "$BINARY_PATH.tmp" "$BINARY_PATH"
|
|
print_info "Binary installed to $BINARY_PATH"
|
|
fi
|
|
|
|
# Create remaining directories with proper ownership (handles fresh installs and upgrades)
|
|
print_info "Setting up service directories with proper ownership..."
|
|
install -d -o pulse-sensor-proxy -g pulse-sensor-proxy -m 0750 /var/lib/pulse-sensor-proxy
|
|
install -d -o pulse-sensor-proxy -g pulse-sensor-proxy -m 0700 "$SSH_DIR"
|
|
install -m 0600 -o pulse-sensor-proxy -g pulse-sensor-proxy /dev/null "$SSH_DIR/known_hosts"
|
|
install -d -o pulse-sensor-proxy -g pulse-sensor-proxy -m 0755 /etc/pulse-sensor-proxy
|
|
|
|
if [[ -n "$CTID" ]]; then
|
|
echo "$CTID" > "$CTID_FILE"
|
|
chmod 0644 "$CTID_FILE"
|
|
fi
|
|
|
|
# HTTP Mode Setup Functions
|
|
setup_tls_certificates() {
|
|
local cert_path="$1"
|
|
local key_path="$2"
|
|
|
|
# Create TLS directory
|
|
install -d -o root -g pulse-sensor-proxy -m 0750 /etc/pulse-sensor-proxy/tls
|
|
|
|
if [[ -n "$cert_path" && -n "$key_path" ]]; then
|
|
# Use provided certificates
|
|
print_info "Using provided TLS certificates..."
|
|
cp "$cert_path" /etc/pulse-sensor-proxy/tls/server.crt
|
|
cp "$key_path" /etc/pulse-sensor-proxy/tls/server.key
|
|
chmod 640 /etc/pulse-sensor-proxy/tls/server.crt
|
|
chmod 640 /etc/pulse-sensor-proxy/tls/server.key
|
|
chown root:pulse-sensor-proxy /etc/pulse-sensor-proxy/tls/server.crt
|
|
chown root:pulse-sensor-proxy /etc/pulse-sensor-proxy/tls/server.key
|
|
else
|
|
# Generate self-signed certificate
|
|
print_info "Generating self-signed TLS certificate..."
|
|
|
|
# Get hostname and IPs for SAN
|
|
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
|
IP_ADDRESSES=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v '^$' | head -5)
|
|
|
|
# Build SAN list
|
|
SAN="DNS:${HOSTNAME},DNS:localhost"
|
|
for ip in $IP_ADDRESSES; do
|
|
SAN="${SAN},IP:${ip}"
|
|
done
|
|
|
|
# Generate 4096-bit RSA key and self-signed cert valid for 10 years
|
|
openssl req -newkey rsa:4096 -nodes -x509 -days 3650 \
|
|
-subj "/CN=${HOSTNAME}/O=Pulse Sensor Proxy" \
|
|
-addext "subjectAltName=${SAN}" \
|
|
-keyout /etc/pulse-sensor-proxy/tls/server.key \
|
|
-out /etc/pulse-sensor-proxy/tls/server.crt \
|
|
2>/dev/null || {
|
|
print_error "Failed to generate TLS certificate"
|
|
exit 1
|
|
}
|
|
|
|
chmod 640 /etc/pulse-sensor-proxy/tls/server.key
|
|
chmod 640 /etc/pulse-sensor-proxy/tls/server.crt
|
|
chown root:pulse-sensor-proxy /etc/pulse-sensor-proxy/tls/server.key
|
|
chown root:pulse-sensor-proxy /etc/pulse-sensor-proxy/tls/server.crt
|
|
|
|
# Log certificate fingerprint for audit
|
|
CERT_FINGERPRINT=$(openssl x509 -in /etc/pulse-sensor-proxy/tls/server.crt -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2)
|
|
print_success "TLS certificate generated (SHA256: ${CERT_FINGERPRINT})"
|
|
fi
|
|
}
|
|
|
|
register_with_pulse() {
|
|
local pulse_url="$1"
|
|
local hostname="$2"
|
|
local proxy_url="$3"
|
|
|
|
# Output to stderr so it doesn't interfere with command substitution
|
|
print_info "Registering temperature proxy with Pulse at $pulse_url..." >&2
|
|
|
|
# Build registration request with retry logic
|
|
local response
|
|
local http_code
|
|
local attempt
|
|
local max_attempts=3
|
|
local register_url="${pulse_url}/api/temperature-proxy/register"
|
|
|
|
for attempt in $(seq 1 $max_attempts); do
|
|
if [[ $attempt -gt 1 ]]; then
|
|
print_info "Retry attempt $attempt/$max_attempts..." >&2
|
|
sleep 2
|
|
fi
|
|
|
|
# Capture both HTTP code and response body
|
|
response=$(curl -w "\n%{http_code}" -f -s -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"hostname\":\"${hostname}\",\"proxy_url\":\"${proxy_url}\"}" \
|
|
"$register_url" 2>&1)
|
|
|
|
local curl_exit=$?
|
|
http_code=$(echo "$response" | tail -1)
|
|
response=$(echo "$response" | head -n -1)
|
|
|
|
if [[ $curl_exit -eq 0 ]]; then
|
|
break
|
|
fi
|
|
|
|
if [[ $attempt -eq $max_attempts ]]; then
|
|
print_error "Failed to register with Pulse API after $max_attempts attempts" >&2
|
|
print_error "" >&2
|
|
print_error "═══════════════════════════════════════════════════════" >&2
|
|
print_error "Registration Details:" >&2
|
|
print_error "═══════════════════════════════════════════════════════" >&2
|
|
print_error "URL: $register_url" >&2
|
|
print_error "HTTP Code: $http_code" >&2
|
|
print_error "Hostname: $hostname" >&2
|
|
print_error "Proxy URL: $proxy_url" >&2
|
|
print_error "Response: $response" >&2
|
|
print_error "" >&2
|
|
print_error "Troubleshooting:" >&2
|
|
print_error " 1. Ensure this PVE instance is added to Pulse first" >&2
|
|
print_error " 2. Verify hostname matches instance name: $hostname" >&2
|
|
print_error " 3. Check Pulse logs: docker logs pulse | tail -50" >&2
|
|
print_error " 4. Test registration manually:" >&2
|
|
print_error " curl -X POST -H 'Content-Type: application/json' \\" >&2
|
|
print_error " -d '{\"hostname\":\"${hostname}\",\"proxy_url\":\"${proxy_url}\"}' \\" >&2
|
|
print_error " $register_url" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
# Parse token from response
|
|
local token
|
|
token=$(echo "$response" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
|
|
|
|
if [[ -z "$token" ]]; then
|
|
print_error "Registration succeeded but no token received"
|
|
print_error "Response: $response"
|
|
return 1
|
|
fi
|
|
|
|
# Store token
|
|
echo "$token" > /etc/pulse-sensor-proxy/.http-auth-token
|
|
chmod 600 /etc/pulse-sensor-proxy/.http-auth-token
|
|
chown pulse-sensor-proxy:pulse-sensor-proxy /etc/pulse-sensor-proxy/.http-auth-token
|
|
|
|
# Output to stderr so it doesn't interfere with command substitution
|
|
print_success "Registered successfully - token received" >&2
|
|
|
|
# Parse instance name from response for logging
|
|
local instance_name
|
|
instance_name=$(echo "$response" | grep -o '"pve_instance":"[^"]*"' | cut -d'"' -f4)
|
|
if [[ -n "$instance_name" ]]; then
|
|
print_info "Linked to PVE instance: $instance_name" >&2
|
|
fi
|
|
|
|
# Return token for caller to use (stdout only)
|
|
echo "$token"
|
|
}
|
|
|
|
# Create base config file if it doesn't exist
|
|
if [[ ! -f /etc/pulse-sensor-proxy/config.yaml ]]; then
|
|
print_info "Creating base configuration file..."
|
|
cat > /etc/pulse-sensor-proxy/config.yaml << 'EOF'
|
|
# Pulse Temperature Proxy Configuration
|
|
allowed_peer_uids: [1000]
|
|
|
|
# Allow ID-mapped root (LXC containers with sub-UID mapping)
|
|
allow_idmapped_root: true
|
|
allowed_idmap_users:
|
|
- root
|
|
|
|
metrics_address: "127.0.0.1:9127"
|
|
|
|
rate_limit:
|
|
per_peer_interval_ms: 333
|
|
per_peer_burst: 10
|
|
EOF
|
|
chown pulse-sensor-proxy:pulse-sensor-proxy /etc/pulse-sensor-proxy/config.yaml
|
|
chmod 0644 /etc/pulse-sensor-proxy/config.yaml
|
|
fi
|
|
|
|
# HTTP Mode Configuration
|
|
if [[ "$HTTP_MODE" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo " HTTP Mode Setup (External PVE Host)"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
|
|
# Validate required parameters
|
|
if [[ -z "$PULSE_SERVER" ]]; then
|
|
print_error "HTTP mode requires --pulse-server parameter"
|
|
print_error "Example: --pulse-server https://pulse.example.com:7655"
|
|
exit 1
|
|
fi
|
|
|
|
# Test Pulse server reachability before proceeding
|
|
print_info "Testing connection to Pulse server..."
|
|
if ! curl -f -s -m 5 "${PULSE_SERVER}/api/health" >/dev/null 2>&1; then
|
|
print_error "Cannot reach Pulse server at: $PULSE_SERVER"
|
|
print_error ""
|
|
print_error "Troubleshooting:"
|
|
print_error " 1. Verify Pulse is running: docker ps | grep pulse"
|
|
print_error " 2. Check URL is correct (include protocol and port)"
|
|
print_error " 3. Test connectivity: curl -v ${PULSE_SERVER}/api/health"
|
|
print_error " 4. Check firewall allows access from this host"
|
|
print_error ""
|
|
print_error "Installation aborted to prevent incomplete setup"
|
|
exit 1
|
|
fi
|
|
print_success "Pulse server is reachable"
|
|
|
|
# Check if port is already in use
|
|
PORT_NUMBER="${HTTP_ADDR#:}"
|
|
if ss -ltn | grep -q ":${PORT_NUMBER} "; then
|
|
print_error "Port ${PORT_NUMBER} is already in use"
|
|
print_error ""
|
|
print_error "Currently using port ${PORT_NUMBER}:"
|
|
ss -ltnp | grep ":${PORT_NUMBER} " || true
|
|
print_error ""
|
|
print_error "Options:"
|
|
print_error " 1. Stop the conflicting service"
|
|
print_error " 2. Use a different port: --http-addr :PORT"
|
|
print_error " 3. If this is a previous sensor-proxy, uninstall first:"
|
|
print_error " $0 --uninstall"
|
|
exit 1
|
|
fi
|
|
|
|
# Setup TLS certificates
|
|
setup_tls_certificates "" "" # Empty params = auto-generate
|
|
|
|
# Determine proxy URL - use IP address for reliable network access
|
|
PRIMARY_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
if [[ -z "$PRIMARY_IP" ]]; then
|
|
print_error "Failed to determine primary IP address"
|
|
print_error "Use --proxy-url to specify manually"
|
|
exit 1
|
|
fi
|
|
|
|
# Validate it's an IPv4 address (not IPv6)
|
|
if [[ ! "$PRIMARY_IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
print_warn "Primary IP appears to be IPv6 or invalid: $PRIMARY_IP"
|
|
print_warn "Attempting to find first IPv4 address..."
|
|
PRIMARY_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
|
|
if [[ -z "$PRIMARY_IP" ]]; then
|
|
print_error "No IPv4 address found"
|
|
print_error "Use --proxy-url https://YOUR_IP${HTTP_ADDR} to specify manually"
|
|
exit 1
|
|
fi
|
|
print_info "Using IPv4 address: $PRIMARY_IP"
|
|
fi
|
|
|
|
# Warn if using loopback
|
|
if [[ "$PRIMARY_IP" == "127.0.0.1" ]]; then
|
|
print_warn "Primary IP is loopback (127.0.0.1)"
|
|
print_warn "Pulse will not be able to reach this proxy!"
|
|
print_error "Use --proxy-url https://YOUR_REAL_IP${HTTP_ADDR} to specify a reachable address"
|
|
exit 1
|
|
fi
|
|
|
|
PROXY_URL="https://${PRIMARY_IP}${HTTP_ADDR}"
|
|
print_info "Proxy will be accessible at: $PROXY_URL"
|
|
|
|
# Register with Pulse and get auth token
|
|
# Use short hostname for registration matching (PVE cluster endpoints use short names)
|
|
SHORT_HOSTNAME=$(hostname -s 2>/dev/null || hostname | cut -d'.' -f1)
|
|
HTTP_AUTH_TOKEN=$(register_with_pulse "$PULSE_SERVER" "$SHORT_HOSTNAME" "$PROXY_URL")
|
|
if [[ $? -ne 0 || -z "$HTTP_AUTH_TOKEN" ]]; then
|
|
print_error "Failed to register with Pulse - aborting installation"
|
|
print_error "Fix the issue and re-run the installer"
|
|
exit 1
|
|
fi
|
|
|
|
# Backup config before modifying
|
|
if [[ -f /etc/pulse-sensor-proxy/config.yaml ]]; then
|
|
BACKUP_CONFIG="/etc/pulse-sensor-proxy/config.yaml.backup.$(date +%s)"
|
|
cp /etc/pulse-sensor-proxy/config.yaml "$BACKUP_CONFIG"
|
|
print_info "Config backed up to: $BACKUP_CONFIG"
|
|
|
|
# Remove any existing HTTP configuration to prevent duplicates
|
|
if grep -q "^# HTTP Mode Configuration" /etc/pulse-sensor-proxy/config.yaml; then
|
|
print_info "Removing existing HTTP configuration..."
|
|
# Remove from "# HTTP Mode Configuration" to end of file
|
|
sed -i '/^# HTTP Mode Configuration/,$ d' /etc/pulse-sensor-proxy/config.yaml
|
|
fi
|
|
fi
|
|
|
|
# Extract Pulse server IP/hostname for allowed_source_subnets
|
|
# Remove protocol and port to get just the host
|
|
PULSE_HOST=$(echo "$PULSE_SERVER" | sed -E 's#^https?://##' | sed -E 's#:[0-9]+$##')
|
|
|
|
# Try to resolve to IP if it's a hostname
|
|
PULSE_IP=$(getent hosts "$PULSE_HOST" 2>/dev/null | awk '{print $1; exit}')
|
|
if [[ -z "$PULSE_IP" ]]; then
|
|
# Fallback: assume PULSE_HOST is already an IP or use it as-is
|
|
PULSE_IP="$PULSE_HOST"
|
|
fi
|
|
|
|
print_info "Pulse server detected at: $PULSE_IP"
|
|
|
|
# Append HTTP configuration to config.yaml
|
|
print_info "Configuring HTTP mode..."
|
|
cat >> /etc/pulse-sensor-proxy/config.yaml << EOF
|
|
|
|
# HTTP Mode Configuration (External PVE Host)
|
|
http_enabled: true
|
|
http_listen_addr: "$HTTP_ADDR"
|
|
http_tls_cert: /etc/pulse-sensor-proxy/tls/server.crt
|
|
http_tls_key: /etc/pulse-sensor-proxy/tls/server.key
|
|
http_auth_token: "$HTTP_AUTH_TOKEN"
|
|
|
|
# Allow HTTP connections from Pulse server and localhost (for self-monitoring)
|
|
allowed_source_subnets:
|
|
- $PULSE_IP/32
|
|
- 127.0.0.1/32
|
|
EOF
|
|
|
|
print_success "HTTP mode configured successfully"
|
|
echo ""
|
|
print_info "Firewall configuration required:"
|
|
print_info " Allow inbound TCP connections on port ${HTTP_ADDR#:} from Pulse server"
|
|
print_info " Command: ufw allow from <pulse-server-ip> to any port ${HTTP_ADDR#:}"
|
|
echo ""
|
|
fi
|
|
|
|
# Stop existing service if running (for upgrades)
|
|
if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then
|
|
print_info "Stopping existing service for upgrade..."
|
|
systemctl stop pulse-sensor-proxy
|
|
fi
|
|
|
|
# Install hardened systemd service
|
|
print_info "Installing hardened systemd service..."
|
|
|
|
# Generate service file based on mode (Proxmox vs standalone)
|
|
if [[ "$STANDALONE" == true ]]; then
|
|
# Standalone/Docker mode - no Proxmox-specific paths
|
|
cat > "$SERVICE_PATH" <<EOF
|
|
[Unit]
|
|
Description=Pulse Temperature Proxy
|
|
Documentation=https://github.com/rcourtman/Pulse
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=pulse-sensor-proxy
|
|
Group=pulse-sensor-proxy
|
|
WorkingDirectory=/var/lib/pulse-sensor-proxy
|
|
ExecStart=${BINARY_PATH} --config /etc/pulse-sensor-proxy/config.yaml
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
|
|
# Runtime dirs/sockets
|
|
RuntimeDirectory=pulse-sensor-proxy
|
|
RuntimeDirectoryMode=0775
|
|
RuntimeDirectoryPreserve=yes
|
|
LogsDirectory=pulse/sensor-proxy
|
|
LogsDirectoryMode=0750
|
|
UMask=0007
|
|
|
|
# Core hardening
|
|
NoNewPrivileges=true
|
|
ProtectSystem=strict
|
|
ProtectHome=read-only
|
|
ReadWritePaths=/var/lib/pulse-sensor-proxy
|
|
ReadWritePaths=-/run/corosync
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
ProtectClock=true
|
|
PrivateTmp=true
|
|
PrivateDevices=true
|
|
ProtectProc=invisible
|
|
ProcSubset=pid
|
|
LockPersonality=true
|
|
RestrictSUIDSGID=true
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
|
RestrictNamespaces=true
|
|
SystemCallFilter=@system-service
|
|
SystemCallErrorNumber=EPERM
|
|
CapabilityBoundingSet=
|
|
AmbientCapabilities=
|
|
KeyringMode=private
|
|
LimitNOFILE=1024
|
|
|
|
# Logging
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=pulse-sensor-proxy
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
else
|
|
# Proxmox mode - include Proxmox paths
|
|
cat > "$SERVICE_PATH" <<EOF
|
|
[Unit]
|
|
Description=Pulse Temperature Proxy
|
|
Documentation=https://github.com/rcourtman/Pulse
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=pulse-sensor-proxy
|
|
Group=pulse-sensor-proxy
|
|
SupplementaryGroups=www-data
|
|
WorkingDirectory=/var/lib/pulse-sensor-proxy
|
|
ExecStart=${BINARY_PATH}
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
|
|
# Runtime dirs/sockets
|
|
RuntimeDirectory=pulse-sensor-proxy
|
|
RuntimeDirectoryMode=0775
|
|
RuntimeDirectoryPreserve=yes
|
|
LogsDirectory=pulse/sensor-proxy
|
|
LogsDirectoryMode=0750
|
|
UMask=0007
|
|
|
|
# Core hardening
|
|
NoNewPrivileges=true
|
|
ProtectSystem=strict
|
|
ProtectHome=read-only
|
|
ReadWritePaths=/var/lib/pulse-sensor-proxy
|
|
ReadWritePaths=-/run/corosync
|
|
ReadOnlyPaths=/run/pve-cluster /etc/pve
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
ProtectClock=true
|
|
PrivateTmp=true
|
|
PrivateDevices=true
|
|
ProtectProc=invisible
|
|
ProcSubset=pid
|
|
LockPersonality=true
|
|
RestrictSUIDSGID=true
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
|
RestrictNamespaces=true
|
|
SystemCallFilter=@system-service
|
|
SystemCallErrorNumber=EPERM
|
|
CapabilityBoundingSet=
|
|
AmbientCapabilities=
|
|
KeyringMode=private
|
|
LimitNOFILE=1024
|
|
|
|
# Logging
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=pulse-sensor-proxy
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
fi
|
|
|
|
# Add HTTP mode paths to service file if needed
|
|
if [[ "$HTTP_MODE" == true ]]; then
|
|
print_info "Updating service file for HTTP mode..."
|
|
|
|
# Add ReadOnlyPaths for TLS directory (after ReadWritePaths line)
|
|
sed -i '/^ReadWritePaths=\/var\/lib\/pulse-sensor-proxy/a ReadOnlyPaths=/etc/pulse-sensor-proxy/tls' "$SERVICE_PATH"
|
|
|
|
print_success "Service file updated for HTTP mode"
|
|
fi
|
|
|
|
# Reload systemd and start service
|
|
print_info "Enabling and starting service..."
|
|
if ! systemctl daemon-reload; then
|
|
print_error "Failed to reload systemd daemon"
|
|
journalctl -u pulse-sensor-proxy -n 20 --no-pager
|
|
exit 1
|
|
fi
|
|
|
|
if ! systemctl enable pulse-sensor-proxy.service; then
|
|
print_error "Failed to enable pulse-sensor-proxy service"
|
|
journalctl -u pulse-sensor-proxy -n 20 --no-pager
|
|
exit 1
|
|
fi
|
|
|
|
if ! systemctl restart pulse-sensor-proxy.service; then
|
|
print_error "Failed to start pulse-sensor-proxy service"
|
|
print_error ""
|
|
|
|
# Attempt rollback if HTTP mode and we have a backup
|
|
if [[ "$HTTP_MODE" == true && -n "$BACKUP_CONFIG" && -f "$BACKUP_CONFIG" ]]; then
|
|
print_warn "Attempting to rollback to previous configuration..."
|
|
if cp "$BACKUP_CONFIG" /etc/pulse-sensor-proxy/config.yaml; then
|
|
print_info "Config restored from backup"
|
|
if systemctl restart pulse-sensor-proxy.service; then
|
|
print_success "Service restarted with previous configuration"
|
|
print_error ""
|
|
print_error "HTTP mode installation failed but previous config restored"
|
|
print_error "Temperature monitoring should still work via Unix socket"
|
|
print_error "Review the error above and fix before retrying"
|
|
exit 1
|
|
else
|
|
print_error "Rollback failed - service won't start even with old config"
|
|
fi
|
|
else
|
|
print_error "Failed to restore config from backup"
|
|
fi
|
|
fi
|
|
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
print_error "Service Status:"
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
systemctl status pulse-sensor-proxy --no-pager --lines=0 2>&1 || true
|
|
print_error ""
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
print_error "Recent Logs (last 40 lines):"
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
journalctl -u pulse-sensor-proxy -n 40 --no-pager 2>&1 || true
|
|
print_error ""
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
print_error "Common Issues:"
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
print_error "1. Missing user: Run 'useradd --system --no-create-home --group pulse-sensor-proxy'"
|
|
print_error "2. Permission errors: Check ownership of /var/lib/pulse-sensor-proxy"
|
|
print_error "3. lm-sensors not installed: Run 'apt-get install lm-sensors && sensors-detect --auto'"
|
|
print_error "4. Standalone node detection: If you see 'pvecm' errors, this is expected for non-clustered hosts"
|
|
print_error "5. Port already in use: Check 'ss -tlnp | grep ${HTTP_ADDR#:}'"
|
|
print_error ""
|
|
print_error "For more help: https://github.com/rcourtman/Pulse/blob/main/docs/TROUBLESHOOTING.md"
|
|
exit 1
|
|
fi
|
|
|
|
# Wait for socket to appear
|
|
print_info "Waiting for socket..."
|
|
for i in {1..10}; do
|
|
if [[ -S "$SOCKET_PATH" ]]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [[ ! -S "$SOCKET_PATH" ]]; then
|
|
print_error "Socket did not appear at $SOCKET_PATH after 10 seconds"
|
|
print_error ""
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
print_error "Diagnostics:"
|
|
print_error "═══════════════════════════════════════════════════════"
|
|
print_error "Service Status:"
|
|
systemctl status pulse-sensor-proxy --no-pager 2>&1 || true
|
|
print_error ""
|
|
print_error "Socket Directory Permissions:"
|
|
ls -la /run/pulse-sensor-proxy/ 2>&1 || echo "Directory does not exist"
|
|
print_error ""
|
|
print_error "Recent Logs:"
|
|
journalctl -u pulse-sensor-proxy -n 20 --no-pager 2>&1 || true
|
|
print_error ""
|
|
print_error "Common Causes:"
|
|
print_error " • Service failed to start (check logs above)"
|
|
print_error " • RuntimeDirectory permissions issue"
|
|
print_error " • Systemd socket creation failed"
|
|
print_error ""
|
|
print_error "Try: systemctl restart pulse-sensor-proxy && watch -n 0.5 'ls -la /run/pulse-sensor-proxy/'"
|
|
exit 1
|
|
fi
|
|
|
|
print_info "Socket ready at $SOCKET_PATH"
|
|
|
|
# If socket verification was deferred because the runtime directory was
|
|
# missing earlier, test the container mount now that the proxy is running.
|
|
if [[ "$STANDALONE" == false && "$DEFER_SOCKET_VERIFICATION" = true ]]; then
|
|
print_info "Validating container socket visibility now that host proxy is running..."
|
|
if pct exec "$CTID" -- test -S "${MOUNT_TARGET}/pulse-sensor-proxy.sock"; then
|
|
print_info "✓ Secure socket communication ready"
|
|
DEFER_SOCKET_VERIFICATION=false
|
|
[ -n "$LXC_CONFIG_BACKUP" ] && rm -f "$LXC_CONFIG_BACKUP"
|
|
else
|
|
print_error "Socket not visible at ${MOUNT_TARGET}/pulse-sensor-proxy.sock"
|
|
print_error "Bind mount exists but container still cannot access the proxy socket"
|
|
print_error "This usually indicates the container needs a restart or the mount failed to attach"
|
|
|
|
if [ -n "$LXC_CONFIG_BACKUP" ] && [ -f "$LXC_CONFIG_BACKUP" ]; then
|
|
print_warn "Rolling back container configuration changes..."
|
|
cp "$LXC_CONFIG_BACKUP" "$LXC_CONFIG"
|
|
rm -f "$LXC_CONFIG_BACKUP"
|
|
print_info "Container configuration restored to previous state"
|
|
fi
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Validate HTTP endpoint if HTTP mode is enabled
|
|
if [[ "$HTTP_MODE" == true ]]; then
|
|
print_info "Validating HTTP endpoint..."
|
|
|
|
# Wait a moment for HTTP server to fully start
|
|
sleep 2
|
|
|
|
# Test HTTP endpoint
|
|
HTTP_CHECK_URL="https://${PRIMARY_IP}${HTTP_ADDR}/health"
|
|
if curl -f -s -k -m 5 \
|
|
-H "Authorization: Bearer ${HTTP_AUTH_TOKEN}" \
|
|
"$HTTP_CHECK_URL" >/dev/null 2>&1; then
|
|
print_success "HTTP endpoint validated successfully"
|
|
else
|
|
print_error "HTTP endpoint validation failed"
|
|
print_error "URL: $HTTP_CHECK_URL"
|
|
print_error ""
|
|
print_error "Troubleshooting:"
|
|
print_error " 1. Check if port ${HTTP_ADDR#:} is listening: ss -tlnp | grep ${HTTP_ADDR#:}"
|
|
print_error " 2. Check sensor-proxy logs: journalctl -u pulse-sensor-proxy -n 50"
|
|
print_error " 3. Test manually: curl -k -H 'Authorization: Bearer \$TOKEN' $HTTP_CHECK_URL"
|
|
print_error ""
|
|
print_warn "Service is running but HTTP endpoint may not be accessible"
|
|
print_warn "Temperature monitoring may not work properly"
|
|
fi
|
|
fi
|
|
|
|
# Install sensor wrapper script for combined sensor and SMART data collection
|
|
print_info "Installing sensor wrapper script..."
|
|
cat > "$WRAPPER_SCRIPT" << 'WRAPPER_EOF'
|
|
#!/bin/bash
|
|
#
|
|
# pulse-sensor-wrapper.sh
|
|
# Combined sensor and SMART temperature collection for Pulse monitoring
|
|
#
|
|
# This script is deployed as the SSH forced command for the sensor proxy.
|
|
# It collects CPU/GPU temps via sensors and disk temps via smartctl,
|
|
# returning a unified JSON payload.
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
CACHE_DIR="/var/cache/pulse-sensor-proxy"
|
|
SMART_CACHE_TTL=1800 # 30 minutes
|
|
MAX_SMARTCTL_TIME=5 # seconds per disk
|
|
|
|
# Ensure cache directory exists
|
|
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
|
|
# Function to get cached SMART data
|
|
get_cached_smart() {
|
|
local cache_file="$CACHE_DIR/smart-temps.json"
|
|
local now=$(date +%s)
|
|
|
|
# Check if cache exists and is fresh
|
|
if [[ -f "$cache_file" ]]; then
|
|
local mtime=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
|
|
local age=$((now - mtime))
|
|
|
|
if [[ $age -lt $SMART_CACHE_TTL ]]; then
|
|
cat "$cache_file"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Cache miss or stale - return empty array and trigger background refresh
|
|
echo "[]"
|
|
|
|
# Trigger async refresh if not already running (use lock file)
|
|
local lock_file="$CACHE_DIR/smart-refresh.lock"
|
|
if ! [ -f "$lock_file" ]; then
|
|
(refresh_smart_cache &)
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Function to refresh SMART cache in background
|
|
refresh_smart_cache() {
|
|
local lock_file="$CACHE_DIR/smart-refresh.lock"
|
|
local cache_file="$CACHE_DIR/smart-temps.json"
|
|
local temp_file="${cache_file}.tmp.$$"
|
|
|
|
# Create lock file and ensure cleanup on exit
|
|
touch "$lock_file" 2>/dev/null || return 1
|
|
trap "rm -f '$lock_file' '$temp_file'" EXIT
|
|
|
|
local disks=()
|
|
|
|
# Find all physical disks (skip partitions, loop devices, etc.)
|
|
while IFS= read -r dev; do
|
|
[[ -b "$dev" ]] && disks+=("$dev")
|
|
done < <(lsblk -nd -o NAME,TYPE | awk '$2=="disk" {print "/dev/"$1}')
|
|
|
|
local results=()
|
|
|
|
for dev in "${disks[@]}"; do
|
|
# Use smartctl with standby check to avoid waking sleeping drives
|
|
# -n standby: skip if drive is in standby/sleep mode
|
|
# --json=o: output original smartctl JSON format
|
|
# timeout: prevent hanging on problematic drives
|
|
|
|
local output
|
|
if output=$(timeout ${MAX_SMARTCTL_TIME}s smartctl -n standby -A --json=o "$dev" 2>/dev/null); then
|
|
# Parse the JSON output
|
|
local temp=$(echo "$output" | jq -r '
|
|
.temperature.current //
|
|
(.ata_smart_attributes.table[] | select(.id == 194) | .raw.value) //
|
|
(.nvme_smart_health_information_log.temperature // empty)
|
|
' 2>/dev/null)
|
|
|
|
local serial=$(echo "$output" | jq -r '.serial_number // empty' 2>/dev/null)
|
|
local wwn=$(echo "$output" | jq -r '.wwn.naa // .wwn.oui // empty' 2>/dev/null)
|
|
local model=$(echo "$output" | jq -r '.model_name // .model_family // empty' 2>/dev/null)
|
|
local transport=$(echo "$output" | jq -r '.device.type // empty' 2>/dev/null)
|
|
|
|
# Only include if we got a valid temperature
|
|
if [[ -n "$temp" && "$temp" != "null" && "$temp" =~ ^[0-9]+$ ]]; then
|
|
local entry=$(jq -n \
|
|
--arg dev "$dev" \
|
|
--arg serial "$serial" \
|
|
--arg wwn "$wwn" \
|
|
--arg model "$model" \
|
|
--arg transport "$transport" \
|
|
--argjson temp "$temp" \
|
|
--arg updated "$(date -Iseconds)" \
|
|
'{
|
|
device: $dev,
|
|
serial: $serial,
|
|
wwn: $wwn,
|
|
model: $model,
|
|
type: $transport,
|
|
temperature: $temp,
|
|
lastUpdated: $updated,
|
|
standbySkipped: false
|
|
}')
|
|
results+=("$entry")
|
|
fi
|
|
elif echo "$output" | grep -q "standby"; then
|
|
# Drive is in standby - record it but don't wake it
|
|
local entry=$(jq -n \
|
|
--arg dev "$dev" \
|
|
--arg updated "$(date -Iseconds)" \
|
|
'{
|
|
device: $dev,
|
|
temperature: null,
|
|
lastUpdated: $updated,
|
|
standbySkipped: true
|
|
}')
|
|
results+=("$entry")
|
|
fi
|
|
|
|
# Small delay between disks to avoid saturating SATA controller
|
|
sleep 0.1
|
|
done
|
|
|
|
# Build final JSON array
|
|
if [[ ${#results[@]} -gt 0 ]]; then
|
|
local json=$(printf '%s\n' "${results[@]}" | jq -s '.')
|
|
else
|
|
local json="[]"
|
|
fi
|
|
|
|
# Atomic write to cache
|
|
echo "$json" > "$temp_file"
|
|
mv "$temp_file" "$cache_file"
|
|
chmod 644 "$cache_file" 2>/dev/null || true
|
|
}
|
|
|
|
# Main execution
|
|
|
|
# Collect sensor data (CPU, GPU temps)
|
|
sensors_data=$(sensors -j 2>/dev/null || echo '{}')
|
|
|
|
# Get SMART data from cache
|
|
smart_data=$(get_cached_smart)
|
|
|
|
# Combine into unified payload
|
|
jq -n \
|
|
--argjson sensors "$sensors_data" \
|
|
--argjson smart "$smart_data" \
|
|
'{
|
|
sensors: $sensors,
|
|
smart: $smart
|
|
}'
|
|
WRAPPER_EOF
|
|
|
|
chmod +x "$WRAPPER_SCRIPT"
|
|
print_success "Sensor wrapper installed at $WRAPPER_SCRIPT"
|
|
|
|
# Install cleanup system for full Pulse removal when nodes are deleted
|
|
print_info "Installing cleanup system..."
|
|
|
|
# Install cleanup script
|
|
cat > "$CLEANUP_SCRIPT_PATH" <<'CLEANUP_EOF'
|
|
#!/bin/bash
|
|
|
|
# pulse-sensor-cleanup.sh - Complete Pulse footprint removal when nodes are removed
|
|
# Removes: SSH keys, proxy service, binaries, API tokens, and LXC bind mounts
|
|
# This script is triggered by systemd path unit when cleanup-request.json is created
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
WORK_DIR="/var/lib/pulse-sensor-proxy"
|
|
CLEANUP_REQUEST="${WORK_DIR}/cleanup-request.json"
|
|
LOCKFILE="${WORK_DIR}/cleanup.lock"
|
|
LOG_TAG="pulse-sensor-cleanup"
|
|
INSTALLER_PATH="/opt/pulse/sensor-proxy/install-sensor-proxy.sh"
|
|
|
|
# Logging functions
|
|
log_info() {
|
|
logger -t "$LOG_TAG" -p user.info "$1"
|
|
echo "[INFO] $1"
|
|
}
|
|
|
|
log_warn() {
|
|
logger -t "$LOG_TAG" -p user.warning "$1"
|
|
echo "[WARN] $1"
|
|
}
|
|
|
|
log_error() {
|
|
logger -t "$LOG_TAG" -p user.err "$1"
|
|
echo "[ERROR] $1" >&2
|
|
}
|
|
|
|
# Acquire exclusive lock to prevent concurrent cleanup runs
|
|
exec 200>"$LOCKFILE"
|
|
if ! flock -n 200; then
|
|
log_info "Another cleanup instance is running, exiting"
|
|
exit 0
|
|
fi
|
|
|
|
# Check if cleanup request file exists
|
|
if [[ ! -f "$CLEANUP_REQUEST" ]]; then
|
|
log_info "No cleanup request found at $CLEANUP_REQUEST"
|
|
exit 0
|
|
fi
|
|
|
|
log_info "Processing cleanup request from $CLEANUP_REQUEST"
|
|
|
|
# Read and parse the cleanup request, then delete immediately to prevent re-processing
|
|
CLEANUP_DATA=$(cat "$CLEANUP_REQUEST")
|
|
HOST=$(echo "$CLEANUP_DATA" | grep -o '"host":"[^"]*"' | cut -d'"' -f4 || echo "")
|
|
REQUESTED_AT=$(echo "$CLEANUP_DATA" | grep -o '"requestedAt":"[^"]*"' | cut -d'"' -f4 || echo "")
|
|
|
|
log_info "Cleanup requested at: ${REQUESTED_AT:-unknown}"
|
|
|
|
# Delete request file IMMEDIATELY to prevent loops (before any long-running operations)
|
|
rm -f "$CLEANUP_REQUEST"
|
|
|
|
# If no specific host was provided, clean up all known nodes
|
|
if [[ -z "$HOST" ]]; then
|
|
log_info "No specific host provided - cleaning up all cluster nodes"
|
|
|
|
# Discover cluster nodes
|
|
if command -v pvecm >/dev/null 2>&1; then
|
|
CLUSTER_NODES=$(pvecm status 2>/dev/null | awk '/0x[0-9a-f]+.*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print $3}' || true)
|
|
|
|
if [[ -n "$CLUSTER_NODES" ]]; then
|
|
for node_ip in $CLUSTER_NODES; do
|
|
log_info "Cleaning up SSH keys on node $node_ip"
|
|
|
|
# Remove both pulse-managed-key and pulse-proxy-key entries
|
|
ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
|
"sed -i -e '/# pulse-managed-key\$/d' -e '/# pulse-proxy-key\$/d' /root/.ssh/authorized_keys" 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "Failed to clean up SSH keys on $node_ip"
|
|
done
|
|
log_info "Cluster cleanup completed"
|
|
else
|
|
# Standalone node - clean up localhost
|
|
log_info "Standalone node detected - cleaning up localhost"
|
|
sed -i -e '/# pulse-managed-key$/d' -e '/# pulse-proxy-key$/d' /root/.ssh/authorized_keys 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "Failed to clean up SSH keys on localhost"
|
|
fi
|
|
else
|
|
log_warn "pvecm command not available - cleaning up localhost only"
|
|
sed -i -e '/# pulse-managed-key$/d' -e '/# pulse-proxy-key$/d' /root/.ssh/authorized_keys 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "Failed to clean up SSH keys on localhost"
|
|
fi
|
|
else
|
|
log_info "Cleaning up specific host: $HOST"
|
|
|
|
# Extract IP from host URL
|
|
HOST_CLEAN=$(echo "$HOST" | sed -e 's|^https\?://||' -e 's|:.*$||')
|
|
|
|
# Check if this is localhost
|
|
LOCAL_IPS=$(hostname -I 2>/dev/null || echo "")
|
|
IS_LOCAL=false
|
|
|
|
for local_ip in $LOCAL_IPS; do
|
|
if [[ "$HOST_CLEAN" == "$local_ip" ]]; then
|
|
IS_LOCAL=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ "$HOST_CLEAN" == "127.0.0.1" || "$HOST_CLEAN" == "localhost" ]]; then
|
|
IS_LOCAL=true
|
|
fi
|
|
|
|
if [[ "$IS_LOCAL" == true ]]; then
|
|
log_info "Performing full cleanup on localhost"
|
|
|
|
# 1. Remove SSH keys
|
|
log_info "Removing SSH keys from authorized_keys"
|
|
sed -i -e '/# pulse-managed-key$/d' -e '/# pulse-proxy-key$/d' /root/.ssh/authorized_keys 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "Failed to clean up SSH keys"
|
|
|
|
# 2. Delete API tokens and user
|
|
log_info "Removing Proxmox API tokens and pulse-monitor user"
|
|
if command -v pveum >/dev/null 2>&1; then
|
|
# Use JSON output if available (Proxmox 7.0+), fall back to text parsing
|
|
if pveum user token list --output-format json-pretty pulse-monitor@pam >/dev/null 2>&1; then
|
|
TOKEN_IDS=$(pveum user token list --output-format json-pretty pulse-monitor@pam 2>/dev/null | \
|
|
grep -o '"tokenid":"[^"]*"' | cut -d'"' -f4 || true)
|
|
else
|
|
# Fall back to parsing table output, filtering table decorations
|
|
TOKEN_IDS=$(pveum user token list pulse-monitor@pam 2>/dev/null | \
|
|
grep -v '^[│┌└╞]' | awk 'NR>1 && /pulse/ {print $1}' | grep -v '^$' || true)
|
|
fi
|
|
|
|
for token_id in $TOKEN_IDS; do
|
|
log_info "Deleting API token: $token_id"
|
|
pveum user token remove pulse-monitor@pam "${token_id}" 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "Failed to delete token $token_id"
|
|
done
|
|
|
|
# Remove the pulse-monitor user
|
|
log_info "Removing pulse-monitor@pam user"
|
|
pveum user delete pulse-monitor@pam 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "pulse-monitor@pam user not found or already removed"
|
|
else
|
|
log_warn "pveum command not available, skipping API token cleanup"
|
|
fi
|
|
|
|
# 3. Remove LXC bind mounts
|
|
log_info "Removing LXC bind mounts from container configs"
|
|
if command -v pct >/dev/null 2>&1; then
|
|
for ctid in $(pct list 2>/dev/null | awk 'NR>1 {print $1}' || true); do
|
|
CONF_FILE="/etc/pve/lxc/${ctid}.conf"
|
|
if [[ -f "$CONF_FILE" ]] && grep -q "pulse-sensor-proxy" "$CONF_FILE" 2>/dev/null; then
|
|
log_info "Removing bind mount from container $ctid"
|
|
sed -i '/pulse-sensor-proxy/d' "$CONF_FILE" 2>&1 | \
|
|
logger -t "$LOG_TAG" -p user.info || \
|
|
log_warn "Failed to update config for container $ctid"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# 4. Uninstall proxy service and remove binaries via isolated transient unit
|
|
log_info "Starting full uninstallation (service, binaries, configs)"
|
|
if [[ -x "$INSTALLER_PATH" ]]; then
|
|
# Use systemd-run to create isolated transient unit that won't be killed
|
|
# when we stop pulse-sensor-proxy.service
|
|
if command -v systemd-run >/dev/null 2>&1; then
|
|
UNINSTALL_UNIT="pulse-uninstall-$(date +%s)"
|
|
log_info "Spawning isolated uninstaller unit: $UNINSTALL_UNIT"
|
|
|
|
systemd-run \
|
|
--unit="${UNINSTALL_UNIT}" \
|
|
--property="Type=oneshot" \
|
|
--property="Conflicts=pulse-sensor-proxy.service" \
|
|
--property="After=pulse-sensor-cleanup.service" \
|
|
--no-block \
|
|
--quiet \
|
|
-- bash -c "$INSTALLER_PATH --uninstall --quiet >> /var/log/pulse/sensor-proxy/uninstall.log 2>&1" \
|
|
2>&1 | logger -t "$LOG_TAG" -p user.info
|
|
|
|
log_info "Uninstaller started in isolated systemd unit (non-blocking)"
|
|
else
|
|
log_warn "systemd-run not available, attempting direct uninstall (may fail)"
|
|
bash "$INSTALLER_PATH" --uninstall --quiet >> /var/log/pulse/sensor-proxy/uninstall.log 2>&1 || \
|
|
log_error "Uninstaller failed - manual cleanup may be required"
|
|
fi
|
|
else
|
|
log_warn "Installer not found at $INSTALLER_PATH, cannot run uninstaller"
|
|
log_info "Manual cleanup required: systemctl stop pulse-sensor-proxy && systemctl disable pulse-sensor-proxy"
|
|
fi
|
|
|
|
log_info "Localhost cleanup initiated (uninstaller running in background)"
|
|
else
|
|
log_info "Cleaning up remote host: $HOST_CLEAN"
|
|
|
|
# Try to use proxy's SSH key first (for standalone nodes), fall back to default
|
|
PROXY_KEY="/var/lib/pulse-sensor-proxy/ssh/id_ed25519"
|
|
SSH_CMD="ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5"
|
|
|
|
if [[ -f "$PROXY_KEY" ]]; then
|
|
log_info "Using proxy SSH key for cleanup"
|
|
SSH_CMD="$SSH_CMD -i $PROXY_KEY"
|
|
fi
|
|
|
|
# Remove both pulse-managed-key and pulse-proxy-key entries from remote host
|
|
CLEANUP_OUTPUT=$($SSH_CMD root@"$HOST_CLEAN" \
|
|
"sed -i -e '/# pulse-managed-key\$/d' -e '/# pulse-proxy-key\$/d' /root/.ssh/authorized_keys && echo 'SUCCESS'" 2>&1)
|
|
|
|
if echo "$CLEANUP_OUTPUT" | grep -q "SUCCESS"; then
|
|
log_info "Successfully cleaned up SSH keys on $HOST_CLEAN"
|
|
else
|
|
# Check if this is a standalone node with forced commands (common case)
|
|
if echo "$CLEANUP_OUTPUT" | grep -q "cpu_thermal\|coretemp\|k10temp"; then
|
|
log_warn "Cannot cleanup standalone node $HOST_CLEAN (forced command prevents cleanup)"
|
|
log_info "Standalone node keys are read-only (sensors -j) - low security risk"
|
|
log_info "Manual cleanup: ssh root@$HOST_CLEAN \"sed -i '/# pulse-proxy-key\$/d' /root/.ssh/authorized_keys\""
|
|
else
|
|
log_error "Failed to clean up SSH keys on $HOST_CLEAN: $CLEANUP_OUTPUT"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
log_info "Cleanup completed successfully"
|
|
exit 0
|
|
CLEANUP_EOF
|
|
|
|
chmod +x "$CLEANUP_SCRIPT_PATH"
|
|
print_info "Cleanup script installed"
|
|
|
|
# Install systemd path unit
|
|
CLEANUP_PATH_UNIT="/etc/systemd/system/pulse-sensor-cleanup.path"
|
|
cat > "$CLEANUP_PATH_UNIT" << 'PATH_EOF'
|
|
[Unit]
|
|
Description=Watch for Pulse sensor cleanup requests
|
|
Documentation=https://github.com/rcourtman/Pulse
|
|
|
|
[Path]
|
|
# Watch for the cleanup request file
|
|
PathChanged=/var/lib/pulse-sensor-proxy/cleanup-request.json
|
|
# Also watch for modifications
|
|
PathModified=/var/lib/pulse-sensor-proxy/cleanup-request.json
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
PATH_EOF
|
|
|
|
# Install systemd service unit
|
|
CLEANUP_SERVICE_UNIT="/etc/systemd/system/pulse-sensor-cleanup.service"
|
|
cat > "$CLEANUP_SERVICE_UNIT" <<SERVICE_EOF
|
|
[Unit]
|
|
Description=Pulse Sensor Cleanup Service
|
|
Documentation=https://github.com/rcourtman/Pulse
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=${CLEANUP_SCRIPT_PATH}
|
|
User=root
|
|
Group=root
|
|
WorkingDirectory=/var/lib/pulse-sensor-proxy
|
|
|
|
# Logging
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=pulse-sensor-cleanup
|
|
|
|
# Security hardening (less restrictive than the proxy since we need SSH access)
|
|
NoNewPrivileges=true
|
|
ProtectSystem=strict
|
|
ReadWritePaths=/var/lib/pulse-sensor-proxy /root/.ssh
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectControlGroups=true
|
|
PrivateTmp=true
|
|
RestrictSUIDSGID=true
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
|
LimitNOFILE=1024
|
|
|
|
[Install]
|
|
# This service is triggered by the .path unit, no need to enable it directly
|
|
SERVICE_EOF
|
|
|
|
# Enable and start the path unit
|
|
systemctl daemon-reload || true
|
|
systemctl enable pulse-sensor-cleanup.path
|
|
systemctl start pulse-sensor-cleanup.path
|
|
|
|
print_info "Cleanup system installed and enabled"
|
|
|
|
# Configure SSH keys for cluster temperature monitoring
|
|
print_info "Configuring proxy SSH access to cluster nodes..."
|
|
|
|
# Wait for proxy to generate SSH keys
|
|
PROXY_KEY_FILE="$SSH_DIR/id_ed25519.pub"
|
|
for i in {1..10}; do
|
|
if [[ -f "$PROXY_KEY_FILE" ]]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [[ ! -f "$PROXY_KEY_FILE" ]]; then
|
|
print_error "Proxy SSH key not generated after 10 seconds"
|
|
print_info "Check service logs: journalctl -u pulse-sensor-proxy -n 50"
|
|
exit 1
|
|
fi
|
|
|
|
PROXY_PUBLIC_KEY=$(cat "$PROXY_KEY_FILE")
|
|
print_info "Proxy public key: ${PROXY_PUBLIC_KEY:0:50}..."
|
|
|
|
# Discover cluster nodes
|
|
if command -v pvecm >/dev/null 2>&1; then
|
|
# Extract node IPs from pvecm status
|
|
CLUSTER_NODES=$(pvecm status 2>/dev/null | awk '/0x[0-9a-f]+.*[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {print $3}' || true)
|
|
|
|
# Extract node names from pvecm nodes (for allowlist)
|
|
CLUSTER_NODE_NAMES=$(pvecm nodes 2>/dev/null | awk 'NR>2 {print $3}' || true)
|
|
|
|
if [[ -n "$CLUSTER_NODES" ]]; then
|
|
print_info "Discovered cluster nodes: $(echo $CLUSTER_NODES | tr '\n' ' ')"
|
|
|
|
# Configure SSH key with forced command restriction
|
|
FORCED_CMD='command="/usr/local/bin/pulse-sensor-wrapper.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
|
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key"
|
|
|
|
# Track SSH key push results
|
|
SSH_SUCCESS_COUNT=0
|
|
SSH_FAILURE_COUNT=0
|
|
declare -a SSH_FAILED_NODES=()
|
|
LOCAL_IPS=$(hostname -I 2>/dev/null || echo "")
|
|
LOCAL_HOSTNAMES="$(hostname 2>/dev/null || echo "") $(hostname -f 2>/dev/null || echo "")"
|
|
LOCAL_HANDLED=false
|
|
|
|
# Push key to each cluster node
|
|
for node_ip in $CLUSTER_NODES; do
|
|
print_info "Authorizing proxy key on node $node_ip..."
|
|
|
|
IS_LOCAL=false
|
|
# Check if node_ip matches any of the local IPs (exact match with word boundaries)
|
|
for local_ip in $LOCAL_IPS; do
|
|
if [[ "$node_ip" == "$local_ip" ]]; then
|
|
IS_LOCAL=true
|
|
break
|
|
fi
|
|
done
|
|
if [[ " $LOCAL_HOSTNAMES " == *" $node_ip "* ]]; then
|
|
IS_LOCAL=true
|
|
fi
|
|
if [[ "$node_ip" == "127.0.0.1" || "$node_ip" == "localhost" ]]; then
|
|
IS_LOCAL=true
|
|
fi
|
|
|
|
if [[ "$IS_LOCAL" = true ]]; then
|
|
configure_local_authorized_key "$AUTH_LINE"
|
|
LOCAL_HANDLED=true
|
|
((SSH_SUCCESS_COUNT+=1))
|
|
continue
|
|
fi
|
|
|
|
# Remove any existing proxy keys first
|
|
ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
|
"sed -i '/# pulse-managed-key\$/d' /root/.ssh/authorized_keys" 2>/dev/null || true
|
|
|
|
# Add new key with forced command
|
|
SSH_ERROR=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
|
"echo '${AUTH_LINE}' >> /root/.ssh/authorized_keys" 2>&1)
|
|
if [[ $? -eq 0 ]]; then
|
|
print_success "SSH key configured on $node_ip"
|
|
((SSH_SUCCESS_COUNT+=1))
|
|
else
|
|
print_warn "Failed to configure SSH key on $node_ip"
|
|
((SSH_FAILURE_COUNT+=1))
|
|
SSH_FAILED_NODES+=("$node_ip")
|
|
# Log detailed error for debugging
|
|
if [[ -n "$SSH_ERROR" ]]; then
|
|
print_info " Error details: $(echo "$SSH_ERROR" | head -1)"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Print summary
|
|
print_info ""
|
|
print_info "SSH key configuration summary:"
|
|
print_info " ✓ Success: $SSH_SUCCESS_COUNT node(s)"
|
|
if [[ $SSH_FAILURE_COUNT -gt 0 ]]; then
|
|
print_warn " ✗ Failed: $SSH_FAILURE_COUNT node(s) - ${SSH_FAILED_NODES[*]}"
|
|
print_info ""
|
|
print_info "To retry failed nodes, re-run this script or manually run:"
|
|
print_info " ssh root@<node> 'echo \"${AUTH_LINE}\" >> /root/.ssh/authorized_keys'"
|
|
fi
|
|
if [[ "$LOCAL_HANDLED" = false ]]; then
|
|
configure_local_authorized_key "$AUTH_LINE"
|
|
((SSH_SUCCESS_COUNT+=1))
|
|
fi
|
|
|
|
# Add discovered cluster nodes to config file for allowlist validation
|
|
print_info "Updating proxy configuration with discovered cluster nodes..."
|
|
# Collect all nodes (IPs and hostnames) into array
|
|
all_nodes=()
|
|
for node_ip in $CLUSTER_NODES; do
|
|
all_nodes+=("$node_ip")
|
|
done
|
|
for node_name in $CLUSTER_NODE_NAMES; do
|
|
all_nodes+=("$node_name")
|
|
done
|
|
# Use helper function to safely update allowed_nodes (prevents duplicates on re-run)
|
|
update_allowed_nodes "Cluster nodes (auto-discovered during installation)" "${all_nodes[@]}"
|
|
else
|
|
# No cluster found - configure standalone node
|
|
print_info "No cluster detected, configuring standalone node..."
|
|
|
|
# Configure SSH key with forced command restriction
|
|
FORCED_CMD='command="/usr/local/bin/pulse-sensor-wrapper.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
|
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key"
|
|
|
|
print_info "Authorizing proxy key on localhost..."
|
|
configure_local_authorized_key "$AUTH_LINE"
|
|
print_info ""
|
|
print_info "Standalone node configuration complete"
|
|
|
|
# Add localhost to config file for allowlist validation
|
|
print_info "Updating proxy configuration for standalone mode..."
|
|
LOCAL_IPS=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v '^$' || echo "127.0.0.1")
|
|
# Collect all local IPs and localhost variants into array
|
|
all_nodes=()
|
|
for local_ip in $LOCAL_IPS; do
|
|
all_nodes+=("$local_ip")
|
|
done
|
|
# Always include localhost variants
|
|
all_nodes+=("127.0.0.1" "localhost")
|
|
# Use helper function to safely update allowed_nodes (prevents duplicates on re-run)
|
|
update_allowed_nodes "Standalone node configuration (auto-configured during installation)" "${all_nodes[@]}"
|
|
fi
|
|
else
|
|
# Proxmox host but pvecm not available (shouldn't happen, but handle it)
|
|
print_warn "pvecm command not available"
|
|
print_info "Configuring SSH key for localhost..."
|
|
|
|
# Configure localhost as fallback
|
|
FORCED_CMD='command="/usr/local/bin/pulse-sensor-wrapper.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
|
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key"
|
|
|
|
configure_local_authorized_key "$AUTH_LINE"
|
|
|
|
# Add localhost to config file for allowlist validation
|
|
print_info "Updating proxy configuration for localhost fallback..."
|
|
LOCAL_IPS=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -v '^$' || echo "127.0.0.1")
|
|
# Collect all local IPs and localhost variants into array
|
|
all_nodes=()
|
|
for local_ip in $LOCAL_IPS; do
|
|
all_nodes+=("$local_ip")
|
|
done
|
|
# Always include localhost variants
|
|
all_nodes+=("127.0.0.1" "localhost")
|
|
# Use helper function to safely update allowed_nodes (prevents duplicates on re-run)
|
|
update_allowed_nodes "Localhost fallback configuration (pvecm unavailable)" "${all_nodes[@]}"
|
|
fi
|
|
|
|
# Container-specific configuration (skip for standalone mode)
|
|
if [[ "$STANDALONE" == false ]]; then
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo " Secure Container Communication Setup"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
echo "Setting up secure socket mount for temperature monitoring:"
|
|
echo " • Container communicates with host proxy via Unix socket"
|
|
echo " • No SSH keys exposed inside container (enhanced security)"
|
|
echo " • Proxy on host manages all temperature collection"
|
|
echo ""
|
|
|
|
# Ensure container mount via mp configuration
|
|
print_info "Configuring socket bind mount..."
|
|
MOUNT_TARGET="/mnt/pulse-proxy"
|
|
HOST_SOCKET_SOURCE="/run/pulse-sensor-proxy"
|
|
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
|
|
|
# Back up container config before modifying
|
|
LXC_CONFIG_BACKUP=$(mktemp)
|
|
cp "$LXC_CONFIG" "$LXC_CONFIG_BACKUP" 2>/dev/null || {
|
|
print_warn "Could not back up container config (may not exist yet)"
|
|
LXC_CONFIG_BACKUP=""
|
|
}
|
|
|
|
CONFIG_CONTENT=$(pct config "$CTID")
|
|
CURRENT_MP=$(pct config "$CTID" | awk -v target="$MOUNT_TARGET" '$1 ~ /^mp[0-9]+:$/ && index($0, "mp=" target) {split($1, arr, ":"); print arr[1]; exit}')
|
|
MOUNT_UPDATED=false
|
|
HOTPLUG_FAILED=false
|
|
CT_RUNNING=false
|
|
if pct status "$CTID" 2>/dev/null | grep -q "running"; then
|
|
CT_RUNNING=true
|
|
fi
|
|
|
|
if [[ -z "$CURRENT_MP" ]]; then
|
|
for idx in $(seq 0 9); do
|
|
if ! printf "%s\n" "$CONFIG_CONTENT" | grep -q "^mp${idx}:"; then
|
|
CURRENT_MP="mp${idx}"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "$CURRENT_MP" ]]; then
|
|
print_error "Unable to find available mp slot for container mount"
|
|
exit 1
|
|
fi
|
|
print_info "Configuring container mount using $CURRENT_MP..."
|
|
SET_ERROR=$(pct set "$CTID" -${CURRENT_MP} "${HOST_SOCKET_SOURCE},mp=${MOUNT_TARGET},replicate=0" 2>&1)
|
|
if [ $? -eq 0 ]; then
|
|
MOUNT_UPDATED=true
|
|
else
|
|
HOTPLUG_FAILED=true
|
|
if [ -n "$SET_ERROR" ]; then
|
|
print_warn "pct set failed: $SET_ERROR"
|
|
fi
|
|
fi
|
|
else
|
|
desired_pattern="^${CURRENT_MP}: ${HOST_SOCKET_SOURCE},mp=${MOUNT_TARGET}"
|
|
if pct config "$CTID" | grep -q "$desired_pattern"; then
|
|
print_info "Container already has socket mount configured ($CURRENT_MP)"
|
|
else
|
|
print_info "Updating container mount configuration ($CURRENT_MP)..."
|
|
SET_ERROR=$(pct set "$CTID" -${CURRENT_MP} "${HOST_SOCKET_SOURCE},mp=${MOUNT_TARGET},replicate=0" 2>&1)
|
|
if [ $? -eq 0 ]; then
|
|
MOUNT_UPDATED=true
|
|
else
|
|
HOTPLUG_FAILED=true
|
|
if [ -n "$SET_ERROR" ]; then
|
|
print_warn "pct set failed: $SET_ERROR"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ "$HOTPLUG_FAILED" = true ]]; then
|
|
print_warn "Hot-plugging socket mount failed (container may be running). Updating config directly."
|
|
CURRENT_MP_LINE="${CURRENT_MP}: ${HOST_SOCKET_SOURCE},mp=${MOUNT_TARGET},replicate=0"
|
|
if ! grep -q "^${CURRENT_MP}:" "$LXC_CONFIG" 2>/dev/null; then
|
|
echo "$CURRENT_MP_LINE" >> "$LXC_CONFIG"
|
|
else
|
|
sed -i "s#^${CURRENT_MP}:.*#${CURRENT_MP_LINE}#" "$LXC_CONFIG"
|
|
fi
|
|
MOUNT_UPDATED=true
|
|
fi
|
|
|
|
# Verify mount configuration actually persisted
|
|
if ! pct config "$CTID" | grep -q "^${CURRENT_MP}:"; then
|
|
print_error "Failed to persist mount configuration for $CURRENT_MP"
|
|
print_error "Expected mount not found in container config"
|
|
exit 1
|
|
fi
|
|
print_info "✓ Mount configuration verified in container config"
|
|
|
|
# Remove legacy lxc.mount.entry directives if present
|
|
if grep -q "lxc.mount.entry: ${HOST_SOCKET_SOURCE}" "$LXC_CONFIG"; then
|
|
print_info "Removing legacy lxc.mount.entry directives for pulse-sensor-proxy"
|
|
sed -i '/lxc\.mount\.entry: \/run\/pulse-sensor-proxy/d' "$LXC_CONFIG"
|
|
MOUNT_UPDATED=true
|
|
fi
|
|
|
|
# Restart container to apply mount if configuration changed or mount missing
|
|
if [[ "$MOUNT_UPDATED" = true ]]; then
|
|
if [[ "$SKIP_RESTART" = true ]]; then
|
|
if [[ "$CT_RUNNING" = true ]]; then
|
|
print_info "Skipping container restart (--skip-restart provided)."
|
|
else
|
|
print_info "Skipping automatic container start (--skip-restart provided)."
|
|
fi
|
|
else
|
|
print_info "Restarting container to activate secure communication..."
|
|
if [[ "$CT_RUNNING" = true ]]; then
|
|
pct stop "$CTID" && sleep 2 && pct start "$CTID"
|
|
else
|
|
pct start "$CTID"
|
|
fi
|
|
sleep 5
|
|
fi
|
|
fi
|
|
|
|
# Verify socket directory and file inside container
|
|
if [[ "$HOTPLUG_FAILED" = true && "$CT_RUNNING" = true ]]; then
|
|
print_warn "Skipping socket verification until container $CTID is restarted."
|
|
print_warn "Please restart container and verify socket manually:"
|
|
print_warn " pct stop $CTID && sleep 2 && pct start $CTID"
|
|
print_warn " pct exec $CTID -- test -S ${MOUNT_TARGET}/pulse-sensor-proxy.sock && echo 'Socket OK'"
|
|
elif [[ "$SKIP_RESTART" = true && "$CT_RUNNING" = false ]]; then
|
|
print_warn "Socket verification deferred. Start container $CTID and run:"
|
|
print_warn " pct exec $CTID -- test -S ${MOUNT_TARGET}/pulse-sensor-proxy.sock && echo 'Socket OK'"
|
|
else
|
|
if [[ ! -S "$SOCKET_PATH" ]]; then
|
|
print_warn "Host proxy socket not available yet; deferring container verification until service starts."
|
|
DEFER_SOCKET_VERIFICATION=true
|
|
else
|
|
print_info "Verifying secure communication channel..."
|
|
if pct exec "$CTID" -- test -S "${MOUNT_TARGET}/pulse-sensor-proxy.sock"; then
|
|
print_info "✓ Secure socket communication ready"
|
|
# Clean up backup since verification succeeded
|
|
[ -n "$LXC_CONFIG_BACKUP" ] && rm -f "$LXC_CONFIG_BACKUP"
|
|
else
|
|
print_error "Socket not visible at ${MOUNT_TARGET}/pulse-sensor-proxy.sock"
|
|
print_error "Mount configuration verified but socket not accessible in container"
|
|
print_error "This indicates a mount or restart issue"
|
|
|
|
# Rollback container config changes
|
|
if [ -n "$LXC_CONFIG_BACKUP" ] && [ -f "$LXC_CONFIG_BACKUP" ]; then
|
|
print_warn "Rolling back container configuration changes..."
|
|
cp "$LXC_CONFIG_BACKUP" "$LXC_CONFIG"
|
|
rm -f "$LXC_CONFIG_BACKUP"
|
|
print_info "Container configuration restored to previous state"
|
|
fi
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Configure Pulse backend environment override inside container
|
|
print_info "Configuring Pulse to use proxy..."
|
|
|
|
# Check if Pulse service exists in container before configuring
|
|
if ! pct exec "$CTID" -- systemctl status pulse >/dev/null 2>&1; then
|
|
print_warn "Pulse service not found in container $CTID; skipping proxy configuration"
|
|
print_info "Install Pulse in the container first, then re-run this installer"
|
|
else
|
|
pct exec "$CTID" -- bash -lc "mkdir -p /etc/systemd/system/pulse.service.d"
|
|
pct exec "$CTID" -- bash -lc "cat <<'EOF' >/etc/systemd/system/pulse.service.d/10-pulse-proxy.conf
|
|
[Service]
|
|
Environment=PULSE_SENSOR_PROXY_SOCKET=${MOUNT_TARGET}/pulse-sensor-proxy.sock
|
|
EOF"
|
|
pct exec "$CTID" -- systemctl daemon-reload || true
|
|
|
|
# Restart Pulse service to apply the new environment variable
|
|
if pct exec "$CTID" -- systemctl is-active --quiet pulse 2>/dev/null; then
|
|
print_info "Restarting Pulse service to apply configuration..."
|
|
pct exec "$CTID" -- systemctl restart pulse
|
|
sleep 2
|
|
print_success "Pulse service restarted with proxy configuration"
|
|
fi
|
|
fi
|
|
|
|
# Test proxy status
|
|
print_info "Testing proxy status..."
|
|
if systemctl is-active --quiet pulse-sensor-proxy; then
|
|
print_info "${GREEN}✓${NC} pulse-sensor-proxy is running"
|
|
else
|
|
print_error "pulse-sensor-proxy is not running"
|
|
print_info "Check logs: journalctl -u pulse-sensor-proxy -n 50"
|
|
exit 1
|
|
fi
|
|
|
|
# Check for and remove legacy SSH keys from container
|
|
print_info "Checking for legacy SSH keys in container..."
|
|
LEGACY_KEYS_FOUND=false
|
|
for key_type in id_rsa id_dsa id_ecdsa id_ed25519; do
|
|
if pct exec "$CTID" -- test -f "/root/.ssh/$key_type" 2>/dev/null; then
|
|
LEGACY_KEYS_FOUND=true
|
|
if [ "$QUIET" != true ]; then
|
|
print_warn "Found legacy SSH key: /root/.ssh/$key_type"
|
|
fi
|
|
pct exec "$CTID" -- rm -f "/root/.ssh/$key_type" "/root/.ssh/${key_type}.pub"
|
|
print_info " Removed /root/.ssh/$key_type"
|
|
fi
|
|
done
|
|
|
|
if [ "$LEGACY_KEYS_FOUND" = true ] && [ "$QUIET" != true ]; then
|
|
print_info ""
|
|
print_info "Legacy SSH keys removed from container for security"
|
|
print_info ""
|
|
fi
|
|
fi # End of container-specific configuration
|
|
|
|
# Install self-heal safeguards to keep proxy available
|
|
print_info "Configuring self-heal safeguards..."
|
|
if ! cache_installer_for_self_heal; then
|
|
if [[ -n "$INSTALLER_CACHE_REASON" ]]; then
|
|
print_warn "Unable to cache installer script for self-heal (${INSTALLER_CACHE_REASON})"
|
|
else
|
|
print_warn "Unable to cache installer script for self-heal"
|
|
fi
|
|
fi
|
|
|
|
cat > "$SELFHEAL_SCRIPT" <<'EOF'
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
SERVICE="pulse-sensor-proxy"
|
|
INSTALLER="/usr/local/share/pulse/install-sensor-proxy.sh"
|
|
CTID_FILE="/etc/pulse-sensor-proxy/ctid"
|
|
LOG_TAG="pulse-sensor-proxy-selfheal"
|
|
|
|
log() {
|
|
logger -t "$LOG_TAG" "$1"
|
|
}
|
|
|
|
if ! command -v systemctl >/dev/null 2>&1; then
|
|
exit 0
|
|
fi
|
|
|
|
if ! systemctl list-unit-files 2>/dev/null | grep -q "^${SERVICE}\\.service"; then
|
|
if [[ -x "$INSTALLER" && -f "$CTID_FILE" ]]; then
|
|
log "Service unit missing; attempting reinstall"
|
|
bash "$INSTALLER" --ctid "$(cat "$CTID_FILE")" --skip-restart --quiet || log "Reinstall attempt failed"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if ! systemctl is-active --quiet "${SERVICE}.service"; then
|
|
systemctl start "${SERVICE}.service" || true
|
|
sleep 2
|
|
fi
|
|
|
|
if ! systemctl is-active --quiet "${SERVICE}.service"; then
|
|
if [[ -x "$INSTALLER" && -f "$CTID_FILE" ]]; then
|
|
log "Service failed to start; attempting reinstall"
|
|
bash "$INSTALLER" --ctid "$(cat "$CTID_FILE")" --skip-restart --quiet || log "Reinstall attempt failed"
|
|
systemctl start "${SERVICE}.service" || true
|
|
fi
|
|
fi
|
|
EOF
|
|
chmod 0755 "$SELFHEAL_SCRIPT"
|
|
|
|
cat > "$SELFHEAL_SERVICE_UNIT" <<EOF
|
|
[Unit]
|
|
Description=Pulse Sensor Proxy Self-Heal
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=${SELFHEAL_SCRIPT}
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
cat > "$SELFHEAL_TIMER_UNIT" <<'EOF'
|
|
[Unit]
|
|
Description=Ensure pulse-sensor-proxy stays installed and running
|
|
|
|
[Timer]
|
|
OnBootSec=5min
|
|
OnUnitActiveSec=30min
|
|
Unit=pulse-sensor-proxy-selfheal.service
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now pulse-sensor-proxy-selfheal.timer >/dev/null 2>&1 || true
|
|
|
|
if [ "$QUIET" = true ]; then
|
|
print_success "pulse-sensor-proxy installed and running"
|
|
else
|
|
print_info "${GREEN}Installation complete!${NC}"
|
|
print_info ""
|
|
print_info "Temperature monitoring will use the secure host-side proxy"
|
|
print_info ""
|
|
|
|
if [[ "$STANDALONE" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo " Docker Container Configuration Required"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo ""
|
|
print_info "${YELLOW}IMPORTANT:${NC} If Pulse is running in Docker, add this bind mount to your docker-compose.yml:"
|
|
echo ""
|
|
echo " volumes:"
|
|
echo " - pulse-data:/data"
|
|
echo " - /run/pulse-sensor-proxy:/run/pulse-sensor-proxy:ro"
|
|
echo ""
|
|
print_info "Then restart your Pulse container:"
|
|
echo " docker-compose down && docker-compose up -d"
|
|
echo ""
|
|
print_info "Or if using Docker directly:"
|
|
echo " docker restart pulse"
|
|
echo ""
|
|
fi
|
|
|
|
print_info "To check proxy status:"
|
|
print_info " systemctl status pulse-sensor-proxy"
|
|
|
|
if [[ "$STANDALONE" == true ]]; then
|
|
echo ""
|
|
print_info "After restarting Pulse, verify the socket is accessible:"
|
|
print_info " docker exec pulse ls -l /run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
|
|
echo ""
|
|
print_info "Check Pulse logs for temperature proxy detection:"
|
|
print_info " docker logs pulse | grep -i 'temperature.*proxy'"
|
|
echo ""
|
|
print_info "For detailed documentation, see:"
|
|
print_info " https://github.com/rcourtman/Pulse/blob/main/docs/TEMPERATURE_MONITORING.md"
|
|
fi
|
|
fi
|
|
|
|
exit 0
|