Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
301
pkg/storage/storage.go
Normal file
301
pkg/storage/storage.go
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
Volt Storage - Git-attached persistent storage
|
||||
|
||||
Features:
|
||||
- Git repositories for persistence
|
||||
- Shared storage across VMs
|
||||
- Copy-on-write overlays
|
||||
- Snapshot/restore via git
|
||||
- Multi-developer collaboration
|
||||
*/
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AttachedStorage represents storage attached to a VM
|
||||
type AttachedStorage struct {
|
||||
Name string
|
||||
Source string // Host path or git URL
|
||||
Target string // Mount point inside VM
|
||||
Type string // git, bind, overlay
|
||||
ReadOnly bool
|
||||
GitBranch string
|
||||
GitRemote string
|
||||
}
|
||||
|
||||
// Manager handles storage operations
|
||||
type Manager struct {
|
||||
baseDir string
|
||||
cacheDir string
|
||||
overlayDir string
|
||||
}
|
||||
|
||||
// NewManager creates a new storage manager
|
||||
func NewManager(baseDir string) *Manager {
|
||||
return &Manager{
|
||||
baseDir: baseDir,
|
||||
cacheDir: filepath.Join(baseDir, "cache"),
|
||||
overlayDir: filepath.Join(baseDir, "overlays"),
|
||||
}
|
||||
}
|
||||
|
||||
// Setup initializes storage directories
|
||||
func (m *Manager) Setup() error {
|
||||
dirs := []string{m.baseDir, m.cacheDir, m.overlayDir}
|
||||
for _, dir := range dirs {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachGit clones or updates a git repository for VM use
|
||||
func (m *Manager) AttachGit(vmName string, gitURL string, branch string) (*AttachedStorage, error) {
|
||||
// Determine local path for this repo
|
||||
repoName := filepath.Base(strings.TrimSuffix(gitURL, ".git"))
|
||||
localPath := filepath.Join(m.cacheDir, "git", repoName)
|
||||
|
||||
// Clone or fetch
|
||||
if _, err := os.Stat(filepath.Join(localPath, ".git")); os.IsNotExist(err) {
|
||||
// Clone
|
||||
fmt.Printf("Cloning %s...\n", gitURL)
|
||||
cmd := exec.Command("git", "clone", "--depth=1", "-b", branch, gitURL, localPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("git clone failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Fetch latest
|
||||
fmt.Printf("Fetching latest from %s...\n", gitURL)
|
||||
cmd := exec.Command("git", "-C", localPath, "fetch", "--depth=1", "origin", branch)
|
||||
cmd.Run() // Ignore errors for offline operation
|
||||
|
||||
cmd = exec.Command("git", "-C", localPath, "checkout", branch)
|
||||
cmd.Run()
|
||||
}
|
||||
|
||||
// Create overlay for this VM (copy-on-write)
|
||||
overlayPath := filepath.Join(m.overlayDir, vmName, repoName)
|
||||
upperDir := filepath.Join(overlayPath, "upper")
|
||||
workDir := filepath.Join(overlayPath, "work")
|
||||
mergedDir := filepath.Join(overlayPath, "merged")
|
||||
|
||||
for _, dir := range []string{upperDir, workDir, mergedDir} {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
// Mount overlay
|
||||
mountCmd := exec.Command("mount", "-t", "overlay", "overlay",
|
||||
"-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", localPath, upperDir, workDir),
|
||||
mergedDir)
|
||||
|
||||
if err := mountCmd.Run(); err != nil {
|
||||
// Fallback: just use the local path directly
|
||||
mergedDir = localPath
|
||||
}
|
||||
|
||||
return &AttachedStorage{
|
||||
Name: repoName,
|
||||
Source: gitURL,
|
||||
Target: filepath.Join("/mnt", repoName),
|
||||
Type: "git",
|
||||
GitBranch: branch,
|
||||
GitRemote: "origin",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AttachBind creates a bind mount from host to VM
|
||||
func (m *Manager) AttachBind(vmName, hostPath, vmPath string, readOnly bool) (*AttachedStorage, error) {
|
||||
// Verify source exists
|
||||
if _, err := os.Stat(hostPath); err != nil {
|
||||
return nil, fmt.Errorf("source path does not exist: %s", hostPath)
|
||||
}
|
||||
|
||||
return &AttachedStorage{
|
||||
Name: filepath.Base(hostPath),
|
||||
Source: hostPath,
|
||||
Target: vmPath,
|
||||
Type: "bind",
|
||||
ReadOnly: readOnly,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateOverlay creates a copy-on-write overlay
|
||||
func (m *Manager) CreateOverlay(vmName, basePath, vmPath string) (*AttachedStorage, error) {
|
||||
overlayPath := filepath.Join(m.overlayDir, vmName, filepath.Base(basePath))
|
||||
upperDir := filepath.Join(overlayPath, "upper")
|
||||
workDir := filepath.Join(overlayPath, "work")
|
||||
mergedDir := filepath.Join(overlayPath, "merged")
|
||||
|
||||
for _, dir := range []string{upperDir, workDir, mergedDir} {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create overlay dir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &AttachedStorage{
|
||||
Name: filepath.Base(basePath),
|
||||
Source: basePath,
|
||||
Target: vmPath,
|
||||
Type: "overlay",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Snapshot creates a git commit of VM changes
|
||||
func (m *Manager) Snapshot(vmName, storageName, message string) error {
|
||||
overlayPath := filepath.Join(m.overlayDir, vmName, storageName, "upper")
|
||||
|
||||
// Check if there are changes
|
||||
if _, err := os.Stat(overlayPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("no overlay found for %s/%s", vmName, storageName)
|
||||
}
|
||||
|
||||
// Create snapshot directory
|
||||
snapshotDir := filepath.Join(m.baseDir, "snapshots", vmName, storageName)
|
||||
os.MkdirAll(snapshotDir, 0755)
|
||||
|
||||
// Initialize git if needed
|
||||
gitDir := filepath.Join(snapshotDir, ".git")
|
||||
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
|
||||
exec.Command("git", "-C", snapshotDir, "init").Run()
|
||||
exec.Command("git", "-C", snapshotDir, "config", "user.email", "volt@localhost").Run()
|
||||
exec.Command("git", "-C", snapshotDir, "config", "user.name", "Volt").Run()
|
||||
}
|
||||
|
||||
// Copy changes to snapshot dir
|
||||
exec.Command("rsync", "-a", "--delete", overlayPath+"/", snapshotDir+"/").Run()
|
||||
|
||||
// Commit
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("Snapshot at %s", timestamp)
|
||||
}
|
||||
|
||||
exec.Command("git", "-C", snapshotDir, "add", "-A").Run()
|
||||
exec.Command("git", "-C", snapshotDir, "commit", "-m", message).Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore restores VM storage from a snapshot
|
||||
func (m *Manager) Restore(vmName, storageName, commitHash string) error {
|
||||
snapshotDir := filepath.Join(m.baseDir, "snapshots", vmName, storageName)
|
||||
overlayUpper := filepath.Join(m.overlayDir, vmName, storageName, "upper")
|
||||
|
||||
// Checkout specific commit
|
||||
if commitHash != "" {
|
||||
exec.Command("git", "-C", snapshotDir, "checkout", commitHash).Run()
|
||||
}
|
||||
|
||||
// Restore to overlay upper
|
||||
os.RemoveAll(overlayUpper)
|
||||
os.MkdirAll(overlayUpper, 0755)
|
||||
exec.Command("rsync", "-a", snapshotDir+"/", overlayUpper+"/").Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSnapshots returns available snapshots for a storage
|
||||
func (m *Manager) ListSnapshots(vmName, storageName string) ([]Snapshot, error) {
|
||||
snapshotDir := filepath.Join(m.baseDir, "snapshots", vmName, storageName)
|
||||
|
||||
// Get git log
|
||||
out, err := exec.Command("git", "-C", snapshotDir, "log", "--oneline", "-20").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list snapshots: %w", err)
|
||||
}
|
||||
|
||||
var snapshots []Snapshot
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
snapshots = append(snapshots, Snapshot{
|
||||
Hash: parts[0],
|
||||
Message: parts[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
// Unmount unmounts all storage for a VM
|
||||
func (m *Manager) Unmount(vmName string) error {
|
||||
vmOverlayDir := filepath.Join(m.overlayDir, vmName)
|
||||
|
||||
// Find and unmount all merged directories
|
||||
entries, err := os.ReadDir(vmOverlayDir)
|
||||
if err != nil {
|
||||
return nil // Nothing to unmount
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
mergedDir := filepath.Join(vmOverlayDir, entry.Name(), "merged")
|
||||
exec.Command("umount", mergedDir).Run()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup removes all storage for a VM
|
||||
func (m *Manager) Cleanup(vmName string) error {
|
||||
m.Unmount(vmName)
|
||||
|
||||
// Remove overlay directory
|
||||
overlayPath := filepath.Join(m.overlayDir, vmName)
|
||||
os.RemoveAll(overlayPath)
|
||||
|
||||
// Keep snapshots (can be manually cleaned)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot represents a storage snapshot
|
||||
type Snapshot struct {
|
||||
Hash string
|
||||
Message string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// MountEntry generates fstab entry for storage
|
||||
func (s *AttachedStorage) MountEntry() string {
|
||||
opts := "defaults"
|
||||
if s.ReadOnly {
|
||||
opts += ",ro"
|
||||
}
|
||||
|
||||
switch s.Type {
|
||||
case "bind":
|
||||
return fmt.Sprintf("%s %s none bind,%s 0 0", s.Source, s.Target, opts)
|
||||
case "overlay":
|
||||
return fmt.Sprintf("overlay %s overlay %s 0 0", s.Target, opts)
|
||||
default:
|
||||
return fmt.Sprintf("%s %s auto %s 0 0", s.Source, s.Target, opts)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncToRemote pushes changes to git remote
|
||||
func (m *Manager) SyncToRemote(vmName, storageName string) error {
|
||||
snapshotDir := filepath.Join(m.baseDir, "snapshots", vmName, storageName)
|
||||
return exec.Command("git", "-C", snapshotDir, "push", "origin", "HEAD").Run()
|
||||
}
|
||||
|
||||
// SyncFromRemote pulls changes from git remote
|
||||
func (m *Manager) SyncFromRemote(vmName, storageName string) error {
|
||||
snapshotDir := filepath.Join(m.baseDir, "snapshots", vmName, storageName)
|
||||
return exec.Command("git", "-C", snapshotDir, "pull", "origin", "HEAD").Run()
|
||||
}
|
||||
Reference in New Issue
Block a user