# Hetzner Cloud Provider Interface Implementation # Implements all 25 required provider interface functions use ./nulib/hetzner/mod.nu * # ============================================================================ # Provider Metadata Function (REQUIRED) # ============================================================================ export def get-provider-metadata []: nothing -> record { { name: "hetzner" version: "1.0.0" type: "cloud" description: "Hetzner Cloud provider for European cloud infrastructure" capabilities: { server_management: true network_management: true storage_management: true load_balancer: true firewall: true floating_ip: true volume_management: true ssh_management: true placement_groups: true backups: true snapshots: true managed_databases: false kubernetes: false } api: { type: "rest" base_url: "https://api.hetzner.cloud/v1" auth_method: "bearer_token" documentation: "https://docs.hetzner.cloud/" version: "v1" } cli: { tool: "hcloud" version: "1.44.0" install_url: "https://github.com/hetznercloud/cli" fallback: true } limits: { max_servers: 50 max_volumes: 100 max_networks: 10 max_floating_ips: 20 } regions: [ {code: "fsn1", name: "Falkenstein", country: "Germany"} {code: "nbg1", name: "Nuremberg", country: "Germany"} {code: "hel1", name: "Helsinki", country: "Finland"} {code: "ash", name: "Ashburn", country: "USA"} {code: "hil", name: "Hillsboro", country: "USA"} ] } } # ============================================================================ # Server Query Functions (4 functions) # ============================================================================ export def query_servers [find?: string, cols?: string]: nothing -> list { let str_find = if $find != null { $find } else { "" } let str_cols = if $cols != null { $cols } else { "" } hetzner_query_servers $str_find $str_cols } export def server_info [server: string, check: bool = false, find?: string, cols?: string]: nothing -> record { let str_find = if $find != null { $find } else { "" } let str_cols = if $cols != null { $cols } else { "" } hetzner_server_info $server $check $str_find $str_cols } export def server_exists [server: string, error_exit: bool = false]: nothing -> bool { hetzner_server_exists $server $error_exit } export def server_is_running [server: string, error_exit: bool = false]: nothing -> bool { hetzner_server_is_running $server $error_exit } # ============================================================================ # Server Lifecycle Functions (6 functions) # ============================================================================ export def check_server_requirements [settings: record, server: record, check: bool = false]: nothing -> bool { hetzner_check_server_requirements $settings $server $check } export def create_server [settings: record, server: record, check: bool = false, wait: bool = true]: nothing -> record { hetzner_create_server $settings $server $check $wait } export def delete_server [settings: record, server: record, keep_storage: bool = true, error_exit: bool = true]: nothing -> nothing { hetzner_delete_server $settings $server.hostname $keep_storage $error_exit } export def delete_server_storage [settings: record, server: record, error_exit: bool = true]: nothing -> nothing { hetzner_delete_server_storage $settings $server.hostname $error_exit } export def post_create_server [settings: record, server: record, check: bool = false]: nothing -> nothing { hetzner_post_create_server $settings $server $check } export def modify_server [settings: record, server: string, new_values: record, error_exit: bool = true]: nothing -> record { hetzner_modify_server $settings $server $new_values $error_exit } # ============================================================================ # Server State Functions (4 functions) # ============================================================================ export def server_state [server: string, new_state: string, error_exit: bool = true, wait: bool = true, settings: record = {}]: nothing -> record { hetzner_server_state $server $new_state $error_exit $wait $settings } # Power off and block until status=="off" (or timeout). Required by callers # that need a deterministic post-state — e.g. fleet daemon's AlwaysOnResize # strategy chains power_off → change_type → power_on and depends on the # guarantee that the server is genuinely off before issuing change_type. export def power_off [server: string, timeout_sec: int = 30, poll_ms: int = 500]: nothing -> record { hetzner_power_off_wait $server $timeout_sec $poll_ms } # Power on (fire-and-forget, idempotent). export def power_on [server: string]: nothing -> record { hetzner_power_on $server } # Full resize flow: power_off (wait) → change_type (upgrade_disk=!keep_disk) → # power_on. keep_disk defaults to true (preserve disk allocation, AlwaysOnResize # semantics). export def resize_server [server: string, target_type: string, keep_disk: bool = true, settings: record = {}]: nothing -> record { hetzner_resize_server $server $target_type $keep_disk $settings } # ============================================================================ # Network Operations (2 functions) # ============================================================================ export def get_ip [settings: record, server: string, ip_type: string = "ipv4", error_exit: bool = true]: nothing -> string { hetzner_get_ip $settings $server $ip_type $error_exit } export def servers_ips [settings: record, data: any, prov: string = "hetzner", serverpos: int = 0]: nothing -> list { hetzner_servers_ips $settings $data $prov $serverpos } # ============================================================================ # Infrastructure Operations (5 functions) # ============================================================================ export def load_infra_servers_info [settings: record, server: record, error_exit: bool = true]: nothing -> record { hetzner_server_info $server.hostname false "" "" } export def load_infra_storages_info [settings: record, server: record, error_exit: bool = true]: nothing -> list { let servers = query_servers "" "" let server_data = $servers | where {|s| $s.name == $server.hostname} | first if ("volumes" in $server_data) { $server_data.volumes } else { [] } } export def get_infra_storage [server: record, settings: record, cloud_data: any, error_exit: bool = true]: nothing -> record { if ("volumes" in $cloud_data) and (not ($cloud_data.volumes | is-empty)) { $cloud_data.volumes | first } else { {} } } export def get_infra_item [server: record, settings: record, cloud_data: any, error_exit: bool = true]: nothing -> record { if ($cloud_data | describe) == "record" { $cloud_data } else { {} } } export def get_infra_price [server: record, data: any, key: string, error_exit: bool = true, price_col: string = "total_monthly_gross"]: nothing -> float { let cost = hetzner_calculate_server_cost $server if ("error" in $cost) { if $error_exit { throw-error $"Could not calculate price: ($cost.message)" } return 0 } if ($price_col in $cost) { $cost | get $price_col } else { 0 } } # ============================================================================ # Cache Operations (5 functions) # ============================================================================ export def start_cache_info [settings: record, server: string]: nothing -> nothing { hetzner_start_cache_info $settings $server } export def create_cache [settings: record, server: string, error_exit: bool = true]: nothing -> nothing { hetzner_create_cache $settings $server $error_exit } export def read_cache [settings: record, server: string, error_exit: bool = true]: nothing -> record { hetzner_read_cache $settings $server $error_exit } export def clean_cache [settings: record, server: string, error_exit: bool = true]: nothing -> nothing { hetzner_clean_cache $settings $server $error_exit } export def ip_from_cache [settings: record, server: string, error_exit: bool = true]: nothing -> string { hetzner_ip_from_cache $settings $server $error_exit } export def enrich_template_context [settings: record, context: record, private_network: string]: nothing -> record { hetzner_enrich_template_context $settings $context $private_network } # ============================================================================ # Display/Message Function (1 function) # ============================================================================ export def on_prov_server [server: record]: nothing -> string { let location = if ("location" in $server) {$server.location} else {"unknown"} let type = if ("server_type" in $server) {$server.server_type} else {"unknown"} $"Hetzner Cloud ($location) - ($type)" } # ============================================================================ # Helper Functions (Optional, but useful) # ============================================================================ export def hetzner_version []: nothing -> string { "1.0.0" } export def hetzner_info []: nothing -> record { get-provider-metadata } export def hetzner_supported_regions []: nothing -> list { let meta = get-provider-metadata $meta.regions | map {|r| $r.code} } export def hetzner_supported_server_types []: nothing -> list { let types = hetzner_api_list_server_types $types | each {|t| $t.name} } export def hetzner_list_locations []: nothing -> list { hetzner_api_list_locations } export def hetzner_get_account_info []: nothing -> record { { provider: "hetzner" version: (hetzner_version) status: "active" interface: (hetzner_interface) timestamp: (now) } }