provisioning-core/nulib/platform/vm/volume_management.nu

424 lines
14 KiB
Text

# Volume Management System (Phase 4)
#
# Manages storage volumes for VMs and containers.
# Rule 1: Single purpose, Rule 5: Atomic operations
export def "volume-create" [
name: string # Volume name
size_gb: int # Size in GB
--type: string = "local" # Volume type
--mount-path: string = "" # Mount path
--readonly = false # Read-only
]: record {
"""
Create a storage volume for VM or container.
Supports local, NFS, CIFS, and cloud storage types.
"""
if ($name | is-empty) {
return {success: false, error: "Volume name required"}
}
if ($size_gb < 1 or $size_gb > 1000) {
return {success: false, error: "Size must be 1-1000 GB"}
}
let volume_dir = (get-volumes-directory)
bash -c $"mkdir -p ($volume_dir)" | complete
let volume_meta = {
name: $name
type: $type
size_gb: $size_gb
mount_path: $mount_path
readonly: $readonly
created_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
status: "created"
path: $"($volume_dir)/($name).img"
}
# Create backing file (no try-catch)
let create_result = (do { bash -c $"qemu-img create -f qcow2 ($volume_meta.path) ($size_gb)G" } | complete)
if $create_result.exit_code != 0 {
return {success: false, error: $"Failed to create volume: ($create_result.stderr)"}
}
# Save metadata (no try-catch)
let mkdir_result = (do { bash -c $"mkdir -p ($volume_dir)/meta" } | complete)
if $mkdir_result.exit_code != 0 {
return {success: false, error: $"Failed to create metadata directory: ($mkdir_result.stderr)"}
}
let save_result = (do { bash -c $"cat > ($volume_dir)/meta/($name).json << 'EOF'\n($volume_meta | to json)\nEOF" } | complete)
if $save_result.exit_code != 0 {
return {success: false, error: $"Failed to save metadata: ($save_result.stderr)"}
}
{
success: true
volume_name: $name
volume_path: $volume_meta.path
size_gb: $size_gb
mount_path: $mount_path
}
}
export def "volume-list" []: table {
"""
List all available volumes.
"""
let volume_dir = (get-volumes-directory)
if (not ($volume_dir | path exists)) {
return []
}
bash -c $"ls -1 ($volume_dir)/meta/*.json 2>/dev/null"
| lines
| each {|file|
# Guard: Check if file can be opened and parsed as JSON (no try-catch)
let json_result = (do { open $file | from json } | complete)
if $json_result.exit_code == 0 {
let meta = ($json_result.stdout)
{
name: $meta.name
type: $meta.type
size_gb: $meta.size_gb
mount_path: $meta.mount_path
status: $meta.status
created: $meta.created_at
}
} else {
null
}
}
| compact
}
export def "volume-info" [
name: string # Volume name
]: record {
"""
Get detailed volume information.
"""
let volume_dir = (get-volumes-directory)
let meta_file = $"($volume_dir)/meta/($name).json"
if (not ($meta_file | path exists)) {
return {success: false, error: "Volume not found"}
}
# Load metadata (no try-catch)
let meta_result = (do { open $meta_file | from json } | complete)
if $meta_result.exit_code != 0 {
return {success: false, error: $"Failed to load volume metadata: ($meta_result.stderr)"}
}
let meta = ($meta_result.stdout)
let usage = (
bash -c $"du -h ($meta.path) 2>/dev/null | cut -f1" | str trim
)
{
success: true
name: $meta.name
type: $meta.type
size_gb: $meta.size_gb
used: $usage
mount_path: $meta.mount_path
readonly: $meta.readonly
created: $meta.created_at
status: $meta.status
}
}
export def "volume-attach" [
volume_name: string # Volume name
vm_name: string # VM name
--mount-path: string = "" # Mount path
]: record {
"""
Attach volume to a VM.
"""
let volume_dir = (get-volumes-directory)
let meta_file = $"($volume_dir)/meta/($volume_name).json"
if (not ($meta_file | path exists)) {
return {success: false, error: "Volume not found"}
}
# Load metadata (no try-catch)
let meta_result = (do { open $meta_file | from json } | complete)
if $meta_result.exit_code != 0 {
return {success: false, error: $"Failed to load volume metadata: ($meta_result.stderr)"}
}
let meta = ($meta_result.stdout)
let mount = (if ($mount_path | is-empty) {$meta.mount_path} else {$mount_path})
# Record attachment (no try-catch)
let attachment = {
vm_name: $vm_name
attached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
mount_path: $mount
}
let mkdir_result = (do { bash -c $"mkdir -p ($volume_dir)/attachments" } | complete)
if $mkdir_result.exit_code != 0 {
return {success: false, error: $"Failed to create attachments directory: ($mkdir_result.stderr)"}
}
let append_result = (do { bash -c $"cat >> ($volume_dir)/attachments/($volume_name).txt << 'EOF'\n($vm_name)|($mount)\nEOF" } | complete)
if $append_result.exit_code != 0 {
return {success: false, error: $"Failed to record attachment: ($append_result.stderr)"}
}
{
success: true
volume: $volume_name
vm: $vm_name
mount_path: $mount
}
}
export def "volume-detach" [
volume_name: string # Volume name
vm_name: string # VM name
]: record {
"""
Detach volume from VM.
"""
let volume_dir = (get-volumes-directory)
let attachments_file = $"($volume_dir)/attachments/($volume_name).txt"
if (not ($attachments_file | path exists)) {
return {success: false, error: "No attachments found"}
}
# Remove attachment entry (no try-catch)
let detach_result = (do { bash -c $"grep -v ($vm_name) ($attachments_file) > ($attachments_file).tmp && mv ($attachments_file).tmp ($attachments_file)" } | complete)
if $detach_result.exit_code != 0 {
return {success: false, error: $"Failed to detach volume: ($detach_result.stderr)"}
}
{
success: true
message: $"Volume detached from VM"
}
}
export def "volume-snapshot" [
volume_name: string # Volume name
snapshot_name: string # Snapshot name
--description: string = "" # Description
]: record {
"""
Create snapshot of a volume.
"""
let volume_dir = (get-volumes-directory)
let meta_file = $"($volume_dir)/meta/($volume_name).json"
if (not ($meta_file | path exists)) {
return {success: false, error: "Volume not found"}
}
# Load metadata (no try-catch)
let meta_result = (do { open $meta_file | from json } | complete)
if $meta_result.exit_code != 0 {
return {success: false, error: $"Failed to load volume metadata: ($meta_result.stderr)"}
}
let meta = ($meta_result.stdout)
let snapshot_path = $"($volume_dir)/snapshots/($volume_name)/($snapshot_name).qcow2"
let mkdir_result = (do { bash -c $"mkdir -p $(dirname ($snapshot_path))" } | complete)
if $mkdir_result.exit_code != 0 {
return {success: false, error: $"Failed to create snapshot directory: ($mkdir_result.stderr)"}
}
# Create snapshot (no try-catch)
let snapshot_result = (do { bash -c $"qemu-img snapshot -c ($snapshot_name) ($meta.path)" } | complete)
if $snapshot_result.exit_code != 0 {
return {success: false, error: $"Failed to create snapshot: ($snapshot_result.stderr)"}
}
let convert_result = (do { bash -c $"qemu-img convert -f qcow2 -O qcow2 -o backing_file=($meta.path) ($snapshot_path)" } | complete)
if $convert_result.exit_code != 0 {
return {success: false, error: $"Failed to convert snapshot: ($convert_result.stderr)"}
}
# Save snapshot metadata (no try-catch)
let snapshot_meta = {
name: $snapshot_name
volume: $volume_name
path: $snapshot_path
created_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
description: $description
}
let meta_mkdir_result = (do { bash -c $"mkdir -p ($volume_dir)/snapshots/($volume_name)" } | complete)
if $meta_mkdir_result.exit_code != 0 {
return {success: false, error: $"Failed to create snapshot metadata directory: ($meta_mkdir_result.stderr)"}
}
let meta_save_result = (do { bash -c $"cat > ($volume_dir)/snapshots/($volume_name)/($snapshot_name).json << 'EOF'\n($snapshot_meta | to json)\nEOF" } | complete)
if $meta_save_result.exit_code != 0 {
return {success: false, error: $"Failed to save snapshot metadata: ($meta_save_result.stderr)"}
}
{
success: true
snapshot: $snapshot_name
volume: $volume_name
path: $snapshot_path
}
}
export def "volume-restore" [
volume_name: string # Volume name
snapshot_name: string # Snapshot name
]: record {
"""
Restore volume from snapshot.
"""
let volume_dir = (get-volumes-directory)
let snapshot_dir = $"($volume_dir)/snapshots/($volume_name)"
let snapshot_meta_file = $"($snapshot_dir)/($snapshot_name).json"
if (not ($snapshot_meta_file | path exists)) {
return {success: false, error: "Snapshot not found"}
}
# Load snapshot metadata (no try-catch)
let snap_result = (do { open $snapshot_meta_file | from json } | complete)
if $snap_result.exit_code != 0 {
return {success: false, error: $"Failed to load snapshot metadata: ($snap_result.stderr)"}
}
let snapshot_meta = ($snap_result.stdout)
let meta_file = $"($volume_dir)/meta/($volume_name).json"
# Load volume metadata (no try-catch)
let meta_result = (do { open $meta_file | from json } | complete)
if $meta_result.exit_code != 0 {
return {success: false, error: $"Failed to load volume metadata: ($meta_result.stderr)"}
}
let meta = ($meta_result.stdout)
# Restore from snapshot (no try-catch)
let restore_result = (do { bash -c $"qemu-img snapshot -a ($snapshot_name) ($meta.path)" } | complete)
if $restore_result.exit_code != 0 {
return {success: false, error: $"Failed to restore snapshot: ($restore_result.stderr)"}
}
{
success: true
message: $"Volume restored from snapshot"
volume: $volume_name
snapshot: $snapshot_name
}
}
export def "volume-delete" [
name: string # Volume name
--force = false # Force delete if in use
]: record {
"""
Delete a volume.
"""
let volume_dir = (get-volumes-directory)
let meta_file = $"($volume_dir)/meta/($name).json"
if (not ($meta_file | path exists)) {
return {success: false, error: "Volume not found"}
}
# Load metadata (no try-catch)
let meta_result = (do { open $meta_file | from json } | complete)
if $meta_result.exit_code != 0 {
return {success: false, error: $"Failed to load volume metadata: ($meta_result.stderr)"}
}
let meta = ($meta_result.stdout)
# Check if in use (no try-catch)
let attachments_file = $"($volume_dir)/attachments/($name).txt"
if (($attachments_file | path exists) and (not $force)) {
let count_result = (do { bash -c $"wc -l < ($attachments_file)" } | complete)
if $count_result.exit_code == 0 {
let count = ($count_result.stdout | str trim | into int)
return {
success: false
error: $"Volume in use by ($count) VM(s)"
}
}
}
# Delete files (no try-catch)
let rm_img_result = (do { bash -c $"rm -f ($meta.path)" } | complete)
if $rm_img_result.exit_code != 0 {
return {success: false, error: $"Failed to delete volume image: ($rm_img_result.stderr)"}
}
let rm_meta_result = (do { bash -c $"rm -f ($meta_file)" } | complete)
if $rm_meta_result.exit_code != 0 {
return {success: false, error: $"Failed to delete metadata file: ($rm_meta_result.stderr)"}
}
let rm_snapshots_result = (do { bash -c $"rm -rf ($volume_dir)/snapshots/($name)" } | complete)
if $rm_snapshots_result.exit_code != 0 {
return {success: false, error: $"Failed to delete snapshots: ($rm_snapshots_result.stderr)"}
}
let rm_attachments_result = (do { bash -c $"rm -f ($attachments_file)" } | complete)
if $rm_attachments_result.exit_code != 0 {
return {success: false, error: $"Failed to delete attachments: ($rm_attachments_result.stderr)"}
}
{
success: true
message: $"Volume deleted"
}
}
export def "volume-stats" []: record {
"""
Get volume system statistics.
"""
let volume_dir = (get-volumes-directory)
let volumes = (volume-list)
let total_size = (
$volumes
| each {|v| $v.size_gb}
| math sum
)
let total_used = (
bash -c $"du -sb ($volume_dir) 2>/dev/null | cut -f1"
| into int
| math round -p 2
) / 1024 / 1024 / 1024
{
total_volumes: ($volumes | length)
total_size_gb: $total_size
total_used_gb: $total_used
utilization_percent: (($total_used / $total_size * 100) | math round -p 1)
}
}
def get-volumes-directory []: string {
"""Get volumes directory path"""
"{{paths.workspace}}/vms/volumes"
}