#!/usr/bin/env nu # Volume management commands — hcloud-backed, workspace-aware. use lib_provisioning/utils/interface.nu [_print set-provisioning-out set-provisioning-no-terminal] use lib_provisioning/utils/settings.nu [find_get_settings] use lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft] def main [ ...args: string --infra (-i): string = "" --yes (-y) --out: string = "" ]: nothing -> nothing { if ($out | is-not-empty) { set-provisioning-out $out set-provisioning-no-terminal true } let subcmd = ($args | get 0? | default "") let rest = if ($args | length) > 1 { $args | skip 1 } else { [] } match $subcmd { "list" | "l" => { main list --infra $infra --out $out } "create" | "c" => { let name = ($rest | get 0? | default "") let size = ($rest | get 1? | default "20") let loc = ($rest | get 2? | default "") main create $name --size ($size | into int) --location $loc --yes=$yes } "attach" | "a" => { let name = ($rest | get 0? | default "") let server = ($rest | get 1? | default "") main attach $name --server $server --yes=$yes } "detach" | "d" => { let name = ($rest | get 0? | default "") main detach $name --yes=$yes } "delete" | "rm" => { let name = ($rest | get 0? | default "") main delete $name --yes=$yes } "" | "help" => { show-volume-help } _ => { _print $"❌ Unknown volume subcommand: ($subcmd)" show-volume-help } } } def show-volume-help [] { _print " Volume Management ================= Usage: provisioning volume [args] Commands: list List all volumes with attachment status create [size] [location] Create a new volume (default: 20GB, infra location) attach Attach a volume to a server detach Detach a volume from its server delete Delete a volume (must be detached) Examples: prvng volume list prvng volume create libre-daoshi-data 20 fsn1 prvng volume attach libre-daoshi-data libre-daoshi-0 prvng volume detach libre-daoshi-data prvng volume delete libre-daoshi-data " } export def "main list" [ --infra (-i): string = "" --out: string = "" ]: nothing -> nothing { if ($out | is-not-empty) { set-provisioning-out $out set-provisioning-no-terminal true } let res = (do { ^hcloud volume list -o json } | complete) if $res.exit_code != 0 or ($res.stdout | str trim | is-empty) { _print "⚠ hcloud unavailable or no volumes found" return } let vols = ($res.stdout | from json) if ($vols | is-empty) { _print "No volumes found" return } # Resolve infra filter from workspace context let infra_filter = if ($infra | is-not-empty) { $infra | path basename } else { let ws_config = ($env.PWD | path join "config" "provisioning.ncl") if ($ws_config | path exists) { (ncl-eval-soft $ws_config [] {} | get -o current_infra | default "") } else { "" } } let rows = ($vols | each {|v| let server_name = ($v.server?.name? | default "—") let protection = if ($v.protection?.delete? | default false) { "🔒" } else { "" } { name: $v.name size: $"($v.size)GB" location: ($v.location?.name? | default "") format: ($v.format? | default "—") server: $server_name status: $v.status protection: $protection } }) _print ($rows | table -i false) } export def "main create" [ name: string --size (-s): int = 20 --location (-l): string = "" --format (-f): string = "ext4" --yes (-y) ]: nothing -> nothing { if ($name | is-empty) { error make { msg: "Usage: provisioning volume create [--size ] [--location ]" } } # Resolve location: flag > infra settings > fsn1 let loc = if ($location | is-not-empty) { $location } else { let ws_config = ($env.PWD | path join "config" "provisioning.ncl") if ($ws_config | path exists) { (ncl-eval-soft $ws_config [] {} | get -o region | default "fsn1") } else { "fsn1" } } # Check if already exists let existing = (do { ^hcloud volume describe $name -o json } | complete) if $existing.exit_code == 0 { _print $"â„šī¸ Volume '($name)' already exists" return } if not $yes { _print $"Create volume '($name)' — ($size)GB, ($loc), format: ($format)" _print "Confirm? [y/N] " let c = (input "") if $c not-in ["y", "Y", "yes"] { _print "Aborted."; return } } let res = (do { ^hcloud volume create --name $name --size ($size | into string) --location $loc --format $format } | complete) if $res.exit_code != 0 { error make { msg: $"Failed to create volume: ($res.stderr)" } } _print $"✓ Volume '($name)' created — ($size)GB at ($loc)" } export def "main attach" [ name: string --server (-s): string = "" --yes (-y) ]: nothing -> nothing { if ($name | is-empty) or ($server | is-empty) { error make { msg: "Usage: provisioning volume attach --server " } } let vol_res = (do { ^hcloud volume describe $name -o json } | complete) if $vol_res.exit_code != 0 { error make { msg: $"Volume '($name)' not found" } } let vol = ($vol_res.stdout | from json) let current_srv = ($vol.server?.name? | default "") if ($current_srv | is-not-empty) { if $current_srv == $server { _print $"â„šī¸ Volume '($name)' already attached to '($server)'" return } error make { msg: $"Volume '($name)' is attached to '($current_srv)' — detach first" } } let res = (do { ^hcloud volume attach $name --server $server } | complete) if $res.exit_code != 0 { error make { msg: $"Failed to attach: ($res.stderr)" } } _print $"✓ Volume '($name)' attached to '($server)'" } export def "main detach" [ name: string --yes (-y) ]: nothing -> nothing { if ($name | is-empty) { error make { msg: "Usage: provisioning volume detach " } } let vol_res = (do { ^hcloud volume describe $name -o json } | complete) if $vol_res.exit_code != 0 { error make { msg: $"Volume '($name)' not found" } } let vol = ($vol_res.stdout | from json) let current_srv = ($vol.server?.name? | default "") if ($current_srv | is-empty) { _print $"â„šī¸ Volume '($name)' is not attached" return } if not $yes { _print $"Detach '($name)' from '($current_srv)'? [y/N] " let c = (input "") if $c not-in ["y", "Y", "yes"] { _print "Aborted."; return } } let res = (do { ^hcloud volume detach $name } | complete) if $res.exit_code != 0 { error make { msg: $"Failed to detach: ($res.stderr)" } } _print $"✓ Volume '($name)' detached from '($current_srv)'" } export def "main delete" [ name: string --yes (-y) ]: nothing -> nothing { if ($name | is-empty) { error make { msg: "Usage: provisioning volume delete " } } let vol_res = (do { ^hcloud volume describe $name -o json } | complete) if $vol_res.exit_code != 0 { error make { msg: $"Volume '($name)' not found" } } let vol = ($vol_res.stdout | from json) if ($vol.server? | default null) != null { error make { msg: $"Volume '($name)' is attached to '($vol.server.name)' — detach first" } } if not $yes { _print $"Permanently delete volume '($name)' (($vol.size)GB)? Type '($name)' to confirm: " let c = (input "") if $c != $name { _print "Aborted."; return } } # Disable protection if set if ($vol.protection?.delete? | default false) { let unlock = (do { ^hcloud volume disable-protection $name delete } | complete) if $unlock.exit_code != 0 { error make { msg: $"Failed to disable protection: ($unlock.stderr)" } } } let res = (do { ^hcloud volume delete $name } | complete) if $res.exit_code != 0 { error make { msg: $"Failed to delete: ($res.stderr)" } } _print $"✓ Volume '($name)' deleted" }