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
302 lines
8.4 KiB
Go
302 lines
8.4 KiB
Go
/*
|
|
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()
|
|
}
|