308 lines
9.7 KiB
Bash
Executable File
308 lines
9.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
readonly PROGRAM="vps-agent"
|
|
readonly BINARY="/usr/local/bin/vps-agent"
|
|
readonly PREVIOUS="/usr/local/lib/vps-agent/vps-agent.previous"
|
|
readonly CONFIG_DIR="/etc/vps-agent"
|
|
readonly CREDENTIAL_DIR="${CONFIG_DIR}/credentials"
|
|
readonly STATE_DIR="/var/lib/vps-agent"
|
|
readonly UNIT="/etc/systemd/system/vps-agent.service"
|
|
readonly DEFAULT_RELEASES="https://gitea.hyperdog.dev/dizyaka/vps-control/releases"
|
|
|
|
VERSION="latest"
|
|
RELEASES_URL="${VPS_AGENT_RELEASES_URL:-$DEFAULT_RELEASES}"
|
|
PURGE=false
|
|
ASSUME_YES=false
|
|
TMP_DIR=""
|
|
DOWNLOADED_BINARY=""
|
|
|
|
usage() {
|
|
printf '%s\n' "Usage: $0 <install|update|remove|rollback|status|restart|version> [options]" \
|
|
" --version VERSION release tag, or latest" \
|
|
" --releases-url URL releases root (default: VPS_AGENT_RELEASES_URL)" \
|
|
" --purge remove configuration and credentials" \
|
|
" --yes confirm destructive purge"
|
|
}
|
|
|
|
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
|
info() { printf '==> %s\n' "$*"; }
|
|
|
|
cleanup() {
|
|
if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
|
|
rm -rf -- "$TMP_DIR"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
require_root() { [[ ${EUID:-$(id -u)} -eq 0 ]] || die "run this action as root"; }
|
|
|
|
check_host() {
|
|
[[ "$(uname -s)" == "Linux" ]] || die "only Linux is supported"
|
|
command -v systemctl >/dev/null || die "systemd is required"
|
|
[[ -d /run/systemd/system ]] || die "systemd is not running"
|
|
}
|
|
|
|
release_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) printf 'amd64\n' ;;
|
|
aarch64|arm64) printf 'arm64\n' ;;
|
|
*) die "unsupported architecture: $(uname -m)" ;;
|
|
esac
|
|
}
|
|
|
|
download_release() {
|
|
command -v curl >/dev/null || die "curl is required"
|
|
command -v sha256sum >/dev/null || die "sha256sum is required"
|
|
TMP_DIR="$(mktemp -d -t vps-agent.XXXXXXXX)"
|
|
chmod 700 "$TMP_DIR"
|
|
local artifact="vps-agent-linux-$(release_arch)"
|
|
local release_url
|
|
if [[ "$VERSION" == "latest" ]]; then
|
|
release_url="${RELEASES_URL%/}/latest/download"
|
|
else
|
|
[[ "$VERSION" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]] || die "invalid release version"
|
|
release_url="${RELEASES_URL%/}/download/${VERSION}"
|
|
fi
|
|
info "downloading ${artifact} (${VERSION})"
|
|
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
|
|
--output "$TMP_DIR/$artifact" "$release_url/$artifact"
|
|
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
|
|
--output "$TMP_DIR/SHA256SUMS" "$release_url/SHA256SUMS"
|
|
|
|
local expected actual
|
|
expected="$(awk -v name="$artifact" '$2 == name || $2 == "*" name { print $1; exit }' "$TMP_DIR/SHA256SUMS")"
|
|
[[ "$expected" =~ ^[a-fA-F0-9]{64}$ ]] || die "artifact checksum is absent or malformed"
|
|
actual="$(sha256sum "$TMP_DIR/$artifact" | awk '{print $1}')"
|
|
[[ "$actual" == "$expected" ]] || die "checksum verification failed"
|
|
|
|
if [[ -n "${VPS_AGENT_MINISIGN_PUBLIC_KEY:-}" ]]; then
|
|
command -v minisign >/dev/null || die "minisign is required when a public key is configured"
|
|
curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \
|
|
--output "$TMP_DIR/SHA256SUMS.minisig" "$release_url/SHA256SUMS.minisig"
|
|
minisign -Vm "$TMP_DIR/SHA256SUMS" -P "$VPS_AGENT_MINISIGN_PUBLIC_KEY"
|
|
fi
|
|
chmod 0755 "$TMP_DIR/$artifact"
|
|
"$TMP_DIR/$artifact" --version >/dev/null || die "downloaded binary self-check failed"
|
|
DOWNLOADED_BINARY="$TMP_DIR/$artifact"
|
|
}
|
|
|
|
install_unit() {
|
|
if [[ -f "$(cd "$(dirname "$0")" && pwd)/../agent/packaging/vps-agent.service" ]]; then
|
|
install -o root -g root -m 0644 \
|
|
"$(cd "$(dirname "$0")" && pwd)/../agent/packaging/vps-agent.service" "$UNIT"
|
|
return
|
|
fi
|
|
install -o root -g root -m 0644 /dev/null "$UNIT"
|
|
tee "$UNIT" >/dev/null <<'EOF'
|
|
[Unit]
|
|
Description=VPS Control monitoring agent
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
[Service]
|
|
Type=simple
|
|
User=vps-agent
|
|
Group=vps-agent
|
|
ExecStart=/usr/local/bin/vps-agent --config /etc/vps-agent/config.toml
|
|
Restart=on-failure
|
|
RestartSec=5s
|
|
TimeoutStopSec=15s
|
|
MemoryHigh=48M
|
|
MemoryMax=64M
|
|
CPUQuota=10%
|
|
TasksMax=64
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
PrivateDevices=true
|
|
ProtectHome=true
|
|
ProtectSystem=strict
|
|
ProtectKernelTunables=true
|
|
ProtectKernelModules=true
|
|
ProtectKernelLogs=true
|
|
ProtectControlGroups=true
|
|
ProtectClock=true
|
|
ProtectHostname=true
|
|
RestrictSUIDSGID=true
|
|
LockPersonality=true
|
|
RestrictRealtime=true
|
|
RestrictNamespaces=true
|
|
CapabilityBoundingSet=
|
|
AmbientCapabilities=
|
|
SystemCallArchitectures=native
|
|
UMask=0077
|
|
ProcSubset=all
|
|
ProtectProc=invisible
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
}
|
|
|
|
create_config() {
|
|
[[ -f "$CONFIG_DIR/config.toml" ]] && return
|
|
install -o root -g vps-agent -m 0640 /dev/null "$CONFIG_DIR/config.toml"
|
|
tee "$CONFIG_DIR/config.toml" >/dev/null <<'EOF'
|
|
[server]
|
|
listen = "127.0.0.1:9105"
|
|
[auth]
|
|
mode = "token"
|
|
token_file = "/etc/vps-agent/credentials/control-plane.token"
|
|
[limits]
|
|
max_concurrent_requests = 32
|
|
request_timeout_seconds = 5
|
|
max_body_bytes = 65536
|
|
read_requests_per_second = 10.0
|
|
write_requests_per_second = 2.0
|
|
burst = 20.0
|
|
command_timeout_seconds = 4
|
|
[services]
|
|
allowed = []
|
|
[firewall]
|
|
management_enabled = false
|
|
rollback_timeout_seconds = 60
|
|
max_snapshots = 5
|
|
[logging]
|
|
level = "info"
|
|
[telemetry]
|
|
connections_enabled = true
|
|
process_info_enabled = true
|
|
[logs]
|
|
enabled = false
|
|
allowed_units = ["vps-agent.service"]
|
|
max_entries_per_request = 200
|
|
max_message_bytes = 16384
|
|
[tls]
|
|
enabled = false
|
|
certificate_file = ""
|
|
private_key_file = ""
|
|
EOF
|
|
}
|
|
|
|
create_token() {
|
|
local token_file="$CREDENTIAL_DIR/control-plane.token"
|
|
[[ -f "$token_file" ]] && return
|
|
if command -v openssl >/dev/null; then
|
|
umask 077
|
|
openssl rand -hex 32 > "$token_file"
|
|
else
|
|
umask 077
|
|
od -An -N32 -tx1 /dev/urandom | tr -d ' \n' > "$token_file"
|
|
printf '\n' >> "$token_file"
|
|
fi
|
|
chown vps-agent:vps-agent "$token_file"
|
|
chmod 0600 "$token_file"
|
|
info "created agent token at $token_file; copy it through a secure channel"
|
|
}
|
|
|
|
wait_healthy() {
|
|
local attempts=15
|
|
while (( attempts > 0 )); do
|
|
if systemctl is-active --quiet vps-agent.service; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
attempts=$((attempts - 1))
|
|
done
|
|
systemctl status --no-pager vps-agent.service || true
|
|
return 1
|
|
}
|
|
|
|
do_install() {
|
|
require_root; check_host
|
|
[[ ! -e "$BINARY" ]] || die "$BINARY already exists; use update"
|
|
local downloaded
|
|
download_release
|
|
downloaded="$DOWNLOADED_BINARY"
|
|
getent group vps-agent >/dev/null || groupadd --system vps-agent
|
|
id -u vps-agent >/dev/null 2>&1 || useradd --system --gid vps-agent --home-dir /nonexistent --shell /usr/sbin/nologin vps-agent
|
|
install -d -o root -g vps-agent -m 0750 "$CONFIG_DIR" "$CREDENTIAL_DIR"
|
|
install -d -o vps-agent -g vps-agent -m 0700 "$STATE_DIR"
|
|
install -d -o root -g root -m 0755 "$(dirname "$PREVIOUS")"
|
|
install -o root -g root -m 0755 "$downloaded" "$BINARY"
|
|
create_config
|
|
create_token
|
|
install_unit
|
|
systemctl daemon-reload
|
|
systemctl enable --now vps-agent.service
|
|
wait_healthy || die "agent failed to become healthy"
|
|
systemctl status --no-pager vps-agent.service
|
|
}
|
|
|
|
do_update() {
|
|
require_root; check_host
|
|
[[ -x "$BINARY" ]] || die "agent is not installed"
|
|
local downloaded staged
|
|
download_release
|
|
downloaded="$DOWNLOADED_BINARY"
|
|
staged="$(dirname "$BINARY")/.vps-agent.new"
|
|
install -o root -g root -m 0755 "$downloaded" "$staged"
|
|
install -o root -g root -m 0755 "$BINARY" "$PREVIOUS"
|
|
mv -f -- "$staged" "$BINARY"
|
|
systemctl restart vps-agent.service
|
|
if ! wait_healthy; then
|
|
info "health check failed; restoring previous binary"
|
|
install -o root -g root -m 0755 "$PREVIOUS" "$BINARY"
|
|
systemctl restart vps-agent.service
|
|
wait_healthy || die "rollback also failed; inspect systemctl status"
|
|
die "update rolled back"
|
|
fi
|
|
info "updated to $($BINARY --version)"
|
|
}
|
|
|
|
do_rollback() {
|
|
require_root; check_host
|
|
[[ -x "$PREVIOUS" ]] || die "no previous binary is available"
|
|
local current="$(dirname "$BINARY")/.vps-agent.current"
|
|
install -o root -g root -m 0755 "$BINARY" "$current"
|
|
install -o root -g root -m 0755 "$PREVIOUS" "$BINARY"
|
|
mv -f -- "$current" "$PREVIOUS"
|
|
systemctl restart vps-agent.service
|
|
wait_healthy || die "rolled-back version did not become healthy"
|
|
}
|
|
|
|
do_remove() {
|
|
require_root; check_host
|
|
if [[ "$PURGE" == true && "$ASSUME_YES" != true ]]; then
|
|
[[ -t 0 ]] || die "--purge requires an interactive terminal or --yes"
|
|
read -r -p "Permanently delete $CONFIG_DIR and credentials? [y/N] " answer
|
|
[[ "$answer" == "y" || "$answer" == "Y" ]] || die "purge cancelled"
|
|
fi
|
|
systemctl disable --now vps-agent.service 2>/dev/null || true
|
|
rm -f -- "$UNIT" "$BINARY" "$PREVIOUS"
|
|
rm -rf -- "$STATE_DIR"
|
|
systemctl daemon-reload
|
|
if [[ "$PURGE" == true ]]; then
|
|
rm -rf -- "$CONFIG_DIR"
|
|
info "removed configuration and credentials; this cannot be recovered"
|
|
else
|
|
info "preserved $CONFIG_DIR (use remove --purge to delete it)"
|
|
fi
|
|
userdel vps-agent 2>/dev/null || true
|
|
groupdel vps-agent 2>/dev/null || true
|
|
}
|
|
|
|
[[ $# -ge 1 ]] || { usage; exit 2; }
|
|
ACTION="$1"; shift
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--version) [[ $# -ge 2 ]] || die "--version needs a value"; VERSION="$2"; shift 2 ;;
|
|
--releases-url) [[ $# -ge 2 ]] || die "--releases-url needs a value"; RELEASES_URL="$2"; shift 2 ;;
|
|
--purge) PURGE=true; shift ;;
|
|
--yes) ASSUME_YES=true; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) die "unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
case "$ACTION" in
|
|
install) do_install ;;
|
|
update) do_update ;;
|
|
remove) do_remove ;;
|
|
rollback) do_rollback ;;
|
|
status) systemctl status --no-pager vps-agent.service ;;
|
|
restart) require_root; systemctl restart vps-agent.service; wait_healthy ;;
|
|
version) [[ -x "$BINARY" ]] && "$BINARY" --version || die "agent is not installed" ;;
|
|
*) usage; exit 2 ;;
|
|
esac
|