#!/usr/bin/env bash
# Vigilant Vole installer for Linux with systemd (Debian, Ubuntu, and others).
#
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ferdinand99/Vigilant-Vole/main/deploy/install.sh)"
# bash -c "$(curl -fsSL .../deploy/install.sh)" -- update
# bash -c "$(curl -fsSL .../deploy/install.sh)" -- uninstall [--purge]
#
# Commands:
# install (default) install or repair; safe to run again
# update replace the binary with the latest (or --version) release,
# keep data and settings, restart whichever services are installed
# uninstall remove the program and the services; keep data and settings.
# With both a server and an agent installed it asks which one
# to remove (or pass --server / --agent; no terminal: both)
# uninstall --purge also delete their data and settings (and the user, once
# nothing is left)
#
# Mode: run in a terminal without a mode and the installer asks whether to set
# up the server (web UI, database) or a remote agent (runs checks for another
# Vigilant Vole server), then asks for the agent's server URL, token and name.
# Without a terminal it installs the server, as before.
# --server install the server (no questions)
# --agent install a remote probe agent (vigilant-vole-agent.service)
# VV_INSTALL_MODE=server|agent the same, from the environment
#
# Options:
# --version vX.Y.Z install this release instead of the latest
# --port N server: HTTP port (default 3001; written to the env file)
# --server URL agent: URL of your Vigilant Vole server (implies --agent;
# or VV_AGENT_SERVER)
# --token T agent: agent token vva_... (or VV_AGENT_TOKEN, which stays out of `ps`)
# --name NAME agent: display name (or VV_AGENT_NAME; default: hostname)
# --yes don't ask (uninstall: remove everything; --purge: delete data)
#
# Layout:
# /opt/vigilant-vole/vigilant-vole binary (+ /usr/local/bin/vigilant-vole)
# /var/lib/vigilant-vole data: database, session key, backups
# /etc/vigilant-vole/vigilant-vole.env server settings (kept on update)
# /etc/vigilant-vole/agent.env agent settings (mode 0600)
# vigilant-vole.service / vigilant-vole-agent.service
#
# Development only: VV_INSTALL_BASE_URL=http://host:port downloads
# // instead of GitHub releases (and /latest holds the
# tag to use as "latest"). Not for production use.
set -euo pipefail
readonly REPO="Ferdinand99/Vigilant-Vole"
readonly APP_DIR="/opt/vigilant-vole"
readonly BIN="$APP_DIR/vigilant-vole"
readonly BIN_LINK="/usr/local/bin/vigilant-vole"
readonly DATA_DIR="/var/lib/vigilant-vole"
readonly CONF_DIR="/etc/vigilant-vole"
readonly ENV_FILE="$CONF_DIR/vigilant-vole.env"
readonly AGENT_ENV_FILE="$CONF_DIR/agent.env"
readonly UNIT_DIR="/etc/systemd/system"
readonly SERVICE="vigilant-vole"
readonly AGENT_SERVICE="vigilant-vole-agent"
readonly SVC_USER="vigilant-vole"
readonly DEFAULT_PORT="3001"
# Characters the agent settings may not contain (they're written in quotes to
# a systemd EnvironmentFile).
readonly BAD_CHARS='["\\$`[:cntrl:]]'
command_name="install"
want_version=""
port=""
# server | agent | "" (ask in a terminal, else server)
install_mode=""
mode_flag=""
server_url_flag=0
agent_server="${VV_AGENT_SERVER:-}"
agent_token="${VV_AGENT_TOKEN:-}"
agent_name="${VV_AGENT_NAME:-}"
purge=0
assume_yes=0
tmp_dir=""
if [[ -t 1 ]]; then
c_bold=$'\e[1m' c_green=$'\e[32m' c_yellow=$'\e[33m' c_red=$'\e[31m' c_reset=$'\e[0m'
else
c_bold="" c_green="" c_yellow="" c_red="" c_reset=""
fi
info() { printf '%s==>%s %s\n' "$c_bold" "$c_reset" "$*"; }
ok() { printf '%s ok%s %s\n' "$c_green" "$c_reset" "$*"; }
warn() { printf '%swarning:%s %s\n' "$c_yellow" "$c_reset" "$*" >&2; }
die() {
printf '%serror:%s %s\n' "$c_red" "$c_reset" "$*" >&2
exit 1
}
cleanup() {
if [[ -n "$tmp_dir" && -d "$tmp_dir" ]]; then
rm -rf "$tmp_dir"
fi
}
trap cleanup EXIT
usage() {
sed -n '2,/^set -euo pipefail/{/^set -euo/d;s/^# \{0,1\}//;p}' "${BASH_SOURCE[0]}" 2>/dev/null ||
echo "usage: install.sh [install|update|uninstall [--purge]] [--server | --agent [--server URL --token T --name N]] [--version vX.Y.Z] [--port N]"
}
set_mode_flag() {
if [[ -n "$mode_flag" && "$mode_flag" != "$1" ]]; then
die "--agent and --server (without a URL) contradict each other; pick one"
fi
mode_flag="$1"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
install | update | uninstall) command_name="$1" ;;
--version)
[[ $# -ge 2 ]] || die "--version needs a value, e.g. --version v0.3.0"
want_version="$2"
shift
;;
--version=*) want_version="${1#*=}" ;;
--port)
[[ $# -ge 2 ]] || die "--port needs a value"
port="$2"
shift
;;
--port=*) port="${1#*=}" ;;
--agent) set_mode_flag agent ;;
--server)
# `--server URL` is the agent's server; a bare `--server` picks server mode.
if [[ $# -ge 2 && "$2" != -* && "$2" != install && "$2" != update && "$2" != uninstall ]]; then
agent_server="$2"
server_url_flag=1
shift
else
set_mode_flag server
fi
;;
--server=*)
agent_server="${1#*=}"
server_url_flag=1
;;
--token)
[[ $# -ge 2 ]] || die "--token needs a value"
agent_token="$2"
shift
;;
--token=*) agent_token="${1#*=}" ;;
--name)
[[ $# -ge 2 ]] || die "--name needs a value"
agent_name="$2"
shift
;;
--name=*) agent_name="${1#*=}" ;;
--purge) purge=1 ;;
--yes | -y) assume_yes=1 ;;
-h | --help)
usage
exit 0
;;
--) ;;
*) die "unknown argument: $1 (see --help)" ;;
esac
shift
done
if [[ -n "$want_version" ]]; then
[[ "$want_version" == v* ]] || want_version="v$want_version"
[[ "$want_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]] ||
die "--version must look like v1.2.3 (got $want_version)"
fi
if [[ -n "$port" ]]; then
if ! [[ "$port" =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then
die "--port must be 1-65535 (got $port)"
fi
fi
if [[ "$purge" == 1 && "$command_name" != "uninstall" ]]; then
die "--purge only goes with uninstall"
fi
# Flags win over VV_INSTALL_MODE; `--server URL` alone means an agent.
local env_mode="${VV_INSTALL_MODE:-}"
env_mode="${env_mode,,}"
case "$env_mode" in
"" | server | agent) ;;
*) die "VV_INSTALL_MODE must be server or agent (got ${VV_INSTALL_MODE})" ;;
esac
install_mode="${mode_flag:-$env_mode}"
if [[ -z "$install_mode" && "$server_url_flag" == 1 ]]; then
install_mode="agent"
fi
if [[ "$command_name" == "install" && "$install_mode" == "server" && "$server_url_flag" == 1 ]]; then
die "--server URL belongs to an agent install (--agent); for the server itself use --server without a URL"
fi
}
# Prints what is wrong with the agent settings, or nothing. They end up quoted
# in a systemd EnvironmentFile, so quotes, backslashes, $ and backticks are out.
agent_setting_error() {
if [[ -z "$agent_server" ]]; then
echo "the agent needs the URL of your Vigilant Vole server: --server https://status.example.com (or VV_AGENT_SERVER)"
elif ! [[ "$agent_server" =~ ^https?://[^[:space:]/]+ ]] || [[ "$agent_server" =~ [[:space:]] || "$agent_server" =~ $BAD_CHARS ]]; then
echo "the server URL must start with http:// or https:// and contain no spaces, quotes, backslashes, \$ or backticks"
elif [[ -z "$agent_token" ]]; then
echo "the agent needs its token: set VV_AGENT_TOKEN=vva_... or pass --token (create one under Settings -> Remote agents on the server)"
elif [[ "$agent_token" != vva_?* || "$agent_token" =~ [[:space:]] || "$agent_token" =~ $BAD_CHARS ]]; then
echo "that is not an agent token: agent tokens start with vva_ (Settings -> Remote agents on the server)"
elif [[ "$agent_name" =~ $BAD_CHARS ]]; then
echo "the agent name must not contain quotes, backslashes, \$, backticks or control characters"
fi
}
validate_agent_settings() {
local err
err="$(agent_setting_error)"
[[ -z "$err" ]] || die "$err"
}
# Questions only go to a person at a terminal. `bash -c "$(curl ...)"` keeps
# stdin on the terminal; `curl ... | bash` and automation don't, and get the
# server (or whatever the flags and VV_* variables say) without being asked.
interactive() {
[[ -t 0 ]]
}
ask() {
# ask [-s]: reads one line into ; EOF is an error.
local _answer=""
if [[ "${3:-}" == "-s" ]]; then
IFS= read -r -s -p "$1" _answer || die "no answer (input closed)"
echo >&2
else
IFS= read -r -p "$1" _answer || die "no answer (input closed)"
fi
# Trim surrounding whitespace (pasted tokens often bring a newline or space).
_answer="${_answer#"${_answer%%[![:space:]]*}"}"
_answer="${_answer%"${_answer##*[![:space:]]}"}"
printf -v "$2" '%s' "$_answer"
}
choose_mode() {
[[ -n "$install_mode" ]] && return 0
if ! interactive; then
install_mode="server"
return 0
fi
local default=1 answer=""
if unit_exists "$AGENT_SERVICE" && ! unit_exists "$SERVICE"; then
default=2
fi
{
echo
printf '%sWhat should this machine run?%s\n' "$c_bold" "$c_reset"
echo " 1) Server (web UI, database)"
echo " 2) Remote agent (runs checks for another Vigilant Vole server)"
} >&2
while true; do
ask "Choose 1 or 2 [$default]: " answer
case "${answer,,}" in
"") [[ "$default" == 2 ]] && install_mode="agent" || install_mode="server" ;;
1 | s | server) install_mode="server" ;;
2 | a | agent) install_mode="agent" ;;
*)
warn "answer 1 (server) or 2 (remote agent)"
continue
;;
esac
break
done
}
# Asks for whatever agent setting the flags and VV_AGENT_* didn't provide.
prompt_agent_settings() {
interactive || return 0
local err asked=0
if [[ -z "$agent_server" || -z "$agent_token" ]]; then
{
echo
echo "Create the agent on your Vigilant Vole server first: Settings -> Remote agents -> New agent."
echo "It shows the token (vva_...) once."
} >&2
fi
if [[ -z "$agent_server" ]]; then
asked=1
while true; do
ask "Server URL (e.g. https://status.example.com): " agent_server
if [[ -z "$agent_server" ]]; then
warn "enter the URL of your Vigilant Vole server"
continue
fi
err="$(agent_setting_error)"
[[ "$err" == "the server URL"* ]] || break
warn "$err"
agent_server=""
done
fi
if [[ -z "$agent_token" ]]; then
asked=1
while true; do
ask "Agent token (vva_..., input hidden): " agent_token -s
if [[ "$agent_token" == vva_?* && ! "$agent_token" =~ [[:space:]] && ! "$agent_token" =~ $BAD_CHARS ]]; then
break
fi
warn "that is not an agent token: they start with vva_ (Settings -> Remote agents on the server)"
agent_token=""
done
fi
if [[ "$asked" == 1 && -z "$agent_name" ]]; then
local host
host="$(hostname 2>/dev/null || echo agent)"
while true; do
ask "Agent name [$host]: " agent_name
[[ "$agent_name" =~ $BAD_CHARS ]] || break
warn "the name must not contain quotes, backslashes, \$ or backticks"
agent_name=""
done
fi
}
# Warnings only: the agent retries until the server is reachable, and a wrong
# token can be fixed in the env file afterwards. The token goes to curl on
# stdin (-K -), not on its command line.
check_server_reachable() {
local base="${agent_server%/}" code=""
if ! curl -fsS --max-time 5 -o /dev/null "$base/healthz" 2>/dev/null; then
warn "could not reach $base/healthz from this machine. Installing anyway; the agent keeps retrying. Check the URL, DNS and firewall if it stays offline."
return 0
fi
code="$(printf 'header = "Authorization: Bearer %s"\n' "$agent_token" |
curl -sS --max-time 10 -o /dev/null -w '%{http_code}' -K - "$base/api/agent/v1/monitors" 2>/dev/null)" || code=""
case "$code" in
200 | 304) ok "Reached $base, which accepts the agent token" ;;
401)
agent_state="refused"
warn "$base rejected this agent token (mistyped, rotated or deleted). Installing anyway; put the right token in $AGENT_ENV_FILE afterwards and run: systemctl restart $AGENT_SERVICE"
;;
403)
agent_state="refused"
warn "this agent is disabled on $base; enable it under Settings -> Remote agents"
;;
*) ok "Reached $base" ;;
esac
}
require_root() {
[[ "$(id -u)" == 0 ]] || die "run this as root (e.g. with sudo)"
}
require_systemd() {
command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]] ||
die "systemd is not running here. This installer sets up a systemd service; on other systems run the binary yourself or use the Docker image (ghcr.io/ferdinand99/vigilant-vole)."
}
detect_target() {
case "$(uname -m)" in
x86_64 | amd64) echo "x86_64-unknown-linux-musl" ;;
aarch64 | arm64) echo "aarch64-unknown-linux-musl" ;;
*) die "unsupported CPU architecture $(uname -m): release builds exist for x86_64 and aarch64 only (build from source, or use the Docker image)" ;;
esac
}
ensure_tools() {
local missing=()
command -v curl >/dev/null 2>&1 || missing+=(curl)
command -v tar >/dev/null 2>&1 || missing+=(tar)
command -v gzip >/dev/null 2>&1 || missing+=(gzip)
command -v sha256sum >/dev/null 2>&1 || missing+=(coreutils)
command -v useradd >/dev/null 2>&1 || missing+=(passwd)
# The server refuses to start without a system CA store (HTTPS checks and
# notifications verify against it).
if [[ ! -s /etc/ssl/certs/ca-certificates.crt && ! -s /etc/pki/tls/certs/ca-bundle.crt && ! -s /etc/ssl/cert.pem ]]; then
missing+=(ca-certificates)
fi
((${#missing[@]} == 0)) && return 0
if command -v apt-get >/dev/null 2>&1; then
info "Installing ${missing[*]}"
DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${missing[@]}" >/dev/null
else
die "missing tools: ${missing[*]}. Install them with your package manager and run this again."
fi
}
download() {
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 -o "$2" "$1" ||
die "download failed: $1"
}
# Prints the release tag to install.
resolve_version() {
if [[ -n "$want_version" ]]; then
echo "$want_version"
return
fi
local tag=""
if [[ -n "${VV_INSTALL_BASE_URL:-}" ]]; then
tag="$(curl -fsSL "${VV_INSTALL_BASE_URL%/}/latest" | tr -d '[:space:]')" ||
die "could not read ${VV_INSTALL_BASE_URL%/}/latest"
else
# The /releases/latest page redirects to /releases/tag/: no API, no rate limit.
local url
url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' "https://github.com/$REPO/releases/latest")" ||
die "could not reach github.com to look up the latest release"
tag="${url##*/}"
fi
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]] || die "could not work out the latest release (got '$tag'); pass --version vX.Y.Z"
echo "$tag"
}
release_url() {
if [[ -n "${VV_INSTALL_BASE_URL:-}" ]]; then
echo "${VV_INSTALL_BASE_URL%/}/$1/$2"
else
echo "https://github.com/$REPO/releases/download/$1/$2"
fi
}
installed_version() {
[[ -x "$BIN" ]] || return 0
"$BIN" --version 2>/dev/null | awk '{print $2}'
}
# Downloads, verifies and unpacks the release into $tmp_dir; sets new_bin.
new_bin=""
fetch_release() {
local tag="$1" target asset
target="$(detect_target)"
asset="vigilant-vole-${target}.tar.gz"
tmp_dir="$(mktemp -d)"
info "Downloading Vigilant Vole $tag ($target)"
download "$(release_url "$tag" "$asset")" "$tmp_dir/$asset"
download "$(release_url "$tag" SHA256SUMS)" "$tmp_dir/SHA256SUMS"
local expected actual
expected="$(awk -v f="$asset" '$2 == f || $2 == "*" f { print $1 }' "$tmp_dir/SHA256SUMS")"
[[ -n "$expected" ]] || die "SHA256SUMS of $tag has no entry for $asset"
actual="$(sha256sum "$tmp_dir/$asset" | awk '{print $1}')"
[[ "$expected" == "$actual" ]] || die "checksum mismatch for $asset (expected $expected, got $actual)"
ok "Checksum verified"
tar -xzf "$tmp_dir/$asset" -C "$tmp_dir" --no-same-owner
new_bin="$tmp_dir/vigilant-vole-${target}/vigilant-vole"
[[ -f "$new_bin" ]] || die "$asset does not contain vigilant-vole-${target}/vigilant-vole"
chmod 0755 "$new_bin"
"$new_bin" --version >/dev/null 2>&1 || die "the downloaded binary does not run on this system"
}
# Puts a new binary in place atomically (same directory, then rename).
place_binary() {
mkdir -p "$APP_DIR"
install -m 0755 -o root -g root "$1" "$APP_DIR/.vigilant-vole.new"
mv -f "$APP_DIR/.vigilant-vole.new" "$BIN"
ln -sfn "$BIN" "$BIN_LINK"
}
ensure_user() {
if ! getent group "$SVC_USER" >/dev/null; then
groupadd --system "$SVC_USER"
fi
if ! id -u "$SVC_USER" >/dev/null 2>&1; then
local shell
shell="$(command -v nologin 2>/dev/null || echo /bin/false)"
useradd --system --gid "$SVC_USER" --home-dir "$DATA_DIR" --no-create-home \
--shell "$shell" --comment "Vigilant Vole" "$SVC_USER"
ok "Created system user $SVC_USER"
fi
}
ensure_dirs() {
install -d -m 0750 -o "$SVC_USER" -g "$SVC_USER" "$DATA_DIR"
# Data restored or copied in as root stays readable for the service.
chown -R "$SVC_USER:$SVC_USER" "$DATA_DIR"
install -d -m 0750 -o root -g "$SVC_USER" "$CONF_DIR"
}
env_changed=0
write_env_file() {
if [[ -f "$ENV_FILE" ]]; then
if [[ -n "$port" && "$(env_port)" != "$port" ]]; then
env_changed=1
if grep -q '^PORT=' "$ENV_FILE"; then
sed -i "s/^PORT=.*/PORT=$port/" "$ENV_FILE"
else
printf 'PORT=%s\n' "$port" >>"$ENV_FILE"
fi
ok "Set PORT=$port in $ENV_FILE"
fi
return
fi
cat >"$ENV_FILE" < for the first-run setup page, so nobody else
# can claim a fresh instance that is reachable from the network.
#VV_SETUP_TOKEN=
# Merge a YAML/JSON config document into the database on every start.
#VV_CONFIG_FILE=/etc/vigilant-vole/vigilant.yml
# Checks allowed to run at the same time (default 64).
#VV_MAX_CONCURRENT_CHECKS=64
# Docker monitors and label discovery (the vigilant-vole user must be able to
# use the socket: usermod -aG docker vigilant-vole).
#DOCKER_HOST=unix:///var/run/docker.sock
# Tailscale ping monitors: path of the tailscaled socket.
#VV_TAILSCALE_SOCKET=/var/run/tailscale/tailscaled.sock
# Log filter.
#RUST_LOG=info
EOF
chown root:"$SVC_USER" "$ENV_FILE"
chmod 0640 "$ENV_FILE"
env_changed=1
ok "Wrote $ENV_FILE"
}
write_agent_env_file() {
local name_line="#VV_AGENT_NAME="
[[ -n "$agent_name" ]] && name_line="VV_AGENT_NAME=\"$agent_name\""
local old_umask before=""
[[ -f "$AGENT_ENV_FILE" ]] && before="$(cat "$AGENT_ENV_FILE")"
old_umask="$(umask)"
umask 077
cat >"$AGENT_ENV_FILE" <"$dest.tmp"
chmod 0644 "$dest.tmp"
mv -f "$dest.tmp" "$dest"
echo 1
}
render_server_unit() {
{
cat </dev/null 2>&1; then
ok "Vigilant Vole is up"
return 0
fi
systemctl is-failed --quiet "$SERVICE" && break
sleep 1
done
journalctl -u "$SERVICE" -n 30 --no-pager >&2 || true
die "Vigilant Vole did not answer on port $p. Check: journalctl -u $SERVICE"
}
# Waits up to ~15 s for the agent's first contact with the server. Sets
# agent_state to connected, refused or waiting; dies if the service stopped.
agent_state=""
wait_agent() {
local invocation="" logs="" i
if [[ "$agent_state" == "refused" ]]; then
sleep 2
systemctl is-active --quiet "$AGENT_SERVICE" ||
die "the agent did not start. Check $AGENT_ENV_FILE and: journalctl -u $AGENT_SERVICE"
return 0
fi
info "Waiting for the agent to reach its server"
for ((i = 0; i < 15; i++)); do
sleep 1
if ! systemctl is-active --quiet "$AGENT_SERVICE" && ((i >= 2)); then
break
fi
invocation="$(systemctl show -p InvocationID --value "$AGENT_SERVICE" 2>/dev/null)" || invocation=""
[[ -n "$invocation" ]] || continue
logs="$(journalctl --no-pager -o cat "_SYSTEMD_INVOCATION_ID=$invocation" 2>/dev/null)" || logs=""
if [[ "$logs" == *"connected to server"* ]]; then
agent_state="connected"
ok "Agent is running and connected to its server"
return 0
fi
if [[ "$logs" == *"server refused the agent"* ]]; then
agent_state="refused"
warn "the server rejected the agent's token (wrong, rotated or disabled agent). Put the right token in $AGENT_ENV_FILE, then: systemctl restart $AGENT_SERVICE"
return 0
fi
done
if systemctl is-active --quiet "$AGENT_SERVICE"; then
agent_state="waiting"
ok "Agent is running; it hasn't reached the server yet and keeps retrying (logs: journalctl -u $AGENT_SERVICE -f)"
return 0
fi
journalctl -u "$AGENT_SERVICE" -n 30 --no-pager >&2 || true
die "the agent did not start. Check $AGENT_ENV_FILE and: journalctl -u $AGENT_SERVICE"
}
agent_server_from_env() {
sed -n 's/^VV_AGENT_SERVER=["]\{0,1\}\([^"]*\)["]\{0,1\}[[:space:]]*$/\1/p' "$AGENT_ENV_FILE" 2>/dev/null | tail -n 1
}
print_agent_summary() {
local server
server="$(agent_server_from_env)"
echo
if [[ "$agent_state" == "refused" ]]; then
printf '%sThe Vigilant Vole agent is installed, but its token was rejected.%s\n' "$c_bold" "$c_reset"
else
printf '%sThe Vigilant Vole agent is running.%s\n' "$c_bold" "$c_reset"
fi
echo "See it on your server under Settings -> Remote agents: ${server%/}/settings/agents"
echo "(it shows as online once it has checked in). Tick \"Check from remote agents too\" on the"
echo "monitors it should run."
echo "Settings: $AGENT_ENV_FILE (mode 0600) Logs: journalctl -u $AGENT_SERVICE -f"
echo "Update: bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/$REPO/main/deploy/install.sh)\" -- update"
}
host_address() {
local a=""
a="$(hostname -I 2>/dev/null | awk '{print $1}')" || true
if [[ -z "$a" ]] && command -v ip >/dev/null 2>&1; then
a="$(ip -o -4 addr show scope global 2>/dev/null | awk '{sub(/\/.*/, "", $4); print $4; exit}')" || true
fi
echo "${a:-localhost}"
}
print_url() {
local p host token
p="$(env_port)"
host="$(host_address)"
token="$(sed -n 's/^VV_SETUP_TOKEN=["]\{0,1\}\([^"]*\)["]\{0,1\}[[:space:]]*$/\1/p' "$ENV_FILE" 2>/dev/null | tail -n 1)"
echo
printf '%sVigilant Vole is running:%s http://%s:%s\n' "$c_bold" "$c_reset" "$host" "$p"
if [[ -n "$token" ]]; then
echo "First visit: http://$host:$p/setup?token="
else
echo "Open it and create the admin account in the setup wizard right away."
echo "(Until then anyone who can reach it can. Set VV_SETUP_TOKEN in $ENV_FILE to lock that down.)"
fi
echo "Settings: $ENV_FILE Data: $DATA_DIR Logs: journalctl -u $SERVICE -f"
echo "Update: bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/$REPO/main/deploy/install.sh)\" -- update"
}
# Installs the requested release if it is not the one already in place.
# Sets binary_changed=1 when the binary was replaced.
binary_changed=0
install_binary() {
local tag current svc
tag="$(resolve_version)"
current="$(installed_version)"
if [[ -n "$current" && "v$current" == "$tag" ]]; then
ok "Vigilant Vole $tag is already installed"
ln -sfn "$BIN" "$BIN_LINK"
return 0
fi
fetch_release "$tag"
local active=()
if [[ -n "$current" ]]; then
for svc in "$SERVICE" "$AGENT_SERVICE"; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
active+=("$svc")
fi
done
if ((${#active[@]} > 0)); then
info "Stopping ${active[*]}"
systemctl stop "${active[@]}"
fi
fi
place_binary "$new_bin"
binary_changed=1
if [[ -n "$current" ]]; then
ok "Updated Vigilant Vole $current -> ${tag#v}"
else
ok "Installed Vigilant Vole ${tag#v} to $BIN"
fi
}
# Starts service $1, or restarts it when the binary, its unit ($2 = 1) or the
# settings changed.
apply_service() {
if [[ "$binary_changed" == 1 || "${2:-0}" == 1 || "$env_changed" == 1 ]]; then
systemctl restart "$1"
else
systemctl start "$1"
fi
}
cmd_install() {
require_root
require_systemd
detect_target >/dev/null
choose_mode
if [[ "$install_mode" == "agent" ]]; then
prompt_agent_settings
validate_agent_settings
fi
ensure_tools
if [[ "$install_mode" == "agent" ]]; then
check_server_reachable
fi
install_binary
ensure_user
if [[ "$install_mode" == "agent" ]]; then
install -d -m 0750 -o root -g "$SVC_USER" "$CONF_DIR"
write_agent_env_file
local agent_unit_changed
agent_unit_changed="$(put_unit "$AGENT_SERVICE" < <(render_agent_unit))"
systemctl daemon-reload
systemctl enable --quiet "$AGENT_SERVICE"
apply_service "$AGENT_SERVICE" "$agent_unit_changed"
if unit_exists "$SERVICE" && [[ "$binary_changed" == 1 ]]; then
systemctl restart "$SERVICE" || true
fi
wait_agent
print_agent_summary
return
fi
ensure_dirs
write_env_file
local server_unit_changed
server_unit_changed="$(put_unit "$SERVICE" < <(render_server_unit))"
systemctl daemon-reload
systemctl enable --quiet "$SERVICE"
apply_service "$SERVICE" "$server_unit_changed"
if unit_exists "$AGENT_SERVICE" && [[ "$binary_changed" == 1 ]]; then
systemctl restart "$AGENT_SERVICE" || true
fi
wait_healthy
print_url
}
cmd_update() {
require_root
require_systemd
local have_server=0 have_agent=0
unit_exists "$SERVICE" && have_server=1
unit_exists "$AGENT_SERVICE" && have_agent=1
if [[ ! -x "$BIN" ]] || ((have_server == 0 && have_agent == 0)); then
die "Vigilant Vole is not installed here (no $BIN with a service). Run the installer without 'update' first."
fi
ensure_tools
install_binary
# Newer installers may bring unit changes; the settings files stay as they are.
local server_unit_changed=0 agent_unit_changed=0
if ((have_server == 1)); then
server_unit_changed="$(put_unit "$SERVICE" < <(render_server_unit))"
fi
if ((have_agent == 1)); then
agent_unit_changed="$(put_unit "$AGENT_SERVICE" < <(render_agent_unit))"
fi
systemctl daemon-reload
if ((have_server == 1)); then
ensure_dirs
# Database migrations run on start; they back up the database to
# $DATA_DIR/backups first.
apply_service "$SERVICE" "$server_unit_changed"
wait_healthy
fi
if ((have_agent == 1)); then
apply_service "$AGENT_SERVICE" "$agent_unit_changed"
wait_agent
fi
ok "Vigilant Vole $(installed_version) is running"
}
remove_unit() {
if unit_exists "$1"; then
info "Removing $1.service"
systemctl disable --now --quiet "$1" 2>/dev/null || true
rm -f "$UNIT_DIR/$1.service"
fi
}
cmd_uninstall() {
require_root
local have_server=0 have_agent=0 rm_server=1 rm_agent=1
unit_exists "$SERVICE" && have_server=1
unit_exists "$AGENT_SERVICE" && have_agent=1
# What to remove: --server / --agent (or VV_INSTALL_MODE) pick one; with both
# installed a person at a terminal is asked; otherwise everything goes.
case "$install_mode" in
server) rm_agent=0 ;;
agent) rm_server=0 ;;
*)
if ((have_server == 1 && have_agent == 1)) && [[ "$assume_yes" != 1 ]] && interactive; then
local answer=""
{
echo "Both the Vigilant Vole server and a remote agent are installed here. Remove:"
echo " 1) the server (vigilant-vole.service)"
echo " 2) the agent (vigilant-vole-agent.service)"
echo " 3) both, and the program"
} >&2
while true; do
ask "Choose 1, 2 or 3 [3]: " answer
case "$answer" in
1) rm_agent=0 ;;
2) rm_server=0 ;;
"" | 3) ;;
*)
warn "answer 1, 2 or 3"
continue
;;
esac
break
done
fi
;;
esac
if [[ "$rm_server" == 1 && "$rm_agent" == 0 && "$have_server" == 0 ]]; then
die "the server (vigilant-vole.service) is not installed here; nothing removed"
fi
if [[ "$rm_agent" == 1 && "$rm_server" == 0 && "$have_agent" == 0 ]]; then
die "no agent (vigilant-vole-agent.service) is installed here; nothing removed"
fi
# The program, its link and the user stay while the other service needs them.
local keep_program=0 what
if [[ "$rm_server" == 0 && "$have_server" == 1 ]] || [[ "$rm_agent" == 0 && "$have_agent" == 1 ]]; then
keep_program=1
fi
if [[ "$keep_program" == 1 && "$rm_server" == 1 ]]; then
what="the server's data in $DATA_DIR (database, backups) and $ENV_FILE"
elif [[ "$keep_program" == 1 ]]; then
what="the agent's settings in $AGENT_ENV_FILE"
else
what="$DATA_DIR (database, backups) and $CONF_DIR"
fi
# Ask before touching anything, so a declined purge changes nothing.
if [[ "$purge" == 1 && "$assume_yes" != 1 ]]; then
if interactive; then
local confirm=""
ask "Delete $what for good? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]] || die "nothing removed (answer y to confirm, or run uninstall without --purge)"
else
die "uninstall --purge deletes $what; add --yes to confirm when not running interactively"
fi
fi
if [[ "$rm_server" == 1 ]]; then
remove_unit "$SERVICE"
fi
if [[ "$rm_agent" == 1 ]]; then
remove_unit "$AGENT_SERVICE"
fi
command -v systemctl >/dev/null 2>&1 && systemctl daemon-reload 2>/dev/null || true
if [[ "$keep_program" == 1 ]]; then
if [[ "$rm_server" == 1 ]]; then
ok "Removed the server; the agent keeps running"
else
ok "Removed the agent; the server keeps running"
fi
if [[ "$purge" != 1 ]]; then
if [[ "$rm_server" == 1 ]]; then
echo "Kept the server's data in $DATA_DIR and settings in $ENV_FILE."
else
echo "Kept the agent's settings in $AGENT_ENV_FILE."
fi
echo "Delete them too with: uninstall --purge $([[ "$rm_server" == 1 ]] && echo --server || echo --agent)"
return
fi
if [[ "$rm_server" == 1 ]]; then
rm -rf "$DATA_DIR" "$ENV_FILE" "$UNIT_DIR/$SERVICE.service.d"
ok "Deleted the server's data and settings"
else
rm -rf "$AGENT_ENV_FILE" "$UNIT_DIR/$AGENT_SERVICE.service.d"
ok "Deleted the agent's settings"
fi
return
fi
if [[ -L "$BIN_LINK" || -f "$BIN_LINK" ]]; then
rm -f "$BIN_LINK"
fi
rm -rf "$APP_DIR"
ok "Removed the program and its services"
if [[ "$purge" != 1 ]]; then
echo "Kept your data in $DATA_DIR and settings in $CONF_DIR."
echo "Delete them too with: uninstall --purge"
return
fi
rm -rf "$DATA_DIR" "$CONF_DIR" "$UNIT_DIR/$SERVICE.service.d" "$UNIT_DIR/$AGENT_SERVICE.service.d"
if id -u "$SVC_USER" >/dev/null 2>&1; then
userdel "$SVC_USER" 2>/dev/null || warn "could not remove user $SVC_USER"
fi
if getent group "$SVC_USER" >/dev/null; then
groupdel "$SVC_USER" 2>/dev/null || true
fi
ok "Deleted data, settings and the $SVC_USER user"
}
main() {
parse_args "$@"
if [[ -n "${VV_INSTALL_BASE_URL:-}" && "$command_name" != "uninstall" ]]; then
warn "VV_INSTALL_BASE_URL is set: downloading from ${VV_INSTALL_BASE_URL} (development only)"
fi
case "$command_name" in
install) cmd_install ;;
update) cmd_update ;;
uninstall) cmd_uninstall ;;
esac
}
# `bash -c "$(curl ...)" update` puts the first word in $0, not $1: take it back.
case "$0" in
install | update | uninstall | --*) set -- "$0" "$@" ;;
esac
main "$@"