258 lines
8.4 KiB
Text
258 lines
8.4 KiB
Text
|
|
#!/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 <command> [args]
|
|||
|
|
|
|||
|
|
Commands:
|
|||
|
|
list List all volumes with attachment status
|
|||
|
|
create <name> [size] [location] Create a new volume (default: 20GB, infra location)
|
|||
|
|
attach <name> <server> Attach a volume to a server
|
|||
|
|
detach <name> Detach a volume from its server
|
|||
|
|
delete <name> 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 <name> [--size <GB>] [--location <loc>]" }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 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 <name> --server <hostname>" }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 <name>" }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 <name>" }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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"
|
|||
|
|
}
|