Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#########################################################################################
|
||||
#### hetzner-cost-monitor.sh — Track and report Hetzner Cloud spending via the REST ####
|
||||
#### API. Server, volume, snapshot, and load balancer costs with alert thresholds ####
|
||||
#### Requires: bash 4+, curl, jq ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version 1.01 ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./hetzner-cost-monitor.sh --summary ####
|
||||
#### ####
|
||||
#### See --help for all options. ####
|
||||
#########################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors (pre-initialized) ─────────────────────────────────────────
|
||||
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""
|
||||
|
||||
setup_colors() {
|
||||
if [[ "${COLOR:-auto}" == "never" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "${COLOR:-auto}" == "always" ]] || [[ -t 1 ]]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
RESET='\033[0m'
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────
|
||||
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
||||
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
||||
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
section_header() {
|
||||
echo ""
|
||||
echo -e " ${BOLD}${CYAN}── $1 ──${RESET}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
field() {
|
||||
printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
|
||||
}
|
||||
|
||||
field_color() {
|
||||
printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
|
||||
}
|
||||
|
||||
elapsed() {
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
echo "$(( end_time - START_TIME ))s"
|
||||
}
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
RUN_MODE=""
|
||||
OUTPUT_FORMAT="${HCM_FORMAT:-table}"
|
||||
ALERT_THRESHOLD="${HCM_ALERT:-0}"
|
||||
VERBOSE="${VERBOSE:-false}"
|
||||
COLOR="${COLOR:-auto}"
|
||||
|
||||
# ── Credentials ───────────────────────────────────────────────────────
|
||||
HCLOUD_TOKEN="${HCLOUD_TOKEN:-}"
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
readonly SCRIPT_NAME
|
||||
START_TIME=""
|
||||
|
||||
# ── API helpers ──────────────────────────────────────────────────────
|
||||
hcloud_api() {
|
||||
local method="$1" endpoint="$2"
|
||||
shift 2
|
||||
local attempt=0 max_attempts=3
|
||||
|
||||
while (( attempt < max_attempts )); do
|
||||
local http_code
|
||||
http_code=$(curl -s -o /tmp/hcm_resp.json -w "%{http_code}" \
|
||||
-X "$method" \
|
||||
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://api.hetzner.cloud/v1${endpoint}" "$@")
|
||||
|
||||
verbose "API ${method} ${endpoint} → HTTP ${http_code}"
|
||||
|
||||
if [[ "$http_code" == "429" ]]; then
|
||||
((attempt++)) || true
|
||||
local wait=$(( attempt * 5 ))
|
||||
warn "Rate limited — retrying in ${wait}s (attempt ${attempt}/${max_attempts})"
|
||||
sleep "$wait"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$http_code" =~ ^[45] ]]; then
|
||||
local errmsg
|
||||
errmsg=$(jq -r '.error.message // empty' /tmp/hcm_resp.json 2>/dev/null)
|
||||
[[ -n "$errmsg" ]] && verbose "API error: ${errmsg}"
|
||||
fi
|
||||
|
||||
cat /tmp/hcm_resp.json
|
||||
return 0
|
||||
done
|
||||
|
||||
err "API request failed after ${max_attempts} attempts: ${method} ${endpoint}"
|
||||
return 1
|
||||
}
|
||||
|
||||
check_credentials() {
|
||||
[[ -z "$HCLOUD_TOKEN" ]] && die "HCLOUD_TOKEN not set"
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
command -v curl &>/dev/null || die "curl is required"
|
||||
command -v jq &>/dev/null || die "jq is required"
|
||||
}
|
||||
|
||||
# ── Pagination helper ────────────────────────────────────────────────
|
||||
fetch_all() {
|
||||
local endpoint="$1" key="$2"
|
||||
local page=1 per_page=50 all_data="[]"
|
||||
while true; do
|
||||
local sep="?"
|
||||
[[ "$endpoint" == *"?"* ]] && sep="&"
|
||||
local resp
|
||||
resp=$(hcloud_api GET "${endpoint}${sep}page=${page}&per_page=${per_page}")
|
||||
local page_data
|
||||
page_data=$(echo "$resp" | jq ".${key} // []" 2>/dev/null)
|
||||
local page_count
|
||||
page_count=$(echo "$page_data" | jq 'length' 2>/dev/null || echo 0)
|
||||
[[ "$page_count" -eq 0 ]] && break
|
||||
all_data=$(echo -e "${all_data}\n${page_data}" | jq -s 'add' 2>/dev/null)
|
||||
(( page_count < per_page )) && break
|
||||
((page++)) || true
|
||||
done
|
||||
echo "$all_data"
|
||||
}
|
||||
|
||||
# ── Cost calculation helpers ─────────────────────────────────────────
|
||||
get_pricing() {
|
||||
hcloud_api GET "/pricing"
|
||||
}
|
||||
|
||||
calc_server_costs() {
|
||||
local servers="$1" pricing="$2"
|
||||
local total="0"
|
||||
|
||||
echo "$servers" | jq -r '.[] | "\(.server_type.name)\t\(.status)"' 2>/dev/null \
|
||||
| while IFS=$'\t' read -r stype status; do
|
||||
if [[ "$status" == "running" ]]; then
|
||||
local hourly
|
||||
hourly=$(echo "$pricing" | jq -r \
|
||||
--arg t "$stype" \
|
||||
'.pricing.server_types[] | select(.name == $t) | .prices[0].price_hourly.gross' \
|
||||
2>/dev/null)
|
||||
[[ -n "$hourly" && "$hourly" != "null" ]] && echo "$hourly"
|
||||
fi
|
||||
done \
|
||||
| awk '{s+=$1} END {printf "%.2f", s * 730}'
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# SUMMARY
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_summary() {
|
||||
local pricing
|
||||
pricing=$(get_pricing)
|
||||
|
||||
local servers
|
||||
servers=$(fetch_all "/servers" "servers")
|
||||
local server_count
|
||||
server_count=$(echo "$servers" | jq 'length' 2>/dev/null || echo 0)
|
||||
local running_count
|
||||
running_count=$(echo "$servers" | jq '[.[] | select(.status == "running")] | length' 2>/dev/null || echo 0)
|
||||
|
||||
local volumes
|
||||
volumes=$(fetch_all "/volumes" "volumes")
|
||||
local volume_count
|
||||
volume_count=$(echo "$volumes" | jq 'length' 2>/dev/null || echo 0)
|
||||
local volume_gb
|
||||
volume_gb=$(echo "$volumes" | jq '[.[].size] | add // 0' 2>/dev/null || echo 0)
|
||||
|
||||
local images
|
||||
images=$(fetch_all "/images?type=snapshot" "images")
|
||||
local snapshot_count
|
||||
snapshot_count=$(echo "$images" | jq 'length' 2>/dev/null || echo 0)
|
||||
local snapshot_gb
|
||||
snapshot_gb=$(echo "$images" | jq '[.[].image_size // 0] | add // 0' 2>/dev/null || echo 0)
|
||||
|
||||
local lbs
|
||||
lbs=$(fetch_all "/load_balancers" "load_balancers")
|
||||
local lb_count
|
||||
lb_count=$(echo "$lbs" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
local ips
|
||||
ips=$(fetch_all "/floating_ips" "floating_ips")
|
||||
local ip_count
|
||||
ip_count=$(echo "$ips" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
# Monthly cost estimates from pricing API
|
||||
local volume_price_gb
|
||||
volume_price_gb=$(echo "$pricing" | jq -r '.pricing.volume.price_per_gb_per_month.gross // "0"' 2>/dev/null)
|
||||
local volume_cost
|
||||
volume_cost=$(awk "BEGIN {printf \"%.2f\", ${volume_gb} * ${volume_price_gb}}")
|
||||
|
||||
local snapshot_price_gb
|
||||
snapshot_price_gb=$(echo "$pricing" | jq -r '.pricing.image.price_per_gb_month.gross // "0"' 2>/dev/null)
|
||||
local snapshot_cost
|
||||
snapshot_cost=$(awk "BEGIN {printf \"%.2f\", ${snapshot_gb} * ${snapshot_price_gb}}")
|
||||
|
||||
local ip_price
|
||||
ip_price=$(echo "$pricing" | jq -r '.pricing.floating_ip.price_monthly.gross // "0"' 2>/dev/null)
|
||||
local ip_cost
|
||||
ip_cost=$(awk "BEGIN {printf \"%.2f\", ${ip_count} * ${ip_price}}")
|
||||
|
||||
# Server costs — sum hourly × 730 hours/month
|
||||
local server_cost="0.00"
|
||||
while IFS=$'\t' read -r stype status; do
|
||||
[[ -z "$stype" ]] && continue
|
||||
if [[ "$status" == "running" ]]; then
|
||||
local hourly
|
||||
hourly=$(echo "$pricing" | jq -r \
|
||||
--arg t "$stype" \
|
||||
'[.pricing.server_types[] | select(.name == $t) | .prices[0].price_hourly.gross][0] // "0"' \
|
||||
2>/dev/null)
|
||||
server_cost=$(awk "BEGIN {printf \"%.2f\", ${server_cost} + (${hourly} * 730)}")
|
||||
fi
|
||||
done < <(echo "$servers" | jq -r '.[] | "\(.server_type.name)\t\(.status)"' 2>/dev/null)
|
||||
|
||||
# LB costs
|
||||
local lb_cost="0.00"
|
||||
while IFS= read -r lbtype; do
|
||||
[[ -z "$lbtype" ]] && continue
|
||||
local lb_hourly
|
||||
lb_hourly=$(echo "$pricing" | jq -r \
|
||||
--arg t "$lbtype" \
|
||||
'[.pricing.load_balancer_types[] | select(.name == $t) | .prices[0].price_hourly.gross][0] // "0"' \
|
||||
2>/dev/null)
|
||||
lb_cost=$(awk "BEGIN {printf \"%.2f\", ${lb_cost} + (${lb_hourly} * 730)}")
|
||||
done < <(echo "$lbs" | jq -r '.[].load_balancer_type.name // empty' 2>/dev/null)
|
||||
|
||||
local total_cost
|
||||
total_cost=$(awk "BEGIN {printf \"%.2f\", ${server_cost} + ${volume_cost} + ${snapshot_cost} + ${lb_cost} + ${ip_cost}}")
|
||||
|
||||
# Alert check
|
||||
local alert_triggered="false"
|
||||
if [[ "$ALERT_THRESHOLD" != "0" ]]; then
|
||||
local over
|
||||
over=$(awk "BEGIN {print (${total_cost} > ${ALERT_THRESHOLD}) ? 1 : 0}")
|
||||
[[ "$over" == "1" ]] && alert_triggered="true"
|
||||
fi
|
||||
|
||||
case "$OUTPUT_FORMAT" in
|
||||
json)
|
||||
jq -n \
|
||||
--argjson servers "$server_count" \
|
||||
--argjson running "$running_count" \
|
||||
--argjson volumes "$volume_count" \
|
||||
--argjson volume_gb "$volume_gb" \
|
||||
--argjson snapshots "$snapshot_count" \
|
||||
--arg snapshot_gb "$snapshot_gb" \
|
||||
--argjson load_balancers "$lb_count" \
|
||||
--argjson floating_ips "$ip_count" \
|
||||
--arg server_cost "$server_cost" \
|
||||
--arg volume_cost "$volume_cost" \
|
||||
--arg snapshot_cost "$snapshot_cost" \
|
||||
--arg lb_cost "$lb_cost" \
|
||||
--arg ip_cost "$ip_cost" \
|
||||
--arg total_cost "$total_cost" \
|
||||
--arg alert_threshold "$ALERT_THRESHOLD" \
|
||||
--argjson alert_triggered "$alert_triggered" \
|
||||
'{
|
||||
servers: $servers, running: $running,
|
||||
volumes: $volumes, volume_gb: $volume_gb,
|
||||
snapshots: $snapshots, snapshot_gb: ($snapshot_gb | tonumber),
|
||||
load_balancers: $load_balancers, floating_ips: $floating_ips,
|
||||
monthly_estimate: {
|
||||
servers: $server_cost, volumes: $volume_cost,
|
||||
snapshots: $snapshot_cost, load_balancers: $lb_cost,
|
||||
floating_ips: $ip_cost, total: $total_cost
|
||||
},
|
||||
alert: { threshold: $alert_threshold, triggered: $alert_triggered }
|
||||
}'
|
||||
;;
|
||||
prometheus)
|
||||
cat <<EOF
|
||||
# HELP hetzner_cost_monthly_estimate_euros Estimated monthly cost in euros
|
||||
# TYPE hetzner_cost_monthly_estimate_euros gauge
|
||||
hetzner_cost_monthly_estimate_euros ${total_cost}
|
||||
# HELP hetzner_cost_servers_euros Estimated monthly server cost
|
||||
# TYPE hetzner_cost_servers_euros gauge
|
||||
hetzner_cost_servers_euros ${server_cost}
|
||||
# HELP hetzner_cost_volumes_euros Estimated monthly volume cost
|
||||
# TYPE hetzner_cost_volumes_euros gauge
|
||||
hetzner_cost_volumes_euros ${volume_cost}
|
||||
# HELP hetzner_cost_snapshots_euros Estimated monthly snapshot cost
|
||||
# TYPE hetzner_cost_snapshots_euros gauge
|
||||
hetzner_cost_snapshots_euros ${snapshot_cost}
|
||||
# HELP hetzner_cost_load_balancers_euros Estimated monthly load balancer cost
|
||||
# TYPE hetzner_cost_load_balancers_euros gauge
|
||||
hetzner_cost_load_balancers_euros ${lb_cost}
|
||||
# HELP hetzner_cost_floating_ips_euros Estimated monthly floating IP cost
|
||||
# TYPE hetzner_cost_floating_ips_euros gauge
|
||||
hetzner_cost_floating_ips_euros ${ip_cost}
|
||||
# HELP hetzner_cost_servers_total Total servers
|
||||
# TYPE hetzner_cost_servers_total gauge
|
||||
hetzner_cost_servers_total ${server_count}
|
||||
# HELP hetzner_cost_servers_running Running servers
|
||||
# TYPE hetzner_cost_servers_running gauge
|
||||
hetzner_cost_servers_running ${running_count}
|
||||
# HELP hetzner_cost_volumes_total Total volumes
|
||||
# TYPE hetzner_cost_volumes_total gauge
|
||||
hetzner_cost_volumes_total ${volume_count}
|
||||
# HELP hetzner_cost_volume_gb_total Total volume storage in GB
|
||||
# TYPE hetzner_cost_volume_gb_total gauge
|
||||
hetzner_cost_volume_gb_total ${volume_gb}
|
||||
# HELP hetzner_cost_snapshots_total Total snapshots
|
||||
# TYPE hetzner_cost_snapshots_total gauge
|
||||
hetzner_cost_snapshots_total ${snapshot_count}
|
||||
# HELP hetzner_cost_load_balancers_total Total load balancers
|
||||
# TYPE hetzner_cost_load_balancers_total gauge
|
||||
hetzner_cost_load_balancers_total ${lb_count}
|
||||
# HELP hetzner_cost_floating_ips_total Total floating IPs
|
||||
# TYPE hetzner_cost_floating_ips_total gauge
|
||||
hetzner_cost_floating_ips_total ${ip_count}
|
||||
# HELP hetzner_cost_alert_triggered Whether cost alert threshold exceeded
|
||||
# TYPE hetzner_cost_alert_triggered gauge
|
||||
hetzner_cost_alert_triggered $([ "$alert_triggered" == "true" ] && echo 1 || echo 0)
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
section_header "Hetzner Cloud Cost Summary"
|
||||
|
||||
printf " ${BOLD}%-20s %8s %10s %12s${RESET}\n" \
|
||||
"RESOURCE" "COUNT" "SIZE" "MONTHLY €"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..54})"
|
||||
|
||||
printf " %-20s %8s %10s %12s\n" \
|
||||
"Servers (running)" "$running_count" "—" "${server_cost}"
|
||||
printf " %-20s %8s %10s %12s\n" \
|
||||
"Servers (total)" "$server_count" "—" "—"
|
||||
printf " %-20s %8s %10s %12s\n" \
|
||||
"Volumes" "$volume_count" "${volume_gb} GB" "${volume_cost}"
|
||||
printf " %-20s %8s %10s %12s\n" \
|
||||
"Snapshots" "$snapshot_count" "${snapshot_gb} GB" "${snapshot_cost}"
|
||||
printf " %-20s %8s %10s %12s\n" \
|
||||
"Load Balancers" "$lb_count" "—" "${lb_cost}"
|
||||
printf " %-20s %8s %10s %12s\n" \
|
||||
"Floating IPs" "$ip_count" "—" "${ip_cost}"
|
||||
|
||||
printf " %s\n" "$(printf '%.0s─' {1..54})"
|
||||
printf " ${BOLD}%-20s %8s %10s %12s${RESET}\n" \
|
||||
"TOTAL" "" "" "${total_cost}"
|
||||
|
||||
if [[ "$alert_triggered" == "true" ]]; then
|
||||
echo ""
|
||||
echo -e " ${RED}⚠ ALERT: Monthly estimate €${total_cost} exceeds threshold €${ALERT_THRESHOLD}${RESET}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# BREAKDOWN
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_breakdown() {
|
||||
local pricing
|
||||
pricing=$(get_pricing)
|
||||
local servers
|
||||
servers=$(fetch_all "/servers" "servers")
|
||||
local server_count
|
||||
server_count=$(echo "$servers" | jq 'length' 2>/dev/null || echo 0)
|
||||
[[ "$server_count" -eq 0 ]] && die "No servers found"
|
||||
|
||||
case "$OUTPUT_FORMAT" in
|
||||
json)
|
||||
echo "$servers" | jq '[.[] | {
|
||||
id: .id, name: .name, status: .status,
|
||||
type: .server_type.name, location: .datacenter.location.name,
|
||||
ip: .public_net.ipv4.ip
|
||||
}]'
|
||||
;;
|
||||
*)
|
||||
section_header "Server Cost Breakdown"
|
||||
|
||||
printf " ${BOLD}%-10s %-20s %-10s %-8s %-8s %10s${RESET}\n" \
|
||||
"ID" "NAME" "TYPE" "STATUS" "LOC" "MONTHLY €"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..70})"
|
||||
|
||||
while IFS=$'\t' read -r sid sname stype status loc; do
|
||||
[[ -z "$sid" ]] && continue
|
||||
local hourly="0"
|
||||
if [[ "$status" == "running" ]]; then
|
||||
hourly=$(echo "$pricing" | jq -r \
|
||||
--arg t "$stype" \
|
||||
'[.pricing.server_types[] | select(.name == $t) | .prices[0].price_hourly.gross][0] // "0"' \
|
||||
2>/dev/null)
|
||||
fi
|
||||
local monthly
|
||||
monthly=$(awk "BEGIN {printf \"%.2f\", ${hourly} * 730}")
|
||||
|
||||
local status_color="$GREEN"
|
||||
[[ "$status" != "running" ]] && status_color="$DIM"
|
||||
|
||||
printf " %-10s %-20s %-10s " "$sid" "${sname:0:18}" "$stype"
|
||||
echo -ne "${status_color}"
|
||||
printf "%-8s" "$status"
|
||||
echo -ne "${RESET}"
|
||||
printf " %-8s %10s\n" "${loc:0:6}" "$monthly"
|
||||
done < <(echo "$servers" | jq -r \
|
||||
'.[] | "\(.id)\t\(.name // "unknown")\t\(.server_type.name // "—")\t\(.status)\t\(.datacenter.location.name // "—")"' \
|
||||
2>/dev/null)
|
||||
|
||||
echo ""
|
||||
field "Servers:" "$server_count"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# RESOURCES
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
do_resources() {
|
||||
case "$OUTPUT_FORMAT" in
|
||||
json)
|
||||
local volumes snapshots lbs ips
|
||||
volumes=$(fetch_all "/volumes" "volumes")
|
||||
snapshots=$(fetch_all "/images?type=snapshot" "images")
|
||||
lbs=$(fetch_all "/load_balancers" "load_balancers")
|
||||
ips=$(fetch_all "/floating_ips" "floating_ips")
|
||||
jq -n \
|
||||
--argjson volumes "$volumes" \
|
||||
--argjson snapshots "$snapshots" \
|
||||
--argjson load_balancers "$lbs" \
|
||||
--argjson floating_ips "$ips" \
|
||||
'{volumes: $volumes, snapshots: $snapshots, load_balancers: $load_balancers, floating_ips: $floating_ips}'
|
||||
;;
|
||||
*)
|
||||
# Volumes
|
||||
local volumes
|
||||
volumes=$(fetch_all "/volumes" "volumes")
|
||||
local vol_count
|
||||
vol_count=$(echo "$volumes" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
if [[ "$vol_count" -gt 0 ]]; then
|
||||
section_header "Volumes"
|
||||
printf " ${BOLD}%-12s %-20s %-8s %-10s %-8s${RESET}\n" \
|
||||
"ID" "NAME" "SIZE" "LOCATION" "STATUS"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..60})"
|
||||
|
||||
echo "$volumes" | jq -r \
|
||||
'.[] | "\(.id)\t\(.name // "—")\t\(.size)GB\t\(.location.name // "—")\t\(.status // "—")"' \
|
||||
2>/dev/null \
|
||||
| while IFS=$'\t' read -r vid vname vsize vloc vstatus; do
|
||||
printf " %-12s %-20s %-8s %-10s %-8s\n" \
|
||||
"$vid" "${vname:0:18}" "$vsize" "${vloc:0:8}" "$vstatus"
|
||||
done
|
||||
echo ""
|
||||
field "Volumes:" "$vol_count"
|
||||
fi
|
||||
|
||||
# Snapshots
|
||||
local snapshots
|
||||
snapshots=$(fetch_all "/images?type=snapshot" "images")
|
||||
local snap_count
|
||||
snap_count=$(echo "$snapshots" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
if [[ "$snap_count" -gt 0 ]]; then
|
||||
section_header "Snapshots"
|
||||
printf " ${BOLD}%-12s %-22s %-10s %-20s${RESET}\n" \
|
||||
"ID" "DESCRIPTION" "SIZE" "CREATED"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..66})"
|
||||
|
||||
echo "$snapshots" | jq -r \
|
||||
'.[] | "\(.id)\t\(.description // "—")\t\(.image_size // 0)GB\t\(.created // "—")"' \
|
||||
2>/dev/null \
|
||||
| while IFS=$'\t' read -r iid idesc isize icreated; do
|
||||
printf " %-12s %-22s %-10s %-20s\n" \
|
||||
"$iid" "${idesc:0:20}" "$isize" "${icreated:0:19}"
|
||||
done
|
||||
echo ""
|
||||
field "Snapshots:" "$snap_count"
|
||||
fi
|
||||
|
||||
# Floating IPs
|
||||
local ips
|
||||
ips=$(fetch_all "/floating_ips" "floating_ips")
|
||||
local ip_count
|
||||
ip_count=$(echo "$ips" | jq 'length' 2>/dev/null || echo 0)
|
||||
|
||||
if [[ "$ip_count" -gt 0 ]]; then
|
||||
section_header "Floating IPs"
|
||||
printf " ${BOLD}%-12s %-18s %-10s %-12s${RESET}\n" \
|
||||
"ID" "IP" "TYPE" "LOCATION"
|
||||
printf " %s\n" "$(printf '%.0s─' {1..54})"
|
||||
|
||||
echo "$ips" | jq -r \
|
||||
'.[] | "\(.id)\t\(.ip)\t\(.type)\t\(.home_location.name // "—")"' \
|
||||
2>/dev/null \
|
||||
| while IFS=$'\t' read -r fid fip ftype floc; do
|
||||
printf " %-12s %-18s %-10s %-12s\n" \
|
||||
"$fid" "$fip" "$ftype" "$floc"
|
||||
done
|
||||
echo ""
|
||||
field "Floating IPs:" "$ip_count"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# HELP
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
${BOLD}${SCRIPT_NAME}${RESET} — Hetzner Cloud Cost Monitor
|
||||
|
||||
Track and report Hetzner Cloud spending via the REST API.
|
||||
|
||||
${BOLD}MODES${RESET}
|
||||
--summary Cost summary across all resources (default)
|
||||
--breakdown Per-server cost breakdown
|
||||
--resources List all billable resources (volumes, snapshots, IPs)
|
||||
|
||||
${BOLD}OPTIONS${RESET}
|
||||
--format FMT Output: table, json, prometheus (default: table)
|
||||
--alert EUROS Alert if monthly estimate exceeds threshold
|
||||
--verbose Debug output
|
||||
--no-color Disable colored output
|
||||
--help Show this help message
|
||||
|
||||
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
||||
HCLOUD_TOKEN Hetzner Cloud API token (required)
|
||||
HCM_FORMAT Default output format
|
||||
HCM_ALERT Default alert threshold in euros
|
||||
VERBOSE Enable verbose output (true/false)
|
||||
COLOR Color mode: auto, always, never
|
||||
|
||||
${BOLD}EXAMPLES${RESET}
|
||||
# Cost summary
|
||||
${SCRIPT_NAME} --summary
|
||||
|
||||
# Per-server breakdown
|
||||
${SCRIPT_NAME} --breakdown
|
||||
|
||||
# List all billable resources
|
||||
${SCRIPT_NAME} --resources
|
||||
|
||||
# JSON output
|
||||
${SCRIPT_NAME} --summary --format json
|
||||
|
||||
# Alert if monthly cost exceeds €50
|
||||
${SCRIPT_NAME} --summary --alert 50
|
||||
|
||||
# Prometheus metrics
|
||||
${SCRIPT_NAME} --summary --format prometheus
|
||||
|
||||
# Cron — hourly cost metrics
|
||||
0 * * * * /usr/local/bin/hetzner-cost-monitor.sh --summary --format prometheus --no-color > /var/lib/node_exporter/textfile/hetzner_cost.prom 2>/dev/null
|
||||
|
||||
${BOLD}EXIT CODES${RESET}
|
||||
0 Success
|
||||
1 Runtime error
|
||||
EOF
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# PARSE ARGS
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--summary) RUN_MODE="summary"; shift ;;
|
||||
--breakdown) RUN_MODE="breakdown"; shift ;;
|
||||
--resources) RUN_MODE="resources"; shift ;;
|
||||
--format) OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
|
||||
--alert) ALERT_THRESHOLD="${2:?--alert requires a threshold}"; shift 2 ;;
|
||||
--verbose) VERBOSE="true"; shift ;;
|
||||
--no-color) COLOR="never"; shift ;;
|
||||
--help|-h) setup_colors; show_help; exit 0 ;;
|
||||
*) die "Unknown option: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# MAIN
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
main() {
|
||||
parse_args "$@"
|
||||
setup_colors
|
||||
|
||||
if [[ -z "$RUN_MODE" ]]; then
|
||||
RUN_MODE="summary"
|
||||
fi
|
||||
|
||||
check_deps
|
||||
check_credentials
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
case "$RUN_MODE" in
|
||||
summary) do_summary ;;
|
||||
breakdown) do_breakdown ;;
|
||||
resources) do_resources ;;
|
||||
*) die "Unknown mode: ${RUN_MODE}" ;;
|
||||
esac
|
||||
|
||||
if [[ "$OUTPUT_FORMAT" != "prometheus" ]]; then
|
||||
echo ""
|
||||
field "Duration:" "$(elapsed)"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user