fix commit
This commit is contained in:
parent
f98da9c152
commit
d5811679d6
11 changed files with 685 additions and 2 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
result
|
result
|
||||||
qvm
|
^qvm
|
||||||
vendor/
|
vendor/
|
||||||
|
|
|
||||||
81
cmd/qvm/clean.go
Normal file
81
cmd/qvm/clean.go
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cleanCmd = &cobra.Command{
|
||||||
|
Use: "clean",
|
||||||
|
Short: "Remove all QVM data",
|
||||||
|
Long: `Completely remove all QVM state, images, and caches.
|
||||||
|
|
||||||
|
This is a destructive operation that removes:
|
||||||
|
- Base image
|
||||||
|
- VM overlay and state
|
||||||
|
- Build caches (cargo, pnpm, sccache)
|
||||||
|
- Configuration and flake
|
||||||
|
|
||||||
|
WARNING: This cannot be undone!`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logging.Warn("This will delete ALL QVM data")
|
||||||
|
fmt.Printf(" - Data directory: %s\n", config.DataDir)
|
||||||
|
fmt.Printf(" - State directory: %s\n", config.StateDir)
|
||||||
|
fmt.Printf(" - Cache directory: %s\n", config.CacheDir)
|
||||||
|
fmt.Printf(" - Config directory: %s\n", config.ConfigDir)
|
||||||
|
fmt.Println("")
|
||||||
|
logging.Warn("This operation CANNOT be undone!")
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
fmt.Print("Are you absolutely sure? [y/N] ")
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
|
||||||
|
if response != "y" && response != "Y" {
|
||||||
|
logging.Info("Clean cancelled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if vm.IsRunning() {
|
||||||
|
logging.Info("Stopping VM...")
|
||||||
|
stopResult := vm.Stop()
|
||||||
|
if stopResult.IsError() {
|
||||||
|
logging.Error(stopResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Removing QVM data directories...")
|
||||||
|
|
||||||
|
dirs := []struct {
|
||||||
|
path string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{config.DataDir, "Data directory"},
|
||||||
|
{config.StateDir, "State directory"},
|
||||||
|
{config.CacheDir, "Cache directory"},
|
||||||
|
{config.ConfigDir, "Config directory"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if _, err := os.Stat(dir.path); err == nil {
|
||||||
|
logging.Info(fmt.Sprintf(" - Deleting: %s", dir.path))
|
||||||
|
if err := os.RemoveAll(dir.path); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to delete %s: %v", dir.name, err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("QVM cleaned successfully!")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("All QVM data has been removed from your system.")
|
||||||
|
fmt.Println("Next run of 'qvm start' will initialize everything from scratch.")
|
||||||
|
fmt.Println("")
|
||||||
|
},
|
||||||
|
}
|
||||||
38
cmd/qvm/main.go
Normal file
38
cmd/qvm/main.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "0.1.0-dev"
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "qvm",
|
||||||
|
Short: "QVM - QEMU Development VM Manager",
|
||||||
|
Long: `QVM is a lightweight CLI tool for running commands in an isolated
|
||||||
|
NixOS VM with persistent state and shared caches.
|
||||||
|
|
||||||
|
Provides VM isolation with transparent workspace mounting and shared build caches.`,
|
||||||
|
Version: version,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(startCmd)
|
||||||
|
rootCmd.AddCommand(stopCmd)
|
||||||
|
rootCmd.AddCommand(statusCmd)
|
||||||
|
rootCmd.AddCommand(sshCmd)
|
||||||
|
rootCmd.AddCommand(runCmd)
|
||||||
|
rootCmd.AddCommand(rebuildCmd)
|
||||||
|
rootCmd.AddCommand(resetCmd)
|
||||||
|
rootCmd.AddCommand(cleanCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
177
cmd/qvm/rebuild.go
Normal file
177
cmd/qvm/rebuild.go
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultFlakePath is the path to the bundled default flake (set at build time or relative to binary)
|
||||||
|
var DefaultFlakePath string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Try to find the default flake relative to the executable
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
// When installed via Nix, the flake is at ../share/qvm/default-vm relative to bin/qvm
|
||||||
|
shareDir := filepath.Join(filepath.Dir(filepath.Dir(exe)), "share", "qvm", "default-vm")
|
||||||
|
if _, err := os.Stat(filepath.Join(shareDir, "flake.nix")); err == nil {
|
||||||
|
DefaultFlakePath = shareDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback for development: look in the project directory
|
||||||
|
if DefaultFlakePath == "" {
|
||||||
|
// Check common development paths
|
||||||
|
candidates := []string{
|
||||||
|
"flake/default-vm",
|
||||||
|
"../flake/default-vm",
|
||||||
|
filepath.Join(os.Getenv("HOME"), "projects/qvm/flake/default-vm"),
|
||||||
|
}
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if _, err := os.Stat(filepath.Join(candidate, "flake.nix")); err == nil {
|
||||||
|
DefaultFlakePath = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureUserFlake() error {
|
||||||
|
// Check if user flake already exists
|
||||||
|
if _, err := os.Stat(filepath.Join(config.UserFlake, "flake.nix")); err == nil {
|
||||||
|
return nil // Already exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to copy default flake
|
||||||
|
if DefaultFlakePath == "" {
|
||||||
|
return fmt.Errorf("default flake not found - please copy a flake to %s", config.UserFlake)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Copying default flake to user config...")
|
||||||
|
|
||||||
|
// Create the user flake directory
|
||||||
|
if err := os.MkdirAll(config.UserFlake, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create flake directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy flake.nix
|
||||||
|
srcFlake := filepath.Join(DefaultFlakePath, "flake.nix")
|
||||||
|
dstFlake := filepath.Join(config.UserFlake, "flake.nix")
|
||||||
|
if err := copyFile(srcFlake, dstFlake); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy flake.nix: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy flake.lock if it exists
|
||||||
|
srcLock := filepath.Join(DefaultFlakePath, "flake.lock")
|
||||||
|
dstLock := filepath.Join(config.UserFlake, "flake.lock")
|
||||||
|
if _, err := os.Stat(srcLock); err == nil {
|
||||||
|
if err := copyFile(srcLock, dstLock); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy flake.lock: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info(fmt.Sprintf("Default flake copied to %s", config.UserFlake))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(dst, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rebuildCmd = &cobra.Command{
|
||||||
|
Use: "rebuild",
|
||||||
|
Short: "Rebuild the base VM image",
|
||||||
|
Long: `Rebuild the base VM image from the NixOS flake.
|
||||||
|
|
||||||
|
Runs 'nix build' on ~/.config/qvm/flake and copies
|
||||||
|
the result to the base image location.
|
||||||
|
|
||||||
|
If the flake doesn't exist, the default flake will be
|
||||||
|
copied to ~/.config/qvm/flake first.
|
||||||
|
|
||||||
|
If the VM is running, you'll need to restart it to
|
||||||
|
use the new image.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logging.Info("Rebuilding base VM image...")
|
||||||
|
|
||||||
|
if err := config.EnsureDirs(); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to create directories: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure user flake exists, copy default if not
|
||||||
|
if err := ensureUserFlake(); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to ensure user flake: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Building disk image from flake...")
|
||||||
|
|
||||||
|
diskImageLink := filepath.Join(config.StateDir, "disk-image-result")
|
||||||
|
nixCmd := exec.Command("nix", "build", config.UserFlake+"#default", "--out-link", diskImageLink, "-L")
|
||||||
|
nixCmd.Stdout = os.Stdout
|
||||||
|
nixCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := nixCmd.Run(); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to build disk image: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Copying qcow2 image to base image location...")
|
||||||
|
|
||||||
|
qcowSource := filepath.Join(diskImageLink, "nixos.qcow2")
|
||||||
|
if _, err := os.Stat(qcowSource); os.IsNotExist(err) {
|
||||||
|
logging.Error(fmt.Sprintf("Built image missing nixos.qcow2 at %s", qcowSource))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(config.BaseImage)
|
||||||
|
|
||||||
|
cpCmd := exec.Command("cp", qcowSource, config.BaseImage)
|
||||||
|
if err := cpCmd.Run(); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to copy base image: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Base disk image built successfully")
|
||||||
|
|
||||||
|
logging.Info("Building VM runner script from flake...")
|
||||||
|
|
||||||
|
vmRunnerLink := filepath.Join(config.StateDir, "vm-runner-result")
|
||||||
|
nixCmd2 := exec.Command("nix", "build", config.UserFlake+"#vm", "--out-link", vmRunnerLink, "-L")
|
||||||
|
nixCmd2.Stdout = os.Stdout
|
||||||
|
nixCmd2.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := nixCmd2.Run(); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to build VM runner: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(config.VMRunner)
|
||||||
|
if err := os.Symlink(vmRunnerLink, config.VMRunner); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to create VM runner symlink: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("VM runner script built successfully")
|
||||||
|
logging.Info("Rebuild complete!")
|
||||||
|
|
||||||
|
if vm.IsRunning() {
|
||||||
|
logging.Warn("VM is currently running")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("The new base image will only take effect after restarting the VM:")
|
||||||
|
fmt.Println(" qvm stop")
|
||||||
|
fmt.Println(" qvm start")
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
56
cmd/qvm/reset.go
Normal file
56
cmd/qvm/reset.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resetCmd = &cobra.Command{
|
||||||
|
Use: "reset",
|
||||||
|
Short: "Reset VM to fresh state",
|
||||||
|
Long: `Delete the VM overlay and workspace registry.
|
||||||
|
|
||||||
|
This returns the VM to a clean state based on the base image.
|
||||||
|
The base image itself is not deleted.
|
||||||
|
|
||||||
|
All cache directories are preserved.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logging.Info("Resetting VM state...")
|
||||||
|
|
||||||
|
if vm.IsRunning() {
|
||||||
|
logging.Info("Stopping VM...")
|
||||||
|
stopResult := vm.Stop()
|
||||||
|
if stopResult.IsError() {
|
||||||
|
logging.Error(stopResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.Overlay); err == nil {
|
||||||
|
logging.Info(fmt.Sprintf("Deleting overlay: %s", config.Overlay))
|
||||||
|
if err := os.Remove(config.Overlay); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to delete overlay: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.WorkspacesFile); err == nil {
|
||||||
|
logging.Info(fmt.Sprintf("Deleting workspaces registry: %s", config.WorkspacesFile))
|
||||||
|
if err := os.Remove(config.WorkspacesFile); err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to delete workspaces: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Reset complete!")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("Next steps:")
|
||||||
|
fmt.Println(" - Run 'qvm start' to boot the VM with a fresh overlay")
|
||||||
|
fmt.Println("")
|
||||||
|
},
|
||||||
|
}
|
||||||
111
cmd/qvm/run.go
Normal file
111
cmd/qvm/run.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
"qvm/internal/workspace"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run <command> [args...]",
|
||||||
|
Short: "Run a command in the VM workspace",
|
||||||
|
Long: `Execute a command in the VM at the current directory.
|
||||||
|
|
||||||
|
The current directory is automatically registered as a workspace
|
||||||
|
and mounted into the VM. The command runs in the mounted workspace.`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(fmt.Sprintf("Failed to get current directory: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
regResult := workspace.Load(config.WorkspacesFile)
|
||||||
|
if regResult.IsError() {
|
||||||
|
logging.Error(regResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
reg := regResult.MustGet()
|
||||||
|
|
||||||
|
wsResult := reg.Register(cwd)
|
||||||
|
if wsResult.IsError() {
|
||||||
|
logging.Error(wsResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ws := wsResult.MustGet()
|
||||||
|
|
||||||
|
saveResult := reg.Save()
|
||||||
|
if saveResult.IsError() {
|
||||||
|
logging.Error(saveResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasRunning := vm.IsRunning()
|
||||||
|
|
||||||
|
if !wasRunning {
|
||||||
|
logging.Info("VM is not running, starting it...")
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
startResult := vm.Start(cfg, reg)
|
||||||
|
if startResult.IsError() {
|
||||||
|
logging.Error(startResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusResult := vm.Status()
|
||||||
|
if statusResult.IsError() {
|
||||||
|
logging.Error(statusResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
status := statusResult.MustGet()
|
||||||
|
|
||||||
|
remoteCmd := fmt.Sprintf("mkdir -p '%s' && mount -t 9p ws_%s '%s' -o trans=virtio,version=9p2000.L,msize=104857600 2>/dev/null || true && cd '%s' && %s",
|
||||||
|
ws.GuestPath, ws.Hash, ws.GuestPath, ws.GuestPath, strings.Join(args, " "))
|
||||||
|
|
||||||
|
sshArgs := []string{
|
||||||
|
"-p", "root",
|
||||||
|
"ssh",
|
||||||
|
"-o", "StrictHostKeyChecking=no",
|
||||||
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
|
"-o", "LogLevel=ERROR",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate TTY for interactive commands
|
||||||
|
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
|
sshArgs = append(sshArgs, "-t")
|
||||||
|
}
|
||||||
|
|
||||||
|
sshArgs = append(sshArgs,
|
||||||
|
"-p", strconv.Itoa(status.SSHPort),
|
||||||
|
"root@localhost",
|
||||||
|
remoteCmd,
|
||||||
|
)
|
||||||
|
|
||||||
|
sshpassCmd := exec.Command("sshpass", sshArgs...)
|
||||||
|
sshpassCmd.Stdin = os.Stdin
|
||||||
|
sshpassCmd.Stdout = os.Stdout
|
||||||
|
sshpassCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := sshpassCmd.Run(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
os.Exit(exitErr.ExitCode())
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
86
cmd/qvm/ssh.go
Normal file
86
cmd/qvm/ssh.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
"qvm/internal/workspace"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sshCommand string
|
||||||
|
)
|
||||||
|
|
||||||
|
var sshCmd = &cobra.Command{
|
||||||
|
Use: "ssh",
|
||||||
|
Short: "SSH into the VM",
|
||||||
|
Long: `Open an SSH session to the running VM.
|
||||||
|
|
||||||
|
By default opens an interactive shell.
|
||||||
|
Use -c to run a single command.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if !vm.IsRunning() {
|
||||||
|
logging.Info("VM is not running, starting it...")
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
regResult := workspace.Load(config.WorkspacesFile)
|
||||||
|
if regResult.IsError() {
|
||||||
|
logging.Error(regResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
reg := regResult.MustGet()
|
||||||
|
|
||||||
|
startResult := vm.Start(cfg, reg)
|
||||||
|
if startResult.IsError() {
|
||||||
|
logging.Error(startResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusResult := vm.Status()
|
||||||
|
if statusResult.IsError() {
|
||||||
|
logging.Error(statusResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
status := statusResult.MustGet()
|
||||||
|
|
||||||
|
sshArgs := []string{
|
||||||
|
"-p", "root",
|
||||||
|
"ssh",
|
||||||
|
"-o", "StrictHostKeyChecking=no",
|
||||||
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
|
"-o", "LogLevel=ERROR",
|
||||||
|
"-p", strconv.Itoa(status.SSHPort),
|
||||||
|
"root@localhost",
|
||||||
|
}
|
||||||
|
|
||||||
|
if sshCommand != "" {
|
||||||
|
sshArgs = append(sshArgs, sshCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshpassCmd := exec.Command("sshpass", sshArgs...)
|
||||||
|
sshpassCmd.Stdin = os.Stdin
|
||||||
|
sshpassCmd.Stdout = os.Stdout
|
||||||
|
sshpassCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := sshpassCmd.Run(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
os.Exit(exitErr.ExitCode())
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sshCmd.Flags().StringVarP(&sshCommand, "command", "c", "", "Command to run instead of interactive shell")
|
||||||
|
}
|
||||||
45
cmd/qvm/start.go
Normal file
45
cmd/qvm/start.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
"qvm/internal/workspace"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var startCmd = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Start the VM",
|
||||||
|
Long: `Start the QVM virtual machine.
|
||||||
|
|
||||||
|
Creates base image and overlay if they don't exist.
|
||||||
|
Mounts all registered workspaces and cache directories.
|
||||||
|
Waits for SSH to become available.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
regResult := workspace.Load(config.WorkspacesFile)
|
||||||
|
if regResult.IsError() {
|
||||||
|
logging.Error(regResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
reg := regResult.MustGet()
|
||||||
|
|
||||||
|
logging.Info("Starting VM...")
|
||||||
|
|
||||||
|
result := vm.Start(cfg, reg)
|
||||||
|
if result.IsError() {
|
||||||
|
logging.Error(result.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("VM started successfully")
|
||||||
|
},
|
||||||
|
}
|
||||||
54
cmd/qvm/status.go
Normal file
54
cmd/qvm/status.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"qvm/internal/config"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
"qvm/internal/workspace"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusCmd = &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Show VM status",
|
||||||
|
Long: `Display VM state, SSH port, and mounted workspaces.
|
||||||
|
|
||||||
|
Shows current running state, PID, SSH connection info,
|
||||||
|
and list of registered workspaces.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
statusResult := vm.Status()
|
||||||
|
if statusResult.IsError() {
|
||||||
|
logging.Error(statusResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
status := statusResult.MustGet()
|
||||||
|
|
||||||
|
if status.Running {
|
||||||
|
fmt.Printf("VM Status: Running\n")
|
||||||
|
fmt.Printf("PID: %d\n", status.PID)
|
||||||
|
fmt.Printf("SSH Port: %d\n", status.SSHPort)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("VM Status: Stopped\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
regResult := workspace.Load(config.WorkspacesFile)
|
||||||
|
if regResult.IsError() {
|
||||||
|
logging.Error(regResult.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
reg := regResult.MustGet()
|
||||||
|
|
||||||
|
workspaces := reg.List()
|
||||||
|
if len(workspaces) > 0 {
|
||||||
|
fmt.Printf("\nMounted Workspaces:\n")
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
fmt.Printf(" %s -> %s\n", ws.Hash, ws.HostPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\nMounted Workspaces:\n (none)\n")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
34
cmd/qvm/stop.go
Normal file
34
cmd/qvm/stop.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"qvm/internal/logging"
|
||||||
|
"qvm/internal/vm"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stopCmd = &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Stop the VM",
|
||||||
|
Long: `Gracefully shut down the running VM.
|
||||||
|
|
||||||
|
Sends ACPI shutdown signal and waits up to 30 seconds.
|
||||||
|
Force kills if timeout is exceeded.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if !vm.IsRunning() {
|
||||||
|
logging.Info("VM is not running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Stopping VM...")
|
||||||
|
|
||||||
|
result := vm.Stop()
|
||||||
|
if result.IsError() {
|
||||||
|
logging.Error(result.Error().Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("VM stopped successfully")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,8 @@
|
||||||
# For development, use: go build ./cmd/qvm
|
# For development, use: go build ./cmd/qvm
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
vendorHash = "sha256-d6Z32nPDawwFqhKfVw/QwHUuDuMuTdQdHApmxcXzFng=";
|
# vendorHash = pkgs.lib.fakeHash;
|
||||||
|
vendorHash = "sha256-G/L3vzOampQK6vB12DdotBB/T8ojNkkrIy+8tQgQTI4=";
|
||||||
|
|
||||||
subPackages = [ "cmd/qvm" ];
|
subPackages = [ "cmd/qvm" ];
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue