/* 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() }