#!/usr/bin/env bash # Small VPS status dashboard for my OCI A1 box. # Needs curl, vnstat and python3. GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # --- config --- # Leave empty if you don't want the media/storage section. MEDIA_DIR="/home/ubuntu/media" # Usually auto-detects fine. Set it manually if needed. INTERFACE="$(ip route show default 2>/dev/null | awk '{print $5; exit}')" INTERFACE="${INTERFACE:-enp0s6}" # Change these if different limits apply to your tenancy. OUTBOUND_LIMIT_BYTES=10000000000000 A1_OCPU_HOUR_LIMIT=3000 A1_RAM_GB_HOUR_LIMIT=18000 # These are manual values; the script doesn't query OCI billing for them. OCI_BLOCK_STORAGE_LIMIT_GB=200 OCI_BOOT_VOLUME_GB=200 OCI_BOOT_VPU_PER_GB=10 # "Name|container|local URL|public URL" # Example: # APPS=( # "Vaultwarden|vaultwarden|http://127.0.0.1:8080|https://vault.example.com" # "Immich|immich_server|http://127.0.0.1:2283|https://photos.example.com" # ) APPS=() SERVICES=(docker caddy vnstat) # --- helpers --- http_ok() { [[ "$1" =~ ^[23][0-9][0-9]$ || "$1" == "401" || "$1" == "403" ]] } human_service_name() { case "$1" in docker) echo "Docker" ;; caddy) echo "Caddy" ;; vnstat) echo "vnStat" ;; *) echo "$1" ;; esac } ALL_APPS_OK=true ALL_SERVICES_OK=true # --- status --- echo echo -e "${BOLD}${CYAN}==========================================" echo " ORACLE VPS STATUS" echo -e "==========================================${NC}" echo echo -e "${BOLD}SYSTEM${NC}" echo "Hostname : $(hostname)" echo "Date : $(date)" echo "Uptime : $(uptime -p)" echo "Load : $(uptime | awk -F'load average:' '{print $2}')" echo echo -e "${BOLD}SERVICES${NC}" for SERVICE in "${SERVICES[@]}"; do NAME="$(human_service_name "$SERVICE")" printf "%-9s: " "$NAME" if systemctl is-active --quiet "$SERVICE"; then echo -e "${GREEN}ACTIVE${NC}" else echo -e "${RED}NOT ACTIVE${NC}" ALL_SERVICES_OK=false fi done echo echo -e "${BOLD}APPLICATIONS${NC}" if [ "${#APPS[@]}" -eq 0 ]; then echo -e "${YELLOW}No applications configured. Edit APPS near the top.${NC}" else for APP in "${APPS[@]}"; do IFS='|' read -r NAME CONTAINER LOCAL_URL PUBLIC_URL <<< "$APP" echo echo -e "${BOLD}$NAME${NC}" if docker inspect "$CONTAINER" >/dev/null 2>&1; then STATUS="$(docker inspect "$CONTAINER" --format '{{.State.Status}}')" HEALTH="$(docker inspect "$CONTAINER" \ --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}')" if [ "$STATUS" = "running" ] && { [ "$HEALTH" = "healthy" ] || [ "$HEALTH" = "none" ]; }; then echo -e " Container : ${GREEN}${STATUS} / ${HEALTH}${NC}" else echo -e " Container : ${RED}${STATUS} / ${HEALTH}${NC}" ALL_APPS_OK=false fi else echo -e " Container : ${RED}NOT FOUND${NC}" ALL_APPS_OK=false fi if [ -n "$LOCAL_URL" ]; then LOCAL_HTTP="$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 5 "$LOCAL_URL" 2>/dev/null || true)" if http_ok "$LOCAL_HTTP"; then echo -e " Local : ${GREEN}HTTP ${LOCAL_HTTP}${NC}" else echo -e " Local : ${RED}HTTP ${LOCAL_HTTP:-000}${NC}" ALL_APPS_OK=false fi fi if [ -n "$PUBLIC_URL" ]; then PUBLIC_HTTP="$(curl -s -o /dev/null -w "%{http_code}" \ --max-time 10 "$PUBLIC_URL" 2>/dev/null || true)" if http_ok "$PUBLIC_HTTP"; then echo -e " Public : ${GREEN}HTTPS ${PUBLIC_HTTP}${NC}" else echo -e " Public : ${RED}HTTPS ${PUBLIC_HTTP:-000}${NC}" ALL_APPS_OK=false fi fi done fi echo echo -e "${BOLD}MEMORY${NC}" free -h | awk ' NR==1 { printf "%-10s %-10s %-10s %-10s\n", "Total", "Used", "Free", "Available" } NR==2 { printf "%-10s %-10s %-10s %-10s\n", $2, $3, $4, $7 } ' echo echo -e "${BOLD}ROOT DISK${NC}" df -h / | awk ' NR==1 { printf "%-10s %-10s %-10s %-10s\n", "Size", "Used", "Available", "Use%" } NR==2 { printf "%-10s %-10s %-10s %-10s\n", $2, $3, $4, $5 } ' if [ -n "$MEDIA_DIR" ]; then echo echo -e "${BOLD}STORAGE DIRECTORY${NC}" if [ -d "$MEDIA_DIR" ]; then du -sh "$MEDIA_DIR"/* 2>/dev/null | sort -h echo "------------------------------------------" echo -n "TOTAL: " du -sh "$MEDIA_DIR" 2>/dev/null | awk '{print $1}' else echo -e "${YELLOW}Directory not found: $MEDIA_DIR${NC}" fi fi echo echo -e "${BOLD}DOCKER STORAGE${NC}" docker system df 2>/dev/null || echo "Docker storage data unavailable." echo echo -e "${BOLD}OCI COST GUARD${NC}" if command -v python3 >/dev/null 2>&1; then python3 - "$A1_OCPU_HOUR_LIMIT" "$A1_RAM_GB_HOUR_LIMIT" <<'PY' import json import sys import urllib.request from datetime import datetime, timezone OCPU_LIMIT = float(sys.argv[1]) RAM_LIMIT = float(sys.argv[2]) def bar(percent, width=30): filled = min(width, max(0, round(percent / 100 * width))) return "[" + "#" * filled + "-" * (width - filled) + "]" def status(percent): if percent > 100: return "OVER LIMIT" if percent >= 90: return "WATCH CLOSELY" if percent >= 75: return "WATCH" return "SAFE" try: req = urllib.request.Request( "http://169.254.169.254/opc/v2/instance/", headers={"Authorization": "Bearer Oracle"}, ) with urllib.request.urlopen(req, timeout=3) as response: meta = json.load(response) shape = meta.get("shape", "unknown") config = meta.get("shapeConfig", {}) ocpus = float(config.get("ocpus", 0)) memory = float(config.get("memoryInGBs", 0)) network = config.get("networkingBandwidthInGbps", "unknown") now = datetime.now(timezone.utc) month_start = datetime(now.year, now.month, 1, tzinfo=timezone.utc) if now.month == 12: next_month = datetime(now.year + 1, 1, 1, tzinfo=timezone.utc) else: next_month = datetime(now.year, now.month + 1, 1, tzinfo=timezone.utc) start = month_start created_raw = meta.get("timeCreated") if created_raw: try: created = datetime.fromisoformat(created_raw.replace("Z", "+00:00")) if created > start: start = created except Exception: pass elapsed_hours = max(0, (now - start).total_seconds() / 3600) month_hours = (next_month - month_start).total_seconds() / 3600 ocpu_used = ocpus * elapsed_hours ram_used = memory * elapsed_hours ocpu_projected = ocpus * month_hours ram_projected = memory * month_hours ocpu_used_pct = ocpu_used / OCPU_LIMIT * 100 ram_used_pct = ram_used / RAM_LIMIT * 100 ocpu_proj_pct = ocpu_projected / OCPU_LIMIT * 100 ram_proj_pct = ram_projected / RAM_LIMIT * 100 print(f"Shape : {shape}") print(f"Provisioned : {ocpus:g} OCPU / {memory:g} GB RAM") print(f"Network capacity : {network} Gbps") print() print("Provisioned-hours this month:") print( f"OCPU-hours : {ocpu_used:.1f} / {OCPU_LIMIT:.0f} " f"({ocpu_used_pct:.1f}%)" ) print(bar(ocpu_used_pct)) print( f"RAM GB-hours : {ram_used:.1f} / {RAM_LIMIT:.0f} " f"({ram_used_pct:.1f}%)" ) print(bar(ram_used_pct)) print() print("Projected month-end if running 24/7:") print( f"OCPU-hours : {ocpu_projected:.0f} / {OCPU_LIMIT:.0f} " f"({ocpu_proj_pct:.1f}%) - {status(ocpu_proj_pct)}" ) print( f"RAM GB-hours : {ram_projected:.0f} / {RAM_LIMIT:.0f} " f"({ram_proj_pct:.1f}%) - {status(ram_proj_pct)}" ) print() print("Estimate is based on provisioned OCPU/RAM and running time, not CPU load.") except Exception as exc: print(f"OCI metadata unavailable: {exc}") PY else echo -e "${YELLOW}python3 not installed; OCI metadata section skipped.${NC}" fi echo echo "Boot/block allocation : ${OCI_BOOT_VOLUME_GB} / ${OCI_BLOCK_STORAGE_LIMIT_GB} GB" if [ "$OCI_BOOT_VOLUME_GB" -ge "$OCI_BLOCK_STORAGE_LIMIT_GB" ]; then echo -e "${YELLOW}${BOLD}WARNING: configured storage reference is fully allocated${NC}" else echo -e "${GREEN}${BOLD}OK: storage allocation below configured reference${NC}" fi echo "Boot performance : ${OCI_BOOT_VPU_PER_GB} VPU/GB" if [ "$OCI_BOOT_VPU_PER_GB" -gt 10 ]; then echo -e "${YELLOW}${BOLD}WARNING: above 10 VPU/GB - check OCI pricing${NC}" else echo -e "${GREEN}${BOLD}OK: 10 VPU/GB or lower${NC}" fi echo echo -e "${YELLOW}Rough local estimate only; check OCI for actual usage/costs.${NC}" echo echo -e "${BOLD}MONTHLY OUTBOUND ($INTERFACE)${NC}" if command -v vnstat >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then BANDWIDTH_DATA="$( vnstat --json m -i "$INTERFACE" 2>/dev/null | python3 -c ' import json import sys LIMIT = int(sys.argv[1]) try: data = json.load(sys.stdin) months = data["interfaces"][0]["traffic"]["month"] latest = max( months, key=lambda m: (m["date"]["year"], m["date"]["month"]) ) tx = int(latest["tx"]) def human(n): units = ["B", "KB", "MB", "GB", "TB"] value = float(n) for unit in units: if value < 1000 or unit == "TB": return f"{value:.2f}{unit}" value /= 1000 percent = (tx / LIMIT) * 100 if LIMIT else 0 remaining = max(0, LIMIT - tx) width = 30 filled = min(width, max(0, round((percent / 100) * width))) bar = "[" + "#" * filled + "-" * (width - filled) + "]" print(tx, human(tx), f"{percent:.4f}", human(remaining), bar) except Exception: print("0 UNKNOWN 0.0000 UNKNOWN [------------------------------]") ' "$OUTBOUND_LIMIT_BYTES" )" read -r TX_BYTES TX_HUMAN TX_PERCENT REMAINING_HUMAN BANDWIDTH_BAR \ <<< "$BANDWIDTH_DATA" echo "Used TX : $TX_HUMAN" echo "Reference limit : 10.00TB" echo "Estimated left : $REMAINING_HUMAN" echo "Usage : $TX_PERCENT%" echo echo "$BANDWIDTH_BAR" echo if [ "$TX_BYTES" -ge 8000000000000 ]; then echo -e "${RED}${BOLD}DANGER - ABOVE 8 TB${NC}" elif [ "$TX_BYTES" -ge 7000000000000 ]; then echo -e "${RED}${BOLD}HIGH USAGE - ABOVE 7 TB${NC}" elif [ "$TX_BYTES" -ge 5000000000000 ]; then echo -e "${YELLOW}${BOLD}WATCH - ABOVE 5 TB${NC}" else echo -e "${GREEN}${BOLD}OK${NC}" fi echo echo -e "${YELLOW}vnStat is local and only counts traffic recorded since it was installed.${NC}" else echo -e "${YELLOW}vnStat and/or python3 unavailable; bandwidth section skipped.${NC}" fi echo echo -e "${BOLD}VNSTAT MONTHLY DETAILS${NC}" if command -v vnstat >/dev/null 2>&1; then vnstat -m -i "$INTERFACE" 2>/dev/null || true else echo "vnStat not installed." fi if [ "${#APPS[@]}" -gt 0 ]; then echo echo -e "${BOLD}DOCKER LOG CONFIG${NC}" for APP in "${APPS[@]}"; do IFS='|' read -r _NAME CONTAINER _LOCAL_URL _PUBLIC_URL <<< "$APP" echo -n "$CONTAINER : " docker inspect "$CONTAINER" \ --format '{{json .HostConfig.LogConfig}}' \ 2>/dev/null || echo "not found" done fi echo echo -e "${BOLD}${CYAN}==========================================${NC}" if $ALL_APPS_OK && $ALL_SERVICES_OK; then echo -e "${GREEN}${BOLD}OK: VPS AND CONFIGURED APPLICATIONS LOOK HEALTHY${NC}" else echo -e "${YELLOW}${BOLD}WARNING: SOMETHING NEEDS ATTENTION${NC}" fi echo -e "${BOLD}${CYAN}==========================================${NC}" echo