266 lines
9.1 KiB
Bash
Executable File
266 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
INSTALLER_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
ENV_FILE="${ENV_FILE:-$INSTALLER_ROOT/.env}"
|
|
ENV_EXAMPLE="$INSTALLER_ROOT/.env.example"
|
|
COMPOSE_FILE="$INSTALLER_ROOT/compose/docker-compose.yml"
|
|
|
|
log() {
|
|
printf '[ours-rp-installer] %s\n' "$*"
|
|
}
|
|
|
|
warn() {
|
|
printf '[ours-rp-installer][WARN] %s\n' "$*" >&2
|
|
}
|
|
|
|
die() {
|
|
printf '[ours-rp-installer][ERROR] %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
|
|
}
|
|
|
|
load_env() {
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
[[ -f "$ENV_EXAMPLE" ]] || die "missing $ENV_EXAMPLE"
|
|
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
|
log "created .env from .env.example"
|
|
fi
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
set +a
|
|
HOST_DATA_DIR="${HOST_DATA_DIR:-/var/lib/ours-rp-arm64}"
|
|
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ours-rp-arm64}"
|
|
RPKI_IMAGE="${RPKI_IMAGE:-ours-rp-runtime-arm64:dev}"
|
|
RPKI_PLATFORM="${RPKI_PLATFORM:-linux/arm64}"
|
|
MONITOR_PLATFORM="${MONITOR_PLATFORM:-linux/arm64}"
|
|
PROMETHEUS_IMAGE="${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}"
|
|
GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}"
|
|
FIRST_RUN_WAIT_TIMEOUT_SECS="${FIRST_RUN_WAIT_TIMEOUT_SECS:-7200}"
|
|
}
|
|
|
|
compose_cmd() {
|
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -p "${COMPOSE_PROJECT_NAME:-ours-rp-arm64}" "$@"
|
|
}
|
|
|
|
create_data_dirs() {
|
|
load_env
|
|
mkdir -p \
|
|
"$HOST_DATA_DIR/state" \
|
|
"$HOST_DATA_DIR/runs" \
|
|
"$HOST_DATA_DIR/logs" \
|
|
"$HOST_DATA_DIR/tmp" \
|
|
"$HOST_DATA_DIR/prometheus" \
|
|
"$HOST_DATA_DIR/grafana"
|
|
chmod 755 "$HOST_DATA_DIR" "$HOST_DATA_DIR/state" "$HOST_DATA_DIR/runs" "$HOST_DATA_DIR/logs" "$HOST_DATA_DIR/tmp" || true
|
|
chmod 777 "$HOST_DATA_DIR/prometheus" "$HOST_DATA_DIR/grafana" || true
|
|
}
|
|
|
|
latest_run_dir() {
|
|
load_env
|
|
find "$HOST_DATA_DIR/runs" -maxdepth 1 -mindepth 1 -type d -name 'run_*' 2>/dev/null | sort | tail -1
|
|
}
|
|
|
|
latest_success_run_dir() {
|
|
load_env
|
|
find "$HOST_DATA_DIR/runs" -maxdepth 2 -type f -path '*/run-summary.json' 2>/dev/null \
|
|
| while read -r summary; do
|
|
if jq -e '.status == "success"' "$summary" >/dev/null 2>&1; then
|
|
dirname "$summary"
|
|
fi
|
|
done | sort | tail -1
|
|
}
|
|
|
|
has_success_run() {
|
|
[[ -n "$(latest_success_run_dir)" ]]
|
|
}
|
|
|
|
print_run_summary() {
|
|
local run_dir="$1"
|
|
local summary="$run_dir/run-summary.json"
|
|
local meta="$run_dir/run-meta.json"
|
|
local timing="$run_dir/stage-timing.json"
|
|
local process_time="$run_dir/process-time.txt"
|
|
local vrps_file="$run_dir/vrps.csv"
|
|
local vaps_file="$run_dir/vaps.csv"
|
|
local status="unknown"
|
|
local sync_mode="unknown"
|
|
local wall_ms="null"
|
|
local validation_ms="null"
|
|
local repo_sync_ms="null"
|
|
local max_rss_kb="null"
|
|
local publication_points="null"
|
|
local vrps="null"
|
|
local vaps="null"
|
|
local warnings="null"
|
|
[[ -f "$summary" ]] || {
|
|
warn "missing run-summary.json in $run_dir"
|
|
return 1
|
|
}
|
|
status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)"
|
|
wall_ms="$(jq -r '.wallMs // .wall_ms // "null"' "$summary" 2>/dev/null || echo null)"
|
|
warnings="$(jq -r '.warningCount // .warnings // "null"' "$summary" 2>/dev/null || echo null)"
|
|
if [[ -f "$meta" ]]; then
|
|
sync_mode="$(jq -r '.sync_mode // .syncMode // "unknown"' "$meta" 2>/dev/null || echo unknown)"
|
|
status="$(jq -r --arg fallback "$status" '.status // $fallback' "$meta" 2>/dev/null || echo "$status")"
|
|
fi
|
|
if [[ -f "$timing" ]]; then
|
|
validation_ms="$(jq -r '.validation_ms // "null"' "$timing" 2>/dev/null || echo null)"
|
|
repo_sync_ms="$(jq -r '.repo_sync_ms_total // "null"' "$timing" 2>/dev/null || echo null)"
|
|
publication_points="$(jq -r '.publication_points // "null"' "$timing" 2>/dev/null || echo null)"
|
|
fi
|
|
if [[ -f "$process_time" ]]; then
|
|
max_rss_kb="$(awk -F': ' '/Maximum resident set size/ {print $2; found=1} END {if (!found) print "null"}' "$process_time")"
|
|
fi
|
|
if [[ -f "$vrps_file" ]]; then
|
|
vrps="$(( $(wc -l < "$vrps_file") > 0 ? $(wc -l < "$vrps_file") - 1 : 0 ))"
|
|
fi
|
|
if [[ -f "$vaps_file" ]]; then
|
|
vaps="$(( $(wc -l < "$vaps_file") > 0 ? $(wc -l < "$vaps_file") - 1 : 0 ))"
|
|
fi
|
|
jq -n \
|
|
--arg run "$(basename "$run_dir")" \
|
|
--arg status "$status" \
|
|
--arg syncMode "$sync_mode" \
|
|
--argjson wallMs "$wall_ms" \
|
|
--argjson validationMs "$validation_ms" \
|
|
--argjson repoSyncMs "$repo_sync_ms" \
|
|
--argjson maxRssKb "$max_rss_kb" \
|
|
--argjson vrps "$vrps" \
|
|
--argjson vaps "$vaps" \
|
|
--argjson publicationPoints "$publication_points" \
|
|
--argjson warnings "$warnings" \
|
|
'{run:$run,status:$status,syncMode:$syncMode,wallMs:$wallMs,validationMs:$validationMs,repoSyncMs:$repoSyncMs,maxRssKb:$maxRssKb,vrps:$vrps,vaps:$vaps,publicationPoints:$publicationPoints,warnings:$warnings}'
|
|
}
|
|
|
|
wait_for_new_success_run() {
|
|
local before_latest="$1"
|
|
local timeout_secs="$2"
|
|
local start_epoch now run_dir summary meta status meta_status
|
|
start_epoch="$(date +%s)"
|
|
while true; do
|
|
run_dir="$(latest_run_dir || true)"
|
|
if [[ -n "$run_dir" && "$run_dir" != "$before_latest" ]]; then
|
|
summary="$run_dir/run-summary.json"
|
|
meta="$run_dir/run-meta.json"
|
|
if [[ -f "$summary" ]]; then
|
|
status="$(jq -r '.status // "unknown"' "$summary" 2>/dev/null || echo unknown)"
|
|
if [[ "$status" == "success" ]]; then
|
|
meta_status="unknown"
|
|
if [[ -f "$meta" ]]; then
|
|
meta_status="$(jq -r '.status // "unknown"' "$meta" 2>/dev/null || echo unknown)"
|
|
fi
|
|
if [[ "$meta_status" == "success" ]]; then
|
|
print_run_summary "$run_dir" || true
|
|
return 0
|
|
fi
|
|
fi
|
|
if [[ "$status" == "failed" || "$status" == "error" ]]; then
|
|
print_run_summary "$run_dir" || true
|
|
die "run failed: $run_dir"
|
|
fi
|
|
fi
|
|
fi
|
|
now="$(date +%s)"
|
|
if (( now - start_epoch > timeout_secs )); then
|
|
die "timed out waiting for first successful run after ${timeout_secs}s"
|
|
fi
|
|
sleep 10
|
|
done
|
|
}
|
|
|
|
docker_compose_available() {
|
|
docker compose version >/dev/null 2>&1
|
|
}
|
|
|
|
install_docker_if_missing() {
|
|
if command -v docker >/dev/null 2>&1 && docker_compose_available && command -v jq >/dev/null 2>&1 && command -v rsync >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
|
|
log "docker and docker compose are already installed"
|
|
return 0
|
|
fi
|
|
if [[ "${SKIP_DEP_INSTALL:-0}" == "1" ]]; then
|
|
die "docker/docker compose missing and SKIP_DEP_INSTALL=1"
|
|
fi
|
|
if ! command -v apt-get >/dev/null 2>&1; then
|
|
die "docker/docker compose missing; automatic install currently supports apt-get only"
|
|
fi
|
|
log "installing missing runtime packages via apt"
|
|
apt-get update
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl jq rsync gzip tar docker.io
|
|
if ! docker_compose_available; then
|
|
if apt-cache show docker-compose-v2 >/dev/null 2>&1; then
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-v2
|
|
elif apt-cache show docker-compose-plugin >/dev/null 2>&1; then
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin
|
|
elif apt-cache show docker-compose >/dev/null 2>&1; then
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose
|
|
fi
|
|
fi
|
|
systemctl enable --now docker >/dev/null 2>&1 || true
|
|
docker_compose_available || die "docker compose is still unavailable after install"
|
|
}
|
|
|
|
load_installer_images() {
|
|
require_cmd docker
|
|
shopt -s nullglob
|
|
local image
|
|
local found=0
|
|
for image in "$INSTALLER_ROOT"/images/*.tar "$INSTALLER_ROOT"/images/*.tar.gz; do
|
|
found=1
|
|
log "loading docker image: $image"
|
|
if [[ "$image" == *.gz ]]; then
|
|
gzip -dc "$image" | docker load
|
|
else
|
|
docker load -i "$image"
|
|
fi
|
|
done
|
|
shopt -u nullglob
|
|
(( found == 1 )) || warn "no image tar found under $INSTALLER_ROOT/images"
|
|
}
|
|
|
|
ensure_binfmt_if_needed() {
|
|
require_cmd docker
|
|
load_env
|
|
local host_arch
|
|
host_arch="$(uname -m)"
|
|
if [[ "$RPKI_PLATFORM" == "linux/arm64" && "$host_arch" != "aarch64" && "$host_arch" != "arm64" ]]; then
|
|
log "host arch is $host_arch; ensuring binfmt/qemu for arm64"
|
|
docker run --rm --privileged tonistiigi/binfmt --install arm64
|
|
fi
|
|
}
|
|
|
|
verify_runtime_image() {
|
|
load_env
|
|
require_cmd docker
|
|
log "verifying runtime image $RPKI_IMAGE on $RPKI_PLATFORM"
|
|
docker image inspect "$RPKI_IMAGE" >/dev/null
|
|
docker run --rm --platform "$RPKI_PLATFORM" "$RPKI_IMAGE" /opt/ours-rp/bin/rpki --help >/tmp/ours-rp-arm64-rpki-help.txt
|
|
head -5 /tmp/ours-rp-arm64-rpki-help.txt || true
|
|
}
|
|
|
|
verify_image_platform() {
|
|
local image="$1"
|
|
local expected_platform="$2"
|
|
local role="$3"
|
|
local actual_platform
|
|
docker image inspect "$image" >/dev/null
|
|
actual_platform="$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$image" 2>/dev/null || echo unknown)"
|
|
[[ "$actual_platform" == "$expected_platform" ]] || die "$role image platform mismatch: image=$image expected=$expected_platform actual=$actual_platform"
|
|
}
|
|
|
|
verify_monitor_images() {
|
|
load_env
|
|
require_cmd docker
|
|
verify_image_platform "$PROMETHEUS_IMAGE" "$MONITOR_PLATFORM" "prometheus"
|
|
verify_image_platform "$GRAFANA_IMAGE" "$MONITOR_PLATFORM" "grafana"
|
|
}
|
|
|
|
endpoint_ok() {
|
|
local url="$1"
|
|
curl -fsS --max-time 5 "$url" >/dev/null 2>&1
|
|
}
|