provisioning-core/nulib/cli/provider.nu

321 lines
12 KiB
Text
Raw Normal View History

#!/usr/bin/env nu
# prvng provider — live resource state for catalog providers.
export-env {
let lib_dirs_raw = ($env.NU_LIB_DIRS? | default "")
let current_lib_dirs = if ($lib_dirs_raw | type) == "string" {
if ($lib_dirs_raw | is-empty) { [] } else { ($lib_dirs_raw | split row ":") }
} else {
$lib_dirs_raw
}
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
$env.NU_LIB_DIRS = ([
"/opt/provisioning/core/nulib"
"/usr/local/provisioning/core/nulib"
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
}
def prov-root [] {
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-empty) {
error make --unspanned { msg: "PROVISIONING env var not set" }
}
let direct = ($prov | path join "catalog" "providers")
if ($direct | path exists) { return $direct }
let via_code = ($prov | path join "code" "catalog" "providers")
if ($via_code | path exists) { return $via_code }
error make --unspanned { msg: $"catalog/providers not found under ($prov)" }
}
def available-providers [] {
ls (prov-root) | where type == "dir" | get name | each { |p| $p | path basename }
}
def hcloud-all [] {
let r = (do { ^hcloud all list -o json } | complete)
if $r.exit_code != 0 {
error make --unspanned { msg: $"hcloud failed: ($r.stderr | str trim)" }
}
$r.stdout | from json
}
def server-location-price [server] {
let loc = $server.location.name
let prices = ($server.server_type.prices? | default [])
let matched = $prices | where location == $loc
let entry = if ($matched | is-empty) {
if ($prices | is-empty) { return 0.0 } else { $prices | first }
} else {
$matched | first
}
$entry.price_monthly.net | into float
}
def sep-line [char: string, width: int] {
0..<$width | each { $char } | str join ""
}
def fmt-eur [amount: float] {
let cents = ($amount * 100 | math round | into int)
let euros = ($cents // 100)
let c = ($cents mod 100 | into string | fill -a r -w 2 -c "0")
$"($euros).($c)"
}
def plural [n: int, singular: string, plural_form: string] {
if $n == 1 { $"($n) ($singular)" } else { $"($n) ($plural_form)" }
}
# Collect all Hetzner resource data into a clean record (used by both text and JSON output).
def hetzner-collect [] {
let r = (hcloud-all)
let servers = ($r.servers? | default [])
let volumes = ($r.volumes? | default [])
let fips = ($r.floating_ips? | default [])
let primary_ips = ($r.primary_ips? | default [])
let firewalls = ($r.firewalls? | default [])
let networks = ($r.networks? | default [])
let pgroups = ($r.placement_groups? | default [])
let ssh_keys = ($r.ssh_keys? | default [])
let primary_ipv4 = ($primary_ips | where type == "ipv4")
# Rates: Hetzner invoice 05/2026 (net, excl. VAT)
let server_total = if ($servers | is-empty) { 0.0 } else {
$servers | each { |s| server-location-price $s } | math sum
}
let volume_gb = if ($volumes | is-empty) { 0 } else { $volumes | each { |v| $v.size } | math sum }
let volume_cost = ($volume_gb * 0.0572)
let fip_cost = (($fips | length) * 3.00)
let primary_cost = (($primary_ipv4 | length) * 0.50)
let obj_cost = 6.49
let total_net = ($server_total + $volume_cost + $fip_cost + $primary_cost + $obj_cost)
{
provider: "hetzner"
resources: {
servers: ($servers | each { |s|
{
name: $s.name
status: $s.status
type: $s.server_type.name
location: $s.location.name
monthly_eur: (server-location-price $s)
}
})
volumes: ($volumes | each { |v|
{
name: $v.name
size_gb: $v.size
location: ($v.location? | default {} | get -o name | default "?")
status: $v.status
}
})
floating_ips: ($fips | each { |f|
let labels = ($f.labels? | default {})
{
name: $f.name
ip: $f.ip
type: $f.type
workspace: ($labels | get -o workspace | default ($labels | get -o cluster | default ""))
}
})
primary_ipv4: ($primary_ipv4 | each { |p|
{
name: $p.name
ip: $p.ip
assigned: ($p.assignee_id? != null)
}
})
firewalls: ($firewalls | each { |fw|
{
name: $fw.name
applied_to: ($fw.applied_to? | default [] | length)
}
})
networks: ($networks | each { |n|
{
name: $n.name
ip_range: ($n.ip_range? | default "?")
server_count: ($n.servers? | default [] | length)
}
})
placement_groups: ($pgroups | each { |pg|
{
name: $pg.name
type: ($pg.type? | default "")
server_count: ($pg.servers? | default [] | length)
}
})
ssh_keys: ($ssh_keys | each { |k| { name: $k.name } })
}
costs: {
servers: ($server_total | math round -p 2)
volumes: ($volume_cost | math round -p 2)
volumes_gb: $volume_gb
floating_ips: ($fip_cost | math round -p 2)
primary_ips: ($primary_cost | math round -p 2)
object_storage: $obj_cost
total_net: ($total_net | math round -p 2)
total_vat: (($total_net * 1.19) | math round -p 2)
}
}
}
def hetzner-usage [fmt: string] {
let d = (hetzner-collect)
match $fmt {
"json" => { $d | to json | print; return }
"yaml" => { $d | to yaml | print; return }
"text" | "human" => {}
_ => { error make --unspanned { msg: $"Unknown format '($fmt)'. Use: text, json, yaml" } }
}
let s = $d.resources.servers
let v = $d.resources.volumes
let f = $d.resources.floating_ips
let pi = $d.resources.primary_ipv4
let fw = $d.resources.firewalls
let n = $d.resources.networks
let pg = $d.resources.placement_groups
let sk = $d.resources.ssh_keys
let c = $d.costs
print $"Hetzner Cloud — Active Resources"
print (sep-line "═" 52)
print ""
if ($s | is-not-empty) {
print $"servers \(($s | length)\)"
for srv in $s {
let name_col = ($srv.name | fill -a l -w 26)
let status_col = ($srv.status | fill -a l -w 9)
let type_col = ($srv.type | fill -a l -w 7)
let loc_col = ($srv.location | fill -a l -w 5)
print $" ($name_col) ($status_col) ($type_col) ($loc_col) €(fmt-eur $srv.monthly_eur)/mo"
}
print ""
}
if ($v | is-not-empty) {
print $"volumes \(($v | length)\)"
for vol in $v {
let name_col = ($vol.name | fill -a l -w 38)
let size_col = ($"($vol.size_gb)GB" | fill -a l -w 7)
print $" ($name_col) ($size_col) ($vol.location) ($vol.status)"
}
print ""
}
if ($f | is-not-empty) {
print $"floating_ips \(($f | length)\)"
for fip in $f {
let name_col = ($fip.name | fill -a l -w 30)
let ip_col = ($fip.ip | fill -a l -w 16)
print $" ($name_col) ($ip_col) ($fip.type) ($fip.workspace)"
}
print ""
}
if ($pi | is-not-empty) {
print $"primary_ips IPv4 \(($pi | length)\)"
for p in $pi {
let name_col = ($p.name | fill -a l -w 30)
let ip_col = ($p.ip | fill -a l -w 16)
let asgn = if $p.assigned { "assigned" } else { "unassigned" }
print $" ($name_col) ($ip_col) ($asgn)"
}
print ""
}
if ($fw | is-not-empty) {
print $"firewalls \(($fw | length)\)"
for firewall in $fw {
let name_col = ($firewall.name | fill -a l -w 30)
print $" ($name_col) applied to (plural $firewall.applied_to "resource" "resources")"
}
print ""
}
if ($n | is-not-empty) {
print $"networks \(($n | length)\)"
for net in $n {
let name_col = ($net.name | fill -a l -w 22)
print $" ($name_col) ($net.ip_range) ($net.server_count) servers"
}
print ""
}
if ($pg | is-not-empty) {
print $"placement_groups \(($pg | length)\)"
for g in $pg {
let name_col = ($g.name | fill -a l -w 26)
print $" ($name_col) type: ($g.type) ($g.server_count) servers"
}
print ""
}
if ($sk | is-not-empty) {
print $"ssh_keys \(($sk | length)\)"
for k in $sk { print $" ($k.name)" }
print ""
}
print (sep-line "─" 52)
print "Monthly estimate (net):"
print $" servers €(fmt-eur $c.servers)"
print $" volumes €(fmt-eur $c.volumes) \(($c.volumes_gb)GB × €0.0572/GB\)"
print $" floating IPv4 €(fmt-eur $c.floating_ips) \(($f | length) × €3.00\)"
print $" primary IPv4 €(fmt-eur $c.primary_ips) \(($pi | length) × €0.50\)"
print $" object storage €(fmt-eur $c.object_storage) \(base — S3, not in hcloud\)"
print $" ─────────────────────────────────"
print $" total net €(fmt-eur $c.total_net)/mo"
print $" total +VAT €(fmt-eur $c.total_vat)/mo"
}
def cmd-usage [provider: string, fmt: string] {
match $provider {
"hetzner" => { hetzner-usage $fmt }
_ => {
let available = (try { available-providers | str join ", " } catch { "unknown" })
error make --unspanned { msg: $"No usage handler for '($provider)'. Catalog providers: ($available)" }
}
}
}
def main [
...args: string
--fmt: string = "text" # output format: text (default), json, yaml
--json (-j) # shorthand for --fmt json
] {
let format = if $json { "json" } else { $fmt }
let cmd = $args | get 0? | default ""
let provider = $args | get 1? | default ""
let sub = $args | get 2? | default "usage"
match $cmd {
"provider" | "pvd" => {
if ($provider | is-empty) {
print "prvng provider <name> [usage] [--fmt text|json|yaml]"
print ""
print "Available providers:"
for p in (try { available-providers } catch { [] }) { print $" ($p)" }
return
}
match $sub {
"usage" | "u" => { cmd-usage $provider $format }
_ => {
print $"prvng provider ($provider) [usage] [--fmt text|json|yaml]"
print ""
print "Subcommands:"
print " usage Live resources + monthly cost estimate"
}
}
}
_ => {
print "Usage: prvng provider <name> [usage] [--fmt text|json|yaml]"
}
}
}