bin/ 230 CLI tools (ask-*, br-*, agent-*, roadid, carpool) scripts/ 99 automation scripts fleet/ Node configs and deployment workers/ Cloudflare Worker sources (roadpay, road-search, squad webhooks) roadc/ RoadC programming language roadnet/ Mesh network (5 APs, WireGuard) operator/ Memory system scripts config/ System configs dotfiles/ Shell configs docs/ Documentation BlackRoad OS — Pave Tomorrow. RoadChain-SHA2048: d1a24f55318d338b RoadChain-Identity: alexa@sovereign RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
460 lines
13 KiB
Bash
Executable File
460 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# BlackRoad Cost Tracker
|
|
# Track resource usage and costs across the cluster
|
|
set -eo pipefail
|
|
|
|
# Source centralized config
|
|
NODES_CONFIG="$HOME/.blackroad/config/nodes.sh"
|
|
if [[ -f "$NODES_CONFIG" ]]; then
|
|
source "$NODES_CONFIG"
|
|
else
|
|
PINK='\033[38;5;205m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'
|
|
YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; RESET='\033[0m'
|
|
fi
|
|
|
|
# Dependency check
|
|
for dep in sqlite3 bc; do
|
|
command -v "$dep" &>/dev/null || { echo "ERROR: $dep required" >&2; exit 1; }
|
|
done
|
|
|
|
# SQL-safe string escaping
|
|
_sql_escape() { echo "$1" | sed "s/'/''/g"; }
|
|
|
|
COST_DIR="$HOME/.blackroad/costs"
|
|
COST_DB="$COST_DIR/costs.db"
|
|
|
|
# Default rates (can be customized)
|
|
RATE_CPU_HOUR=0.001 # $ per CPU-hour
|
|
RATE_MEM_GB_HOUR=0.0005 # $ per GB-hour
|
|
RATE_GPU_HOUR=0.01 # $ per GPU-hour (Hailo)
|
|
RATE_INFERENCE=0.0001 # $ per inference request
|
|
RATE_TOKEN=0.000001 # $ per token
|
|
|
|
# Initialize
|
|
init() {
|
|
mkdir -p "$COST_DIR"/{reports,budgets}
|
|
|
|
sqlite3 "$COST_DB" << 'SQL'
|
|
CREATE TABLE IF NOT EXISTS usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
node TEXT,
|
|
project TEXT DEFAULT 'default',
|
|
resource_type TEXT,
|
|
quantity REAL,
|
|
unit TEXT,
|
|
cost REAL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS rates (
|
|
resource_type TEXT PRIMARY KEY,
|
|
rate REAL,
|
|
unit TEXT,
|
|
description TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS budgets (
|
|
project TEXT PRIMARY KEY,
|
|
monthly_limit REAL,
|
|
alert_threshold REAL DEFAULT 0.8,
|
|
current_spend REAL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS invoices (
|
|
id TEXT PRIMARY KEY,
|
|
project TEXT,
|
|
period_start DATE,
|
|
period_end DATE,
|
|
total REAL,
|
|
status TEXT DEFAULT 'pending',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_project ON usage(project);
|
|
CREATE INDEX IF NOT EXISTS idx_timestamp ON usage(timestamp);
|
|
SQL
|
|
|
|
# Seed default rates
|
|
seed_rates
|
|
|
|
echo -e "${GREEN}Cost tracker initialized${RESET}"
|
|
}
|
|
|
|
# Seed default rates
|
|
seed_rates() {
|
|
sqlite3 "$COST_DB" << SQL
|
|
INSERT OR IGNORE INTO rates (resource_type, rate, unit, description) VALUES
|
|
('cpu', $RATE_CPU_HOUR, 'cpu-hour', 'CPU compute time'),
|
|
('memory', $RATE_MEM_GB_HOUR, 'gb-hour', 'Memory usage'),
|
|
('gpu', $RATE_GPU_HOUR, 'gpu-hour', 'Hailo accelerator time'),
|
|
('inference', $RATE_INFERENCE, 'request', 'LLM inference request'),
|
|
('tokens', $RATE_TOKEN, 'token', 'Input/output tokens'),
|
|
('storage', 0.00001, 'gb-hour', 'Disk storage'),
|
|
('network', 0.00001, 'gb', 'Network transfer');
|
|
SQL
|
|
}
|
|
|
|
# Record usage
|
|
record() {
|
|
local resource="$1"
|
|
local quantity="$2"
|
|
local project="${3:-default}"
|
|
local node="${4:-$(hostname)}"
|
|
|
|
local rate=$(sqlite3 "$COST_DB" "SELECT rate FROM rates WHERE resource_type = '$resource'")
|
|
local cost=$(echo "scale=6; $quantity * $rate" | bc)
|
|
|
|
sqlite3 "$COST_DB" "
|
|
INSERT INTO usage (node, project, resource_type, quantity, unit, cost)
|
|
VALUES ('$node', '$project', '$resource', $quantity, (SELECT unit FROM rates WHERE resource_type = '$resource'), $cost)
|
|
"
|
|
|
|
# Update budget
|
|
sqlite3 "$COST_DB" "
|
|
UPDATE budgets SET current_spend = current_spend + $cost WHERE project = '$project'
|
|
"
|
|
|
|
echo -e "${GREEN}Recorded: $quantity $resource = \$$cost${RESET}"
|
|
}
|
|
|
|
# Record inference usage
|
|
record_inference() {
|
|
local project="${1:-default}"
|
|
local tokens_in="${2:-0}"
|
|
local tokens_out="${3:-0}"
|
|
local node="${4:-$(hostname)}"
|
|
|
|
record "inference" 1 "$project" "$node"
|
|
record "tokens" "$((tokens_in + tokens_out))" "$project" "$node"
|
|
}
|
|
|
|
# Set rate
|
|
set_rate() {
|
|
local resource="$1"
|
|
local rate="$2"
|
|
local unit="${3:-unit}"
|
|
|
|
sqlite3 "$COST_DB" "
|
|
INSERT OR REPLACE INTO rates (resource_type, rate, unit)
|
|
VALUES ('$resource', $rate, '$unit')
|
|
"
|
|
|
|
echo -e "${GREEN}Rate set: $resource = \$$rate per $unit${RESET}"
|
|
}
|
|
|
|
# List rates
|
|
rates() {
|
|
echo -e "${PINK}=== RESOURCE RATES ===${RESET}"
|
|
echo
|
|
|
|
sqlite3 "$COST_DB" "SELECT resource_type, rate, unit, description FROM rates ORDER BY resource_type" | \
|
|
while IFS='|' read -r resource rate unit desc; do
|
|
printf " %-15s \$%-10.6f per %-10s %s\n" "$resource" "$rate" "$unit" "$desc"
|
|
done
|
|
}
|
|
|
|
# Create budget
|
|
budget_create() {
|
|
local project="$1"
|
|
local limit="$2"
|
|
local threshold="${3:-0.8}"
|
|
|
|
sqlite3 "$COST_DB" "
|
|
INSERT OR REPLACE INTO budgets (project, monthly_limit, alert_threshold, current_spend)
|
|
VALUES ('$project', $limit, $threshold, 0)
|
|
"
|
|
|
|
echo -e "${GREEN}Budget created: $project = \$$limit/month${RESET}"
|
|
}
|
|
|
|
# Check budgets
|
|
budget_check() {
|
|
echo -e "${PINK}=== BUDGET STATUS ===${RESET}"
|
|
echo
|
|
|
|
sqlite3 "$COST_DB" "SELECT project, monthly_limit, current_spend, alert_threshold FROM budgets" | \
|
|
while IFS='|' read -r project limit spend threshold; do
|
|
local pct=$(echo "scale=1; $spend * 100 / $limit" | bc 2>/dev/null || echo 0)
|
|
local threshold_pct=$(echo "scale=0; $threshold * 100" | bc)
|
|
|
|
local color=$GREEN
|
|
local alert_val=$(echo "$spend / $limit" | bc -l)
|
|
if [ "$(echo "$alert_val > $threshold" | bc -l)" = "1" ]; then
|
|
color=$YELLOW
|
|
fi
|
|
if [ "$(echo "$alert_val > 1" | bc -l)" = "1" ]; then
|
|
color=$RED
|
|
fi
|
|
|
|
printf " %-15s ${color}\$%.2f / \$%.2f (%.1f%%)${RESET}\n" "$project" "$spend" "$limit" "$pct"
|
|
done
|
|
}
|
|
|
|
# Current period costs
|
|
current() {
|
|
local project="${1:-all}"
|
|
local period="${2:-month}"
|
|
|
|
echo -e "${PINK}=== CURRENT $period COSTS ===${RESET}"
|
|
echo
|
|
|
|
local where=""
|
|
[ "$project" != "all" ] && where="AND project = '$project'"
|
|
|
|
local period_filter
|
|
case "$period" in
|
|
day) period_filter="date(timestamp) = date('now')" ;;
|
|
week) period_filter="datetime(timestamp, '+7 days') > datetime('now')" ;;
|
|
month) period_filter="datetime(timestamp, '+1 month') > datetime('now')" ;;
|
|
esac
|
|
|
|
echo "By resource:"
|
|
sqlite3 "$COST_DB" "
|
|
SELECT resource_type, SUM(quantity), unit, SUM(cost)
|
|
FROM usage
|
|
WHERE $period_filter $where
|
|
GROUP BY resource_type
|
|
ORDER BY SUM(cost) DESC
|
|
" | while IFS='|' read -r resource qty unit cost; do
|
|
printf " %-15s %10.2f %-10s \$%.4f\n" "$resource" "$qty" "$unit" "$cost"
|
|
done
|
|
|
|
echo
|
|
echo "By project:"
|
|
sqlite3 "$COST_DB" "
|
|
SELECT project, SUM(cost)
|
|
FROM usage
|
|
WHERE $period_filter $where
|
|
GROUP BY project
|
|
ORDER BY SUM(cost) DESC
|
|
" | while IFS='|' read -r proj cost; do
|
|
printf " %-15s \$%.4f\n" "$proj" "$cost"
|
|
done
|
|
|
|
echo
|
|
echo "By node:"
|
|
sqlite3 "$COST_DB" "
|
|
SELECT node, SUM(cost)
|
|
FROM usage
|
|
WHERE $period_filter $where
|
|
GROUP BY node
|
|
ORDER BY SUM(cost) DESC
|
|
" | while IFS='|' read -r node cost; do
|
|
printf " %-15s \$%.4f\n" "$node" "$cost"
|
|
done
|
|
|
|
echo
|
|
local total=$(sqlite3 "$COST_DB" "SELECT SUM(cost) FROM usage WHERE $period_filter $where")
|
|
echo -e "Total: ${GREEN}\$${total:-0}${RESET}"
|
|
}
|
|
|
|
# Generate invoice
|
|
invoice() {
|
|
local project="$1"
|
|
local start_date="${2:-$(date -d 'first day of this month' +%Y-%m-%d 2>/dev/null || date -v1d +%Y-%m-%d)}"
|
|
local end_date="${3:-$(date +%Y-%m-%d)}"
|
|
|
|
local invoice_id="inv_$(date +%Y%m)_${project}"
|
|
|
|
echo -e "${PINK}=== INVOICE: $invoice_id ===${RESET}"
|
|
echo
|
|
echo "Project: $project"
|
|
echo "Period: $start_date to $end_date"
|
|
echo
|
|
|
|
echo "─────────────────────────────────────────────────────────────────"
|
|
printf "%-20s %15s %12s %12s\n" "Resource" "Quantity" "Rate" "Cost"
|
|
echo "─────────────────────────────────────────────────────────────────"
|
|
|
|
local total=0
|
|
sqlite3 "$COST_DB" "
|
|
SELECT u.resource_type, SUM(u.quantity), u.unit, r.rate, SUM(u.cost)
|
|
FROM usage u
|
|
JOIN rates r ON u.resource_type = r.resource_type
|
|
WHERE u.project = '$project'
|
|
AND date(u.timestamp) BETWEEN '$start_date' AND '$end_date'
|
|
GROUP BY u.resource_type
|
|
" | while IFS='|' read -r resource qty unit rate cost; do
|
|
printf "%-20s %12.2f %-3s \$%-8.6f \$%.4f\n" "$resource" "$qty" "$unit" "$rate" "$cost"
|
|
total=$(echo "$total + $cost" | bc)
|
|
done
|
|
|
|
echo "─────────────────────────────────────────────────────────────────"
|
|
|
|
total=$(sqlite3 "$COST_DB" "
|
|
SELECT SUM(cost) FROM usage
|
|
WHERE project = '$project'
|
|
AND date(timestamp) BETWEEN '$start_date' AND '$end_date'
|
|
")
|
|
|
|
printf "%48s \$%.4f\n" "TOTAL:" "$total"
|
|
echo
|
|
|
|
# Save invoice
|
|
sqlite3 "$COST_DB" "
|
|
INSERT OR REPLACE INTO invoices (id, project, period_start, period_end, total)
|
|
VALUES ('$invoice_id', '$project', '$start_date', '$end_date', $total)
|
|
"
|
|
|
|
# Export to file
|
|
local invoice_file="$COST_DIR/reports/${invoice_id}.txt"
|
|
{
|
|
echo "INVOICE: $invoice_id"
|
|
echo "Project: $project"
|
|
echo "Period: $start_date to $end_date"
|
|
echo "Generated: $(date)"
|
|
echo ""
|
|
echo "Total: \$$total"
|
|
} > "$invoice_file"
|
|
|
|
echo "Saved to: $invoice_file"
|
|
}
|
|
|
|
# Cost forecast
|
|
forecast() {
|
|
local project="${1:-all}"
|
|
local days="${2:-30}"
|
|
|
|
echo -e "${PINK}=== COST FORECAST ===${RESET}"
|
|
echo "Based on last 7 days, projecting $days days"
|
|
echo
|
|
|
|
local where=""
|
|
[ "$project" != "all" ] && where="WHERE project = '$project'"
|
|
|
|
local daily_avg=$(sqlite3 "$COST_DB" "
|
|
SELECT SUM(cost) / 7 FROM usage
|
|
WHERE datetime(timestamp, '+7 days') > datetime('now')
|
|
$where
|
|
")
|
|
|
|
local projected=$(echo "scale=2; $daily_avg * $days" | bc)
|
|
|
|
echo "Daily average: \$${daily_avg:-0}"
|
|
echo "Projected ${days}-day cost: \$$projected"
|
|
|
|
if [ "$project" != "all" ]; then
|
|
local limit=$(sqlite3 "$COST_DB" "SELECT monthly_limit FROM budgets WHERE project = '$project'")
|
|
if [ -n "$limit" ]; then
|
|
local pct=$(echo "scale=1; $projected * 100 / $limit" | bc)
|
|
echo "Budget utilization: ${pct}%"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Collect usage from nodes
|
|
collect() {
|
|
echo -e "${PINK}=== COLLECTING USAGE ===${RESET}"
|
|
echo
|
|
|
|
for node in "${ALL_NODES[@]}"; do
|
|
echo -n " $node: "
|
|
|
|
if ! ssh -o ConnectTimeout=3 "$node" "echo ok" >/dev/null 2>&1; then
|
|
echo "(offline)"
|
|
continue
|
|
fi
|
|
|
|
# Get resource usage
|
|
local metrics=$(ssh "$node" "
|
|
cpu_hours=\$(cat /proc/stat | awk '/^cpu / {print (\$2+\$3+\$4)/100/3600}')
|
|
mem_gb=\$(free -g | awk '/Mem:/ {print \$3}')
|
|
disk_gb=\$(df / | awk 'NR==2 {print \$3/1024/1024}')
|
|
echo \"\$cpu_hours|\$mem_gb|\$disk_gb\"
|
|
" 2>/dev/null)
|
|
|
|
if [ -n "$metrics" ]; then
|
|
local cpu=$(echo "$metrics" | cut -d'|' -f1)
|
|
local mem=$(echo "$metrics" | cut -d'|' -f2)
|
|
local disk=$(echo "$metrics" | cut -d'|' -f3)
|
|
|
|
record "cpu" "$cpu" "default" "$node" >/dev/null
|
|
record "memory" "$mem" "default" "$node" >/dev/null
|
|
record "storage" "$disk" "default" "$node" >/dev/null
|
|
|
|
echo "collected"
|
|
else
|
|
echo "failed"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Reset monthly budgets
|
|
reset_budgets() {
|
|
sqlite3 "$COST_DB" "UPDATE budgets SET current_spend = 0"
|
|
echo -e "${GREEN}Reset all budget counters${RESET}"
|
|
}
|
|
|
|
# Help
|
|
help() {
|
|
echo -e "${PINK}BlackRoad Cost Tracker${RESET}"
|
|
echo
|
|
echo "Track resource usage and costs"
|
|
echo
|
|
echo "Usage Recording:"
|
|
echo " record <resource> <qty> [proj] Record usage"
|
|
echo " record-inference [proj] [in] [out] Record inference"
|
|
echo " collect Collect from nodes"
|
|
echo
|
|
echo "Rates & Budgets:"
|
|
echo " rates List rates"
|
|
echo " set-rate <res> <rate> [unit] Set rate"
|
|
echo " budget-create <proj> <limit> Create budget"
|
|
echo " budget-check Check budgets"
|
|
echo
|
|
echo "Reports:"
|
|
echo " current [proj] [day|week|month] Current costs"
|
|
echo " invoice <proj> [start] [end] Generate invoice"
|
|
echo " forecast [proj] [days] Cost forecast"
|
|
echo
|
|
echo "Examples:"
|
|
echo " $0 record inference 100 myproject"
|
|
echo " $0 budget-create myproject 50"
|
|
echo " $0 invoice myproject 2024-01-01"
|
|
}
|
|
|
|
# Ensure initialized
|
|
[ -f "$COST_DB" ] || init >/dev/null
|
|
|
|
case "${1:-help}" in
|
|
init)
|
|
init
|
|
;;
|
|
record)
|
|
record "$2" "$3" "$4" "$5"
|
|
;;
|
|
record-inference)
|
|
record_inference "$2" "$3" "$4" "$5"
|
|
;;
|
|
collect)
|
|
collect
|
|
;;
|
|
rates)
|
|
rates
|
|
;;
|
|
set-rate)
|
|
set_rate "$2" "$3" "$4"
|
|
;;
|
|
budget-create|budget)
|
|
budget_create "$2" "$3" "$4"
|
|
;;
|
|
budget-check|budgets)
|
|
budget_check
|
|
;;
|
|
current|costs)
|
|
current "$2" "$3"
|
|
;;
|
|
invoice)
|
|
invoice "$2" "$3" "$4"
|
|
;;
|
|
forecast)
|
|
forecast "$2" "$3"
|
|
;;
|
|
reset-budgets)
|
|
reset_budgets
|
|
;;
|
|
*)
|
|
help
|
|
;;
|
|
esac
|