git init
This commit is contained in:
307
scripts/vps-agentctl.sh
Executable file
307
scripts/vps-agentctl.sh
Executable file
@@ -0,0 +1,307 @@
|
||||
#!/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://github.com/vps-control/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
|
||||
Reference in New Issue
Block a user