#!/bin/sh # ----------------------------------------------------------------------------- # Padmini Systems - Monitoring Stack Installer # Prometheus + Node Exporter + Grafana on Ubuntu 24.04 (IP only, no DNS) # # Usage: # curl -sfL https://mon.padminisys.com/setup | sudo sh - # # Optional overrides (env): # BIND_IP=1.2.3.4 IP to bind Grafana/Prometheus (default: auto-detect) # RETENTION=90d Prometheus history retention (default: 30d) # GRAFANA_PASS=xxxx Grafana admin password (default: generated) # PROM_PASS=xxxx Prometheus admin password (default: generated) # # e.g. curl -sfL https://mon.padminisys.com/setup | sudo RETENTION=90d sh - # # Safe to re-run: upgrades binaries, keeps data and saved passwords. # ----------------------------------------------------------------------------- set -eu PROM_FALLBACK=3.15.0 NODE_FALLBACK=1.12.1 RETENTION="${RETENTION:-30d}" GRAFANA_PORT=3000 PROM_PORT=9090 CRED_FILE=/root/monitoring-credentials.txt DASH_URL="https://raw.githubusercontent.com/rfmoz/grafana-dashboards/master/prometheus/node-exporter-full.json" DASH_FILE=/var/lib/grafana/dashboards/node-exporter-full.json export DEBIAN_FRONTEND=noninteractive # Skip Ubuntu's needrestart apt hook ("Scanning processes..." noise) export NEEDRESTART_SUSPEND=1 info() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; } # stdin is the script itself when piped, so never let a child read it apt_install() { apt-get install -y -q -o Dpkg::Options::=--force-confdef \ -o Dpkg::Options::=--force-confold "$@" /dev/null } gen_pass() { tr -dc 'A-Za-z0-9' /dev/null; do n=$((n + 1)); [ "$n" -ge 60 ] && return 1; sleep 2 done } ip_is_local() { ip -4 -o addr show | awk '{split($4, a, "/"); print a[1]}' | grep -qx "$1" } # --------------------------------------------------------------------------- preflight() { [ "$(id -u)" -eq 0 ] || die "Run as root: curl -sfL https://mon.padminisys.com/setup | sudo sh -" # shellcheck disable=SC1091 . /etc/os-release [ "${ID:-}" = "ubuntu" ] || die "Ubuntu required (found: ${ID:-unknown})" [ "${VERSION_ID:-}" = "24.04" ] || warn "Built for Ubuntu 24.04 (found: ${VERSION_ID:-unknown})" case "$(dpkg --print-architecture)" in amd64) ARCH=amd64 ;; arm64) ARCH=arm64 ;; *) die "Unsupported architecture: $(dpkg --print-architecture)" ;; esac command -v systemctl >/dev/null || die "systemd is required" } base_packages() { info "Installing base packages" apt-get update -q /dev/null apt_install ca-certificates curl gnupg apache2-utils iproute2 } detect_ip() { PUBLIC_IP=$(curl -4 -fsS --max-time 5 https://api.ipify.org 2>/dev/null \ || curl -4 -fsS --max-time 5 https://ifconfig.me 2>/dev/null || true) if [ -z "${BIND_IP:-}" ]; then if [ -n "$PUBLIC_IP" ] && ip_is_local "$PUBLIC_IP"; then BIND_IP=$PUBLIC_IP else # Public IP is NAT/floating (not on any NIC): bind to primary NIC IP BIND_IP=$(ip -4 route get 1.1.1.1 2>/dev/null \ | awk '{for (i = 1; i <= NF; i++) if ($i == "src") { print $(i + 1); exit }}') fi fi [ -n "$BIND_IP" ] || die "Could not detect an IP. Re-run with BIND_IP=" ip_is_local "$BIND_IP" || die "BIND_IP $BIND_IP is not assigned on this VM" ACCESS_IP=${PUBLIC_IP:-$BIND_IP} info "Binding services to $BIND_IP (access via $ACCESS_IP)" } load_creds() { if [ -f "$CRED_FILE" ]; then : "${GRAFANA_PASS:=$(sed -n 's/^GRAFANA_PASS=//p' "$CRED_FILE")}" : "${PROM_PASS:=$(sed -n 's/^PROM_PASS=//p' "$CRED_FILE")}" fi [ -n "${GRAFANA_PASS:-}" ] || GRAFANA_PASS=$(gen_pass) [ -n "${PROM_PASS:-}" ] || PROM_PASS=$(gen_pass) (umask 077; cat > "$CRED_FILE" </dev/null | sed -n 's#.*/tag/v##p') || true echo "${v:-$2}" } fetch_release() { # $1 = name, $2 = repo, $3 = version -> sets SRC_DIR file="$1-$3.linux-$ARCH.tar.gz" url="https://github.com/$2/releases/download/v$3" info "Downloading $1 v$3" curl -fsSL -o "$TMP/$file" "$url/$file" || die "Download failed: $url/$file" curl -fsSL -o "$TMP/sha256sums.txt" "$url/sha256sums.txt" || die "Download failed: sha256sums.txt" (cd "$TMP" && awk -v f="$file" '$2 == f' sha256sums.txt | sha256sum -c --status -) \ || die "Checksum mismatch: $file" tar -xzf "$TMP/$file" -C "$TMP" SRC_DIR="$TMP/$1-$3.linux-$ARCH" } add_user() { id "$1" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$1" } # --------------------------------------------------------------------------- install_node_exporter() { NODE_VER=$(latest_version prometheus/node_exporter "$NODE_FALLBACK") fetch_release node_exporter prometheus/node_exporter "$NODE_VER" add_user node_exporter systemctl stop node_exporter 2>/dev/null || true install -m 0755 "$SRC_DIR/node_exporter" /usr/local/bin/node_exporter # Local only - Prometheus scrapes it, no need to expose cat > /etc/systemd/system/node_exporter.service <<'EOF' [Unit] Description=Prometheus Node Exporter Wants=network-online.target After=network-online.target [Service] User=node_exporter Group=node_exporter ExecStart=/usr/local/bin/node_exporter --web.listen-address=127.0.0.1:9100 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now node_exporter >/dev/null 2>&1 wait_http http://127.0.0.1:9100/metrics || die "Node Exporter not up (journalctl -u node_exporter)" info "Node Exporter running on 127.0.0.1:9100" } install_prometheus() { PROM_VER=$(latest_version prometheus/prometheus "$PROM_FALLBACK") fetch_release prometheus prometheus/prometheus "$PROM_VER" add_user prometheus systemctl stop prometheus 2>/dev/null || true install -m 0755 "$SRC_DIR/prometheus" "$SRC_DIR/promtool" /usr/local/bin/ install -d -o prometheus -g prometheus -m 0750 /var/lib/prometheus install -d -m 0755 /etc/prometheus cat > /etc/prometheus/prometheus.yml < /etc/prometheus/web.yml chown root:prometheus /etc/prometheus/prometheus.yml /etc/prometheus/web.yml chmod 0640 /etc/prometheus/prometheus.yml /etc/prometheus/web.yml out=$(promtool check config /etc/prometheus/prometheus.yml 2>&1) || die "Invalid prometheus.yml: $out" out=$(promtool check web-config /etc/prometheus/web.yml 2>&1) || die "Invalid web.yml: $out" cat > /etc/systemd/system/prometheus.service </dev/null 2>&1 wait_http -u "admin:$PROM_PASS" "http://$BIND_IP:$PROM_PORT/-/ready" \ || die "Prometheus not ready (journalctl -u prometheus)" info "Prometheus running on $BIND_IP:$PROM_PORT" } install_grafana() { info "Installing Grafana" install -d -m 0755 /etc/apt/keyrings curl -fsSL https://apt.grafana.com/gpg.key | gpg --batch --yes --dearmor -o /etc/apt/keyrings/grafana.gpg echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" \ > /etc/apt/sources.list.d/grafana.list apt-get update -q /dev/null apt_install grafana # Datasource: Prometheus (with basic auth) cat > /etc/grafana/provisioning/datasources/prometheus.yml < /etc/grafana/provisioning/dashboards/padmini.yml <<'EOF' apiVersion: 1 providers: - name: padmini type: file allowUiUpdates: true options: path: /var/lib/grafana/dashboards EOF chown root:grafana /etc/grafana/provisioning/datasources/prometheus.yml \ /etc/grafana/provisioning/dashboards/padmini.yml chmod 0640 /etc/grafana/provisioning/datasources/prometheus.yml \ /etc/grafana/provisioning/dashboards/padmini.yml install -d -o grafana -g grafana /var/lib/grafana/dashboards HOME_DASH="" if curl -fsSL --max-time 30 -o "$TMP/node.json" "$DASH_URL"; then # shellcheck disable=SC2016 sed 's/\${DS_PROMETHEUS}/prometheus/g' "$TMP/node.json" > "$DASH_FILE" chown grafana:grafana "$DASH_FILE" HOME_DASH="Environment=GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=$DASH_FILE" elif [ -f "$DASH_FILE" ]; then HOME_DASH="Environment=GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=$DASH_FILE" else warn "Dashboard download failed - import ID 1860 in Grafana manually" fi # Bind Grafana to the selected IP (systemd drop-in, upgrade safe) install -d -m 0755 /etc/systemd/system/grafana-server.service.d cat > /etc/systemd/system/grafana-server.service.d/padmini.conf </dev/null || true out=$(cd /usr/share/grafana && runuser -u grafana -- /usr/sbin/grafana cli \ admin reset-admin-password "$GRAFANA_PASS" &1) \ || die "Could not set Grafana admin password: $out" systemctl enable grafana-server >/dev/null 2>&1 systemctl restart grafana-server wait_http "http://$BIND_IP:$GRAFANA_PORT/api/health" \ || die "Grafana not ready (journalctl -u grafana-server)" # One attempt only: repeated bad logins trigger Grafana's brute-force lockout curl -fsS -o /dev/null --max-time 5 -u "admin:$GRAFANA_PASS" \ "http://$BIND_IP:$GRAFANA_PORT/api/user" 2>/dev/null \ || die "Grafana rejected the new admin password (journalctl -u grafana-server)" info "Grafana running on $BIND_IP:$GRAFANA_PORT" } open_firewall() { if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow "$GRAFANA_PORT/tcp" >/dev/null ufw allow "$PROM_PORT/tcp" >/dev/null info "UFW: opened $GRAFANA_PORT/tcp and $PROM_PORT/tcp" fi } summary() { cat <