provisioning-core/nulib/cli/provider.nu

320 lines
12 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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]"
}
}
}