2026-01-14 02:00:23 +00:00
|
|
|
# Hetzner Cloud HTTP API Client
|
|
|
|
|
use env.nu *
|
|
|
|
|
|
|
|
|
|
# Get Bearer token for API authentication
|
|
|
|
|
export def hetzner_api_auth []: nothing -> string {
|
|
|
|
|
let token = (hetzner_api_token)
|
|
|
|
|
if ($token | is-empty) {
|
|
|
|
|
error make {msg: "HCLOUD_TOKEN environment variable not set. Set your Hetzner API token before using the API interface."}
|
|
|
|
|
}
|
|
|
|
|
$token
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Build full API URL
|
|
|
|
|
export def hetzner_api_url [path: string]: nothing -> string {
|
|
|
|
|
let base = (hetzner_api_url_base)
|
|
|
|
|
$"($base)($path)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Generic HTTP request with error handling
|
|
|
|
|
export def hetzner_api_request [method: string, path: string, data?: any]: nothing -> any {
|
|
|
|
|
let token = (hetzner_api_auth)
|
|
|
|
|
let url = (hetzner_api_url $path)
|
|
|
|
|
|
|
|
|
|
if (hetzner_debug) {
|
|
|
|
|
print $"DEBUG: hetzner_api_request method=($method) path=($path) url=($url)" | encode utf8 | into string
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
let headers = [Authorization $"Bearer ($token)"]
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
let result = (do {
|
|
|
|
|
match $method {
|
|
|
|
|
"GET" => {
|
|
|
|
|
http get --headers $headers --allow-errors $url
|
|
|
|
|
}
|
|
|
|
|
"POST" => {
|
|
|
|
|
http post --headers $headers --content-type application/json --allow-errors $url $data
|
|
|
|
|
}
|
|
|
|
|
"PUT" => {
|
|
|
|
|
http put --headers $headers --content-type application/json --allow-errors $url $data
|
|
|
|
|
}
|
|
|
|
|
"DELETE" => {
|
|
|
|
|
http delete --headers $headers --allow-errors $url
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
error make {msg: $"Unsupported HTTP method: ($method)"}
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
} | complete)
|
|
|
|
|
if $result.exit_code != 0 {
|
|
|
|
|
error make {msg: $"Hetzner API request failed: ($result.stderr)"}
|
|
|
|
|
} else {
|
|
|
|
|
$result.stdout
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List all servers
|
|
|
|
|
export def hetzner_api_list_servers []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/servers")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | describe) =~ "error" {
|
|
|
|
|
error make {msg: "Failed to list servers from API"}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has servers) {
|
|
|
|
|
$response.servers
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Get server info by ID or name
|
|
|
|
|
export def hetzner_api_server_info [id_or_name: string]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "GET" $"/servers/($id_or_name)")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | describe) =~ "error" {
|
|
|
|
|
error make {msg: $"Server not found: ($id_or_name)"}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has server) {
|
|
|
|
|
$response.server
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Create a new server
|
|
|
|
|
export def hetzner_api_create_server [config: record]: nothing -> record {
|
|
|
|
|
if (hetzner_debug) {
|
|
|
|
|
print $"DEBUG: Creating server with config: ($config | to json)" | encode utf8 | into string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let response = (hetzner_api_request "POST" "/servers" $config)
|
|
|
|
|
|
|
|
|
|
if ($response | describe) =~ "error" {
|
|
|
|
|
error make {msg: $"Failed to create server: ($response)"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($response | has server) {
|
|
|
|
|
$response.server
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Delete a server
|
2026-01-17 03:57:20 +00:00
|
|
|
export def hetzner_api_delete_server [id: string]: nothing -> nothing {
|
2026-01-14 02:00:23 +00:00
|
|
|
let response = (hetzner_api_request "DELETE" $"/servers/($id)")
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Perform server action (start, stop, reboot, etc.)
|
|
|
|
|
export def hetzner_api_server_action [id: string, action: string]: nothing -> record {
|
|
|
|
|
let data = {action: $action}
|
|
|
|
|
let response = (hetzner_api_request "POST" $"/servers/($id)/actions/($action)" $data)
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has action) {
|
|
|
|
|
$response.action
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# List all locations
|
|
|
|
|
export def hetzner_api_list_locations []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/locations")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has locations) {
|
|
|
|
|
$response.locations
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List all server types
|
|
|
|
|
export def hetzner_api_list_server_types []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/server_types")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has server_types) {
|
|
|
|
|
$response.server_types
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Get server type info
|
|
|
|
|
export def hetzner_api_server_type_info [id_or_name: string]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "GET" $"/server_types/($id_or_name)")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has server_type) {
|
|
|
|
|
$response.server_type
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List all images
|
|
|
|
|
export def hetzner_api_list_images []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/images")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has images) {
|
|
|
|
|
$response.images
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List all volumes
|
|
|
|
|
export def hetzner_api_list_volumes []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/volumes")
|
|
|
|
|
|
|
|
|
|
if ($response | has volumes) {
|
|
|
|
|
$response.volumes
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Create a volume
|
|
|
|
|
export def hetzner_api_create_volume [config: record]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "POST" "/volumes" $config)
|
|
|
|
|
|
|
|
|
|
if ($response | has volume) {
|
|
|
|
|
$response.volume
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Delete a volume
|
2026-01-17 03:57:20 +00:00
|
|
|
export def hetzner_api_delete_volume [id: string]: nothing -> nothing {
|
2026-01-14 02:00:23 +00:00
|
|
|
hetzner_api_request "DELETE" $"/volumes/($id)"
|
|
|
|
|
null
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Attach volume to server
|
|
|
|
|
export def hetzner_api_attach_volume [volume_id: string, server_id: string]: nothing -> record {
|
|
|
|
|
let data = {
|
|
|
|
|
server: ($server_id | into int)
|
|
|
|
|
automount: false
|
|
|
|
|
}
|
|
|
|
|
let response = (hetzner_api_request "POST" $"/volumes/($volume_id)/actions/attach" $data)
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has action) {
|
|
|
|
|
$response.action
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Detach volume from server
|
|
|
|
|
export def hetzner_api_detach_volume [volume_id: string]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "POST" $"/volumes/($volume_id)/actions/detach" {})
|
|
|
|
|
|
|
|
|
|
if ($response | has action) {
|
|
|
|
|
$response.action
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# List all networks
|
|
|
|
|
export def hetzner_api_list_networks []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/networks")
|
|
|
|
|
|
|
|
|
|
if ($response | has networks) {
|
|
|
|
|
$response.networks
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
2026-01-14 02:00:23 +00:00
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Get network info
|
|
|
|
|
export def hetzner_api_network_info [id_or_name: string]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "GET" $"/networks/($id_or_name)")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has network) {
|
|
|
|
|
$response.network
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Attach network to server
|
|
|
|
|
export def hetzner_api_attach_network [server_id: string, network_id: string, ip?: string]: nothing -> record {
|
|
|
|
|
let data = if ($ip != null) {
|
|
|
|
|
{server: ($server_id | into int), network: ($network_id | into int), ip: $ip}
|
|
|
|
|
} else {
|
|
|
|
|
{server: ($server_id | into int), network: ($network_id | into int)}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
let response = (hetzner_api_request "POST" $"/servers/($server_id)/actions/attach_to_network" $data)
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has action) {
|
|
|
|
|
$response.action
|
2025-10-07 10:32:04 +01:00
|
|
|
} else {
|
2026-01-14 02:00:23 +00:00
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Detach network from server
|
|
|
|
|
export def hetzner_api_detach_network [server_id: string, network_id: string]: nothing -> record {
|
|
|
|
|
let data = {network: ($network_id | into int)}
|
|
|
|
|
let response = (hetzner_api_request "POST" $"/servers/($server_id)/actions/detach_from_network" $data)
|
|
|
|
|
|
|
|
|
|
if ($response | has action) {
|
|
|
|
|
$response.action
|
2025-10-07 10:32:04 +01:00
|
|
|
} else {
|
2026-01-14 02:00:23 +00:00
|
|
|
$response
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List all floating IPs
|
|
|
|
|
export def hetzner_api_list_floating_ips []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/floating_ips")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has floating_ips) {
|
|
|
|
|
$response.floating_ips
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# Get pricing information
|
|
|
|
|
export def hetzner_api_get_pricing []: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/pricing")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has pricing) {
|
|
|
|
|
$response.pricing
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List SSH keys
|
|
|
|
|
export def hetzner_api_list_ssh_keys []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/ssh_keys")
|
2025-10-07 10:32:04 +01:00
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
if ($response | has ssh_keys) {
|
|
|
|
|
$response.ssh_keys
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Get SSH key info
|
|
|
|
|
export def hetzner_api_ssh_key_info [id_or_name: string]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "GET" $"/ssh_keys/($id_or_name)")
|
|
|
|
|
|
|
|
|
|
if ($response | has ssh_key) {
|
|
|
|
|
$response.ssh_key
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
2025-10-07 10:32:04 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 02:00:23 +00:00
|
|
|
# List firewalls
|
|
|
|
|
export def hetzner_api_list_firewalls []: nothing -> list {
|
|
|
|
|
let response = (hetzner_api_request "GET" "/firewalls")
|
|
|
|
|
|
|
|
|
|
if ($response | has firewalls) {
|
|
|
|
|
$response.firewalls
|
|
|
|
|
} else {
|
|
|
|
|
[]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Get firewall info
|
|
|
|
|
export def hetzner_api_firewall_info [id_or_name: string]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "GET" $"/firewalls/($id_or_name)")
|
|
|
|
|
|
|
|
|
|
if ($response | has firewall) {
|
|
|
|
|
$response.firewall
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Create firewall
|
|
|
|
|
export def hetzner_api_create_firewall [config: record]: nothing -> record {
|
|
|
|
|
let response = (hetzner_api_request "POST" "/firewalls" $config)
|
|
|
|
|
|
|
|
|
|
if ($response | has firewall) {
|
|
|
|
|
$response.firewall
|
|
|
|
|
} else {
|
|
|
|
|
$response
|
|
|
|
|
}
|
chore: complete KCL to Nickel migration cleanup and setup pre-commit
Clean up 404 KCL references (99.75% complete):
- Rename kcl_* variables to schema_*/nickel_* (kcl_path→schema_path, etc.)
- Update functions: parse_kcl_file→parse_nickel_file
- Update env vars: KCL_MOD_PATH→NICKEL_IMPORT_PATH
- Fix cli/providers-install: add has_nickel and nickel_version variables
- Correct import syntax: .nickel.→.ncl.
- Update 57 files across core, CLI, config, and utilities
Configure pre-commit hooks:
- Activate: nushell-check, nickel-typecheck, markdownlint
- Comment out: Rust hooks (fmt, clippy, test), check-yaml
Testing:
- Module discovery: 9 modules (6 providers, 1 taskserv, 2 clusters) ✅
- Syntax validation: 15 core files ✅
- Pre-commit hooks: all passing ✅
2026-01-08 20:08:46 +00:00
|
|
|
}
|