mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
chore: snapshot current changes
This commit is contained in:
parent
fb22469eb0
commit
5c4be1921c
38 changed files with 11149 additions and 4102 deletions
|
|
@ -48,10 +48,25 @@ export FRONTEND_DEV_HOST FRONTEND_DEV_PORT
|
|||
export PULSE_DEV_API_HOST PULSE_DEV_API_PORT PULSE_DEV_API_URL PULSE_DEV_WS_URL
|
||||
|
||||
# Auto-detect pulse-sensor-proxy socket if available
|
||||
HOST_PROXY_SOCKET="/mnt/pulse-proxy/pulse-sensor-proxy.sock"
|
||||
CONTAINER_PROXY_SOCKET="/run/pulse-sensor-proxy/pulse-sensor-proxy.sock"
|
||||
|
||||
if [[ -z ${PULSE_SENSOR_PROXY_SOCKET:-} ]]; then
|
||||
if [[ -S /mnt/pulse-proxy/pulse-sensor-proxy.sock ]]; then
|
||||
export PULSE_SENSOR_PROXY_SOCKET=/mnt/pulse-proxy/pulse-sensor-proxy.sock
|
||||
if [[ -S "${HOST_PROXY_SOCKET}" ]]; then
|
||||
export PULSE_SENSOR_PROXY_SOCKET="${HOST_PROXY_SOCKET}"
|
||||
printf "[hot-dev] Detected pulse-sensor-proxy socket at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}"
|
||||
elif [[ -S "${CONTAINER_PROXY_SOCKET}" ]]; then
|
||||
export PULSE_SENSOR_PROXY_SOCKET="${CONTAINER_PROXY_SOCKET}"
|
||||
printf "[hot-dev] WARNING: Using container-local pulse-sensor-proxy socket at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}"
|
||||
printf "[hot-dev] WARNING: Host proxy is missing; temperatures will not reach Pulse until it is reinstalled.\n"
|
||||
else
|
||||
printf "[hot-dev] WARNING: No pulse-sensor-proxy socket detected. Temperatures will be unavailable.\n"
|
||||
fi
|
||||
else
|
||||
if [[ ! -S "${PULSE_SENSOR_PROXY_SOCKET}" ]]; then
|
||||
printf "[hot-dev] WARNING: Configured pulse-sensor-proxy socket not found at %s\n" "${PULSE_SENSOR_PROXY_SOCKET}"
|
||||
elif [[ "${PULSE_SENSOR_PROXY_SOCKET}" == "${CONTAINER_PROXY_SOCKET}" && ! -S "${HOST_PROXY_SOCKET}" ]]; then
|
||||
printf "[hot-dev] WARNING: Using container-local proxy socket; reinstall host pulse-sensor-proxy for real telemetry.\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -697,6 +697,7 @@ download_agent_binary() {
|
|||
fi
|
||||
|
||||
if http::download "${download_args[@]}"; then
|
||||
AGENT_DOWNLOAD_SOURCE="${primary_url}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -711,6 +712,7 @@ download_agent_binary() {
|
|||
download_args+=(--insecure)
|
||||
fi
|
||||
if http::download "${download_args[@]}"; then
|
||||
AGENT_DOWNLOAD_SOURCE="${fallback_url}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -722,6 +724,8 @@ download_agent_binary() {
|
|||
return 1
|
||||
}
|
||||
|
||||
unset AGENT_DOWNLOAD_SOURCE
|
||||
|
||||
if download_agent_binary "$DOWNLOAD_URL" "$DOWNLOAD_URL_BASE"; then
|
||||
:
|
||||
else
|
||||
|
|
@ -730,6 +734,103 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
fetch_checksum_header() {
|
||||
local url="$1"
|
||||
local header=""
|
||||
|
||||
if command -v curl &> /dev/null; then
|
||||
local curl_args=(-fsSI "$url")
|
||||
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
|
||||
curl_args=(-k "${curl_args[@]}")
|
||||
fi
|
||||
header=$(curl "${curl_args[@]}" 2>/dev/null || true)
|
||||
elif command -v wget &> /dev/null; then
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
|
||||
wget --spider --no-check-certificate --server-response "$url" >/dev/null 2>"$tmp" || true
|
||||
else
|
||||
wget --spider --server-response "$url" >/dev/null 2>"$tmp" || true
|
||||
fi
|
||||
header=$(cat "$tmp" 2>/dev/null || true)
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
|
||||
if [[ -z "$header" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local checksum_line
|
||||
checksum_line=$(printf '%s\n' "$header" | awk 'BEGIN{IGNORECASE=1} /^ *X-Checksum-Sha256:/{print $0; exit}')
|
||||
if [[ -z "$checksum_line" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local value
|
||||
value=$(printf '%s\n' "$checksum_line" | awk -F':' '{sub(/^[[:space:]]*/,"",$2); print $2}')
|
||||
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ -z "$value" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
FETCHED_CHECKSUM="$value"
|
||||
return 0
|
||||
}
|
||||
|
||||
calculate_sha256() {
|
||||
local file="$1"
|
||||
local hash=""
|
||||
|
||||
if command -v sha256sum &> /dev/null; then
|
||||
hash=$(sha256sum "$file" | awk '{print $1}')
|
||||
elif command -v shasum &> /dev/null; then
|
||||
hash=$(shasum -a 256 "$file" | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [[ -z "$hash" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
CALCULATED_CHECKSUM=$(printf '%s' "$hash" | tr '[:upper:]' '[:lower:]')
|
||||
return 0
|
||||
}
|
||||
|
||||
verify_agent_checksum() {
|
||||
local url="$1"
|
||||
|
||||
if common::is_dry_run; then
|
||||
log_info '[dry-run] Skipping checksum verification'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! fetch_checksum_header "$url"; then
|
||||
log_warn 'Agent download did not include X-Checksum-Sha256 header; skipping verification'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! calculate_sha256 "$AGENT_PATH"; then
|
||||
log_warn 'Unable to calculate sha256 checksum locally; skipping verification'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$FETCHED_CHECKSUM" != "$CALCULATED_CHECKSUM" ]]; then
|
||||
rm -f "$AGENT_PATH"
|
||||
log_error "Checksum mismatch. Expected $FETCHED_CHECKSUM but downloaded $CALCULATED_CHECKSUM"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success 'Checksum verified for agent binary'
|
||||
unset FETCHED_CHECKSUM CALCULATED_CHECKSUM
|
||||
return 0
|
||||
}
|
||||
|
||||
if [[ -n "${AGENT_DOWNLOAD_SOURCE:-}" ]]; then
|
||||
if ! verify_agent_checksum "$AGENT_DOWNLOAD_SOURCE"; then
|
||||
log_error 'Agent download failed checksum verification'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! common::is_dry_run; then
|
||||
chmod +x "$AGENT_PATH"
|
||||
fi
|
||||
|
|
@ -841,8 +942,8 @@ SYSTEMD_ENV_INSECURE_LINE="Environment=\"PULSE_INSECURE_SKIP_VERIFY=$PRIMARY_INS
|
|||
systemd::create_service "$SERVICE_PATH" <<EOF
|
||||
[Unit]
|
||||
Description=Pulse Docker Agent
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.socket docker.service
|
||||
Wants=network-online.target docker.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
|
@ -854,6 +955,22 @@ ExecStart=$AGENT_PATH --url "$PRIMARY_URL" --interval "$INTERVAL"$NO_AUTO_UPDATE
|
|||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
User=root
|
||||
ProtectSystem=full
|
||||
ProtectHome=read-only
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelLogs=yes
|
||||
UMask=0077
|
||||
NoNewPrivileges=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictRealtime=yes
|
||||
PrivateTmp=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadWritePaths=/var/run/docker.sock
|
||||
ProtectHostname=yes
|
||||
ProtectClock=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
|
|
@ -343,6 +343,193 @@ resolve_agent_path_for_uninstall() {
|
|||
printf '%s' "$DEFAULT_AGENT_PATH"
|
||||
}
|
||||
|
||||
ensure_service_user() {
|
||||
SERVICE_USER_ACTUAL="$SERVICE_USER"
|
||||
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
|
||||
SERVICE_USER_AVAILABLE="true"
|
||||
|
||||
if [[ "$SERVICE_USER" == "root" ]]; then
|
||||
SERVICE_GROUP_ACTUAL="root"
|
||||
SERVICE_USER_AVAILABLE="false"
|
||||
return
|
||||
fi
|
||||
|
||||
if id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
if getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
|
||||
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
|
||||
else
|
||||
SERVICE_GROUP_ACTUAL="$(id -gn "$SERVICE_USER")"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v useradd >/dev/null 2>&1; then
|
||||
if useradd --system --home-dir "$SERVICE_HOME" --shell /usr/sbin/nologin "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
SERVICE_USER_CREATED="true"
|
||||
fi
|
||||
elif command -v adduser >/dev/null 2>&1; then
|
||||
if adduser --system --home "$SERVICE_HOME" --shell /usr/sbin/nologin "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
SERVICE_USER_CREATED="true"
|
||||
fi
|
||||
else
|
||||
log_warn "Unable to create dedicated service user; running agent as root"
|
||||
SERVICE_USER_ACTUAL="root"
|
||||
SERVICE_GROUP_ACTUAL="root"
|
||||
SERVICE_USER_AVAILABLE="false"
|
||||
return
|
||||
fi
|
||||
|
||||
if id -u "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
SERVICE_USER_ACTUAL="$SERVICE_USER"
|
||||
if getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
|
||||
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
|
||||
else
|
||||
SERVICE_GROUP_ACTUAL="$(id -gn "$SERVICE_USER")"
|
||||
fi
|
||||
if [[ "$SERVICE_USER_CREATED" == "true" ]]; then
|
||||
log_success "Created service user: $SERVICE_USER"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
log_warn "Failed to create service user; falling back to root"
|
||||
SERVICE_USER_ACTUAL="root"
|
||||
SERVICE_GROUP_ACTUAL="root"
|
||||
SERVICE_USER_AVAILABLE="false"
|
||||
}
|
||||
|
||||
ensure_service_home() {
|
||||
if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "$SERVICE_HOME" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SERVICE_HOME" ]]; then
|
||||
mkdir -p "$SERVICE_HOME"
|
||||
fi
|
||||
|
||||
chown "$SERVICE_USER_ACTUAL":"$SERVICE_GROUP_ACTUAL" "$SERVICE_HOME" >/dev/null 2>&1 || true
|
||||
chmod 750 "$SERVICE_HOME" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
ensure_docker_group_membership() {
|
||||
SYSTEMD_SUPPLEMENTARY_GROUPS_LINE=""
|
||||
if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if getent group docker >/dev/null 2>&1; then
|
||||
DOCKER_GROUP_PRESENT="true"
|
||||
if ! id -nG "$SERVICE_USER_ACTUAL" 2>/dev/null | tr ' ' '\n' | grep -Fxq "docker"; then
|
||||
if command -v usermod >/dev/null 2>&1; then
|
||||
usermod -a -G docker "$SERVICE_USER_ACTUAL" >/dev/null 2>&1 || log_warn "Failed to add $SERVICE_USER_ACTUAL to docker group; adjust socket permissions manually."
|
||||
elif command -v adduser >/dev/null 2>&1; then
|
||||
adduser "$SERVICE_USER_ACTUAL" docker >/dev/null 2>&1 || log_warn "Failed to add $SERVICE_USER_ACTUAL to docker group; adjust socket permissions manually."
|
||||
else
|
||||
log_warn "Unable to manage docker group membership; ensure $SERVICE_USER_ACTUAL can access /var/run/docker.sock"
|
||||
fi
|
||||
fi
|
||||
|
||||
if id -nG "$SERVICE_USER_ACTUAL" 2>/dev/null | tr ' ' '\n' | grep -Fxq "docker"; then
|
||||
SYSTEMD_SUPPLEMENTARY_GROUPS_LINE="SupplementaryGroups=docker"
|
||||
log_success "Ensured docker group access for $SERVICE_USER_ACTUAL"
|
||||
else
|
||||
log_warn "Service user $SERVICE_USER_ACTUAL is not in docker group; ensure the Docker socket ACL grants access."
|
||||
fi
|
||||
else
|
||||
log_warn "docker group not found; ensure the agent user can access /var/run/docker.sock"
|
||||
fi
|
||||
}
|
||||
|
||||
write_env_file() {
|
||||
local target="$ENV_FILE"
|
||||
local dir
|
||||
dir=$(dirname "$target")
|
||||
mkdir -p "$dir"
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp "${target}.XXXXXX")
|
||||
chmod 600 "$tmp"
|
||||
|
||||
{
|
||||
if [[ -n "$PRIMARY_URL" ]]; then
|
||||
printf 'PULSE_URL=%q\n' "$PRIMARY_URL"
|
||||
fi
|
||||
if [[ -n "$PRIMARY_TOKEN" ]]; then
|
||||
printf 'PULSE_TOKEN=%q\n' "$PRIMARY_TOKEN"
|
||||
fi
|
||||
if [[ -n "$JOINED_TARGETS" ]]; then
|
||||
printf 'PULSE_TARGETS=%q\n' "$JOINED_TARGETS"
|
||||
fi
|
||||
if [[ -n "$PRIMARY_INSECURE" ]]; then
|
||||
printf 'PULSE_INSECURE_SKIP_VERIFY=%q\n' "$PRIMARY_INSECURE"
|
||||
fi
|
||||
if [[ -n "$INTERVAL" ]]; then
|
||||
printf 'PULSE_INTERVAL=%q\n' "$INTERVAL"
|
||||
fi
|
||||
if [[ -n "$NO_AUTO_UPDATE_FLAG" ]]; then
|
||||
printf 'PULSE_NO_AUTO_UPDATE=true\n'
|
||||
fi
|
||||
} > "$tmp"
|
||||
|
||||
chown root:root "$tmp"
|
||||
chmod 600 "$tmp"
|
||||
mv "$tmp" "$target"
|
||||
log_success "Wrote environment file: $target"
|
||||
}
|
||||
|
||||
configure_polkit_rule() {
|
||||
if [[ "$SERVICE_USER_ACTUAL" == "root" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local polkit_dir="/etc/polkit-1/rules.d"
|
||||
if [[ ! -d "$polkit_dir" ]]; then
|
||||
log_warn "polkit not detected; remote stop commands may require manual sudo access"
|
||||
return
|
||||
fi
|
||||
|
||||
local rule_path="${polkit_dir}/90-pulse-docker-agent.rules"
|
||||
local tmp
|
||||
tmp=$(mktemp "${rule_path}.XXXXXX")
|
||||
cat > "$tmp" <<EOF
|
||||
// Pulse Docker agent installer managed rule
|
||||
polkit.addRule(function(action, subject) {
|
||||
if ((action.id == "org.freedesktop.systemd1.manage-units" ||
|
||||
action.id == "org.freedesktop.systemd1.manage-unit-files") &&
|
||||
subject.user == "$SERVICE_USER_ACTUAL") {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
EOF
|
||||
|
||||
chown root:root "$tmp" 2>/dev/null || true
|
||||
chmod 0644 "$tmp"
|
||||
|
||||
if [[ -f "$rule_path" ]]; then
|
||||
if command -v cmp >/dev/null 2>&1 && cmp -s "$tmp" "$rule_path" 2>/dev/null; then
|
||||
rm -f "$tmp"
|
||||
log_info "polkit rule already present for $SERVICE_USER_ACTUAL"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
mv "$tmp" "$rule_path"
|
||||
log_success "Configured polkit rule allowing $SERVICE_USER_ACTUAL to manage pulse-docker-agent service"
|
||||
}
|
||||
|
||||
remove_polkit_rule() {
|
||||
local polkit_dir="/etc/polkit-1/rules.d"
|
||||
local rule_path="${polkit_dir}/90-pulse-docker-agent.rules"
|
||||
if [[ -f "$rule_path" ]] && grep -q 'Pulse Docker agent installer managed rule' "$rule_path" 2>/dev/null; then
|
||||
rm -f "$rule_path"
|
||||
log_success "Removed polkit rule: $rule_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pulse Docker Agent Installer/Uninstaller
|
||||
# Install (single target):
|
||||
# curl -fsSL http://pulse.example.com/install-docker-agent.sh | bash -s -- --url http://pulse.example.com --token <api-token>
|
||||
|
|
@ -370,6 +557,7 @@ DEFAULT_AGENT_PATH_WRITABLE="unknown"
|
|||
EXISTING_AGENT_PATH=""
|
||||
AGENT_PATH=""
|
||||
SERVICE_PATH="/etc/systemd/system/pulse-docker-agent.service"
|
||||
ENV_FILE="/etc/pulse/pulse-docker-agent.env"
|
||||
UNRAID_STARTUP="/boot/config/go.d/pulse-docker-agent.sh"
|
||||
LOG_PATH="/var/log/pulse-docker-agent.log"
|
||||
INTERVAL="30s"
|
||||
|
|
@ -385,6 +573,15 @@ PRIMARY_TOKEN=""
|
|||
PRIMARY_INSECURE="false"
|
||||
JOINED_TARGETS=""
|
||||
ORIGINAL_ARGS=("$@")
|
||||
SERVICE_USER="pulse-docker"
|
||||
SERVICE_GROUP="$SERVICE_USER"
|
||||
SERVICE_HOME="/var/lib/pulse-docker-agent"
|
||||
SERVICE_USER_ACTUAL="$SERVICE_USER"
|
||||
SERVICE_GROUP_ACTUAL="$SERVICE_GROUP"
|
||||
SERVICE_USER_CREATED="false"
|
||||
SERVICE_USER_AVAILABLE="true"
|
||||
DOCKER_GROUP_PRESENT="false"
|
||||
SYSTEMD_SUPPLEMENTARY_GROUPS_LINE=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
|
@ -687,6 +884,11 @@ if [ "$UNINSTALL" = true ]; then
|
|||
log_warn "systemctl not found; skipping service disable"
|
||||
fi
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
rm -f "$ENV_FILE"
|
||||
log_success "Removed environment file: $ENV_FILE"
|
||||
fi
|
||||
|
||||
if pgrep -f pulse-docker-agent > /dev/null 2>&1; then
|
||||
log_info "Stopping running agent processes"
|
||||
pkill -f pulse-docker-agent 2>/dev/null || true
|
||||
|
|
@ -705,6 +907,8 @@ if [ "$UNINSTALL" = true ]; then
|
|||
log_success "Removed Unraid startup script: $UNRAID_STARTUP"
|
||||
fi
|
||||
|
||||
remove_polkit_rule
|
||||
|
||||
if [ "$PURGE" = true ]; then
|
||||
if [ -f "$LOG_PATH" ]; then
|
||||
rm -f "$LOG_PATH"
|
||||
|
|
@ -712,6 +916,10 @@ if [ "$UNINSTALL" = true ]; then
|
|||
else
|
||||
log_info "Agent log file already absent: $LOG_PATH"
|
||||
fi
|
||||
if [[ -d "$SERVICE_HOME" ]]; then
|
||||
rm -rf "$SERVICE_HOME"
|
||||
log_success "Removed service home directory: $SERVICE_HOME"
|
||||
fi
|
||||
elif [ -f "$LOG_PATH" ]; then
|
||||
log_info "Preserving agent log file at $LOG_PATH (use --purge to remove)"
|
||||
fi
|
||||
|
|
@ -946,16 +1154,109 @@ download_agent_from_url() {
|
|||
return 1
|
||||
}
|
||||
|
||||
fetch_checksum_header() {
|
||||
local url="$1"
|
||||
local header=""
|
||||
|
||||
if command -v curl &> /dev/null; then
|
||||
local curl_args=(-fsSI "$url")
|
||||
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
|
||||
curl_args=(-k "${curl_args[@]}")
|
||||
fi
|
||||
header=$(curl "${curl_args[@]}" 2>/dev/null || true)
|
||||
elif command -v wget &> /dev/null; then
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
if [[ "$PRIMARY_INSECURE" == "true" ]]; then
|
||||
wget --spider --no-check-certificate --server-response "$url" >/dev/null 2>"$tmp" || true
|
||||
else
|
||||
wget --spider --server-response "$url" >/dev/null 2>"$tmp" || true
|
||||
fi
|
||||
header=$(cat "$tmp" 2>/dev/null || true)
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
|
||||
if [[ -z "$header" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local checksum_line
|
||||
checksum_line=$(printf '%s\n' "$header" | awk 'BEGIN{IGNORECASE=1} /^ *X-Checksum-Sha256:/{print $0; exit}')
|
||||
if [[ -z "$checksum_line" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local value
|
||||
value=$(printf '%s\n' "$checksum_line" | awk -F':' '{sub(/^[[:space:]]*/,"",$2); print $2}')
|
||||
value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ -z "$value" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
FETCHED_CHECKSUM="$value"
|
||||
return 0
|
||||
}
|
||||
|
||||
calculate_sha256() {
|
||||
local file="$1"
|
||||
local hash=""
|
||||
|
||||
if command -v sha256sum &> /dev/null; then
|
||||
hash=$(sha256sum "$file" | awk '{print $1}')
|
||||
elif command -v shasum &> /dev/null; then
|
||||
hash=$(shasum -a 256 "$file" | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [[ -z "$hash" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
CALCULATED_CHECKSUM=$(printf '%s' "$hash" | tr '[:upper:]' '[:lower:]')
|
||||
return 0
|
||||
}
|
||||
|
||||
verify_agent_checksum() {
|
||||
local url="$1"
|
||||
if ! fetch_checksum_header "$url"; then
|
||||
log_warn 'Agent download did not include X-Checksum-Sha256 header; skipping verification'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! calculate_sha256 "$AGENT_PATH"; then
|
||||
log_warn 'Unable to calculate sha256 checksum locally; skipping verification'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$FETCHED_CHECKSUM" != "$CALCULATED_CHECKSUM" ]]; then
|
||||
rm -f "$AGENT_PATH"
|
||||
log_error "Checksum mismatch. Expected $FETCHED_CHECKSUM but downloaded $CALCULATED_CHECKSUM"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success 'Checksum verified for agent binary'
|
||||
unset FETCHED_CHECKSUM CALCULATED_CHECKSUM
|
||||
return 0
|
||||
}
|
||||
|
||||
DOWNLOAD_SUCCESS_URL=""
|
||||
if download_agent_from_url "$DOWNLOAD_URL"; then
|
||||
:
|
||||
DOWNLOAD_SUCCESS_URL="$DOWNLOAD_URL"
|
||||
elif [[ "$DOWNLOAD_URL" != "$DOWNLOAD_URL_BASE" ]] && download_agent_from_url "$DOWNLOAD_URL_BASE"; then
|
||||
log_info 'Falling back to server default agent binary'
|
||||
DOWNLOAD_SUCCESS_URL="$DOWNLOAD_URL_BASE"
|
||||
else
|
||||
log_warn 'Failed to download agent binary'
|
||||
log_warn "Ensure the Pulse server is reachable at $PRIMARY_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$DOWNLOAD_SUCCESS_URL" ]]; then
|
||||
if ! verify_agent_checksum "$DOWNLOAD_SUCCESS_URL"; then
|
||||
log_error 'Agent download failed checksum verification'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x "$AGENT_PATH"
|
||||
log_success "Agent binary installed"
|
||||
|
||||
|
|
@ -991,9 +1292,11 @@ allow_reenroll_if_needed() {
|
|||
fi
|
||||
|
||||
if [[ "$success" == "true" ]]; then
|
||||
log_success "Cleared any previous stop block for host"
|
||||
log_success "Cleared previous removal block via Pulse API"
|
||||
else
|
||||
log_warn "Unable to confirm removal block clearance (continuing)"
|
||||
log_warn 'Pulse still considers this host removed.'
|
||||
log_warn 'If you just reinstalled, visit Pulse → Docker → Removed Hosts and allow re-enroll,'
|
||||
log_warn 'or rerun this installer with an API token that includes the docker:manage scope.'
|
||||
fi
|
||||
|
||||
return 0
|
||||
|
|
@ -1070,31 +1373,49 @@ if [[ -n "$NO_AUTO_UPDATE_FLAG" ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
log_header 'Preparing service environment'
|
||||
ensure_service_user
|
||||
ensure_service_home
|
||||
ensure_docker_group_membership
|
||||
write_env_file
|
||||
configure_polkit_rule
|
||||
|
||||
# Create systemd service
|
||||
log_header 'Configuring systemd service'
|
||||
SYSTEMD_ENV_TARGETS_LINE=""
|
||||
if [[ -n "$JOINED_TARGETS" ]]; then
|
||||
SYSTEMD_ENV_TARGETS_LINE="Environment=\"PULSE_TARGETS=$JOINED_TARGETS\""
|
||||
fi
|
||||
SYSTEMD_ENV_URL_LINE="Environment=\"PULSE_URL=$PRIMARY_URL\""
|
||||
SYSTEMD_ENV_TOKEN_LINE="Environment=\"PULSE_TOKEN=$PRIMARY_TOKEN\""
|
||||
SYSTEMD_ENV_INSECURE_LINE="Environment=\"PULSE_INSECURE_SKIP_VERIFY=$PRIMARY_INSECURE\""
|
||||
cat > "$SERVICE_PATH" << EOF
|
||||
[Unit]
|
||||
Description=Pulse Docker Agent
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.socket docker.service
|
||||
Wants=network-online.target docker.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
$SYSTEMD_ENV_URL_LINE
|
||||
$SYSTEMD_ENV_TOKEN_LINE
|
||||
$SYSTEMD_ENV_TARGETS_LINE
|
||||
$SYSTEMD_ENV_INSECURE_LINE
|
||||
EnvironmentFile=-$ENV_FILE
|
||||
ExecStart=$AGENT_PATH --url "$PRIMARY_URL" --interval "$INTERVAL"$NO_AUTO_UPDATE_FLAG
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
User=root
|
||||
StartLimitIntervalSec=120
|
||||
StartLimitBurst=5
|
||||
User=$SERVICE_USER_ACTUAL
|
||||
Group=$SERVICE_GROUP_ACTUAL
|
||||
$SYSTEMD_SUPPLEMENTARY_GROUPS_LINE
|
||||
UMask=0077
|
||||
NoNewPrivileges=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictRealtime=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=full
|
||||
ProtectHome=read-only
|
||||
ProtectControlGroups=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelLogs=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
ReadWritePaths=/var/run/docker.sock
|
||||
ProtectHostname=yes
|
||||
ProtectClock=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ INTERVAL="30s"
|
|||
UNINSTALL="false"
|
||||
PLATFORM=""
|
||||
FORCE=false
|
||||
KEYCHAIN_ENABLED=true
|
||||
KEYCHAIN_OPT_OUT=false
|
||||
KEYCHAIN_OPT_OUT_REASON=""
|
||||
USE_KEYCHAIN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -94,6 +98,12 @@ while [[ $# -gt 0 ]]; do
|
|||
FORCE=true
|
||||
shift
|
||||
;;
|
||||
--no-keychain)
|
||||
KEYCHAIN_ENABLED=false
|
||||
KEYCHAIN_OPT_OUT=true
|
||||
KEYCHAIN_OPT_OUT_REASON="flag"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
|
|
@ -136,6 +146,10 @@ fi
|
|||
|
||||
print_header
|
||||
|
||||
if [[ "$FORCE" == true ]]; then
|
||||
log_warn "--force enabled: skipping interactive confirmations and accepting secure defaults."
|
||||
fi
|
||||
|
||||
# Interactive prompts if parameters not provided (unless --force is used)
|
||||
if [[ -z "$PULSE_URL" ]]; then
|
||||
if [[ "$FORCE" == false ]]; then
|
||||
|
|
@ -148,7 +162,10 @@ fi
|
|||
|
||||
if [[ -z "$PULSE_URL" ]]; then
|
||||
log_error "Pulse URL is required"
|
||||
echo "Usage: $0 --url <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force]"
|
||||
echo "Usage: $0 --url <pulse-url> --token <api-token> [--interval 30s] [--platform linux|darwin|windows] [--force] [--no-keychain]"
|
||||
echo ""
|
||||
echo " --force Skip interactive prompts and accept secure defaults (including Keychain storage)."
|
||||
echo " --no-keychain Disable Keychain storage and embed the token in the launch agent plist instead."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -416,8 +433,32 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
|
|||
mkdir -p "$MACOS_LOG_DIR"
|
||||
mkdir -p "$HOME/Library/LaunchAgents"
|
||||
|
||||
if [[ -n "$PULSE_TOKEN" && "$KEYCHAIN_ENABLED" == true && "$FORCE" == false ]]; then
|
||||
echo ""
|
||||
log_info "It is recommended to store the token in your Keychain so it never lands on disk."
|
||||
KEYCHAIN_PROMPTED=false
|
||||
if [[ -t 0 ]]; then
|
||||
read -r -p "Store the token in the macOS Keychain? [Y/n]: " KEYCHAIN_RESPONSE
|
||||
KEYCHAIN_PROMPTED=true
|
||||
elif [[ -r /dev/tty ]]; then
|
||||
read -r -p "Store the token in the macOS Keychain? [Y/n]: " KEYCHAIN_RESPONSE </dev/tty
|
||||
KEYCHAIN_PROMPTED=true
|
||||
else
|
||||
log_warn "No interactive terminal detected; defaulting to Keychain storage. Use --no-keychain to opt out."
|
||||
fi
|
||||
if [[ "$KEYCHAIN_PROMPTED" == true && "$KEYCHAIN_RESPONSE" =~ ^[Nn] ]]; then
|
||||
KEYCHAIN_ENABLED=false
|
||||
KEYCHAIN_OPT_OUT=true
|
||||
KEYCHAIN_OPT_OUT_REASON="prompt"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Store token in macOS Keychain for better security
|
||||
if [[ -n "$PULSE_TOKEN" ]]; then
|
||||
if [[ -n "$PULSE_TOKEN" && "$KEYCHAIN_ENABLED" == true ]]; then
|
||||
log_info "For security, the token is stored in your macOS Keychain so it never lands on disk."
|
||||
log_info "macOS may ask to allow access the first time the agent runs."
|
||||
log_info "Use --no-keychain to opt out (the token will be embedded in the launchd plist instead)."
|
||||
log_info "Storing token in macOS Keychain..."
|
||||
|
||||
# Delete existing keychain entry if it exists
|
||||
|
|
@ -429,12 +470,13 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
|
|||
|
||||
KEYCHAIN_APPS=(
|
||||
"/usr/local/bin/pulse-host-agent"
|
||||
"/usr/local/bin/pulse-host-agent-wrapper.sh"
|
||||
"/usr/bin/security"
|
||||
)
|
||||
KEYCHAIN_ARGS=()
|
||||
for app in "${KEYCHAIN_APPS[@]}"; do
|
||||
KEYCHAIN_ARGS+=(-T "$app")
|
||||
if [[ -e "$app" ]]; then
|
||||
KEYCHAIN_ARGS+=(-T "$app")
|
||||
fi
|
||||
done
|
||||
|
||||
if security add-generic-password \
|
||||
|
|
@ -456,6 +498,17 @@ elif [[ "$PLATFORM" == "darwin" ]] && command -v launchctl &> /dev/null; then
|
|||
log_info "You may need to grant Keychain access permissions"
|
||||
USE_KEYCHAIN=false
|
||||
fi
|
||||
elif [[ -n "$PULSE_TOKEN" ]]; then
|
||||
if [[ "$KEYCHAIN_OPT_OUT" == true ]]; then
|
||||
if [[ "$KEYCHAIN_OPT_OUT_REASON" == "flag" ]]; then
|
||||
log_warn "Keychain storage disabled via --no-keychain; token will be embedded in the launchd plist."
|
||||
elif [[ "$KEYCHAIN_OPT_OUT_REASON" == "prompt" ]]; then
|
||||
log_warn "Keychain storage skipped at user prompt; token will be embedded in the launchd plist."
|
||||
fi
|
||||
else
|
||||
log_warn "Keychain storage disabled; token will be embedded in the launchd plist."
|
||||
fi
|
||||
USE_KEYCHAIN=false
|
||||
else
|
||||
USE_KEYCHAIN=false
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -67,12 +67,19 @@ 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_SCRIPT_PATH="/usr/local/bin/pulse-sensor-cleanup.sh"
|
||||
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"
|
||||
SHARE_DIR="/usr/local/share/pulse"
|
||||
STORED_INSTALLER="${SHARE_DIR}/install-sensor-proxy.sh"
|
||||
SELFHEAL_SCRIPT="/usr/local/bin/pulse-sensor-proxy-selfheal.sh"
|
||||
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}")"
|
||||
|
||||
cleanup_local_authorized_keys() {
|
||||
local auth_keys_file="/root/.ssh/authorized_keys"
|
||||
|
|
@ -198,6 +205,34 @@ EOF
|
|||
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
|
||||
|
|
@ -558,6 +593,11 @@ 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
|
||||
|
||||
# Create config file with ACL for Docker containers (standalone mode)
|
||||
if [[ "$STANDALONE" == true ]]; then
|
||||
print_info "Creating config file with Docker container ACL..."
|
||||
|
|
@ -946,7 +986,7 @@ LimitNOFILE=1024
|
|||
SERVICE_EOF
|
||||
|
||||
# Enable and start the path unit
|
||||
systemctl daemon-reload
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable pulse-sensor-cleanup.path
|
||||
systemctl start pulse-sensor-cleanup.path
|
||||
|
||||
|
|
@ -1261,7 +1301,7 @@ for key_type in id_rsa id_dsa id_ecdsa id_ed25519; do
|
|||
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"
|
||||
print_info " Removed /root/.ssh/$key_type"
|
||||
fi
|
||||
done
|
||||
|
||||
|
|
@ -1272,6 +1312,88 @@ if [ "$LEGACY_KEYS_FOUND" = true ] && [ "$QUIET" != true ]; then
|
|||
fi
|
||||
fi # End of container-specific configuration
|
||||
|
||||
# Install self-heal safeguards to keep proxy available
|
||||
print_info "Configuring self-heal safeguards..."
|
||||
if [[ -n "$SCRIPT_SOURCE" && -f "$SCRIPT_SOURCE" ]]; then
|
||||
install -d "$SHARE_DIR"
|
||||
cp "$SCRIPT_SOURCE" "$STORED_INSTALLER"
|
||||
chmod 0755 "$STORED_INSTALLER"
|
||||
else
|
||||
print_warn "Unable to cache installer script for self-heal (source path unavailable)"
|
||||
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 | 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=/usr/local/bin/pulse-sensor-proxy-selfheal.sh
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue