prvng_core/nulib/provisioning-volume.nu

258 lines
8.4 KiB
Text
Raw Permalink Normal View History

feat(core): three-layer DAG, unified component arch, commands-registry cache, Nushell 0.112.2 migration - DAG architecture: `dag show/validate/export` (nulib/main_provisioning/dag.nu), config loader (lib_provisioning/config/loader/dag.nu), taskserv dag-executor. Backed by schemas/lib/dag/*.ncl; orchestrator emits NATS events via WorkspaceComposition::into_workflow. See ADR-020, ADR-021. - Unified Component Architecture: components/mod.nu, main_provisioning/ {components,workflow,extensions,ontoref-queries}.nu. Full workflow engine with topological sort and NATS subject emission. Blocks A-H complete (libre-daoshi). - Commands-registry: nulib/commands-registry.ncl (Nickel source, 314 lines) + JSON cache at ~/.cache/provisioning/commands-registry.json rebuilt on source change. cli/provisioning fast-path alias expansion avoids cold Nu startup. ADDING_COMMANDS.md documents new-command workflow. - Platform service manager: service-manager.nu (+573), startup.nu (+611), service-check.nu (+255); autostart/bootstrap/health/target refactored. - Nushell 0.112.2 migration: removed all try/catch and bash redirections; external commands prefixed with ^; type signatures enforced. Driven by scripts/refactor-try-catch{,-simplified}.nu. - TTY stack: removed shlib/*-tty.sh; replaced by cli/tty-dispatch.sh, tty-filter.sh, tty-commands.conf. - New domain modules: images/ (golden image lifecycle), workspace/{state,sync}.nu, main_provisioning/{bootstrap,cluster-deploy,fip,state}.nu, commands/{state, build,integrations/auth,utilities/alias}.nu, platform.nu expanded (+874). - Config loader overhaul: loader/core.nu slimmed (-759), cache/core.nu refactored (-454), removed legacy loaders/file_loader.nu (-330). - Thirteen new provisioning-<domain>.nu top-level modules for bash dispatcher. - Tests: test_workspace_state.nu (+351); updates to test_oci_registry, test_services. - README + CHANGELOG updated.
2026-04-17 04:27:33 +01:00
#!/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"
}