diff --git a/pkg/api/handlers.go b/pkg/api/handlers.go index 3a4e336..86e5108 100644 --- a/pkg/api/handlers.go +++ b/pkg/api/handlers.go @@ -90,13 +90,13 @@ func handleCreateVM(cfg config.Config, svc Services) http.HandlerFunc { return } // Resolve disk paths for dir pools - for i := range spec.Disks { - if spec.Disks[i].Path == "" && spec.Disks[i].Pool != "" { - if path, err := storage.ResolveVolume(toPoolConfigs(cfg.StoragePools), spec.Disks[i].Pool, spec.Disks[i].Name+".qcow2"); err == nil { - spec.Disks[i].Path = path - } + for i := range spec.Disks { + if spec.Disks[i].Path == "" && spec.Disks[i].Pool != "" { + if path, err := storage.ResolveVolume(toPoolConfigs(cfg.StoragePools), spec.Disks[i].Pool, spec.Disks[i].Name+".qcow2"); err == nil { + spec.Disks[i].Path = path } } + } if err := svc.Store.SaveVM(spec); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return diff --git a/pkg/api/handlers_test.go b/pkg/api/handlers_test.go index 922c9cd..e836f33 100644 --- a/pkg/api/handlers_test.go +++ b/pkg/api/handlers_test.go @@ -185,6 +185,37 @@ func TestVMLifecycleStartStop(t *testing.T) { } } +func TestCTLifecycleStartStop(t *testing.T) { + tmpDir := t.TempDir() + store := state.NewStore(filepath.Join(tmpDir, "vm"), filepath.Join(tmpDir, "ct")) + cfg := config.Config{ + StoragePools: []config.StoragePool{{Name: "local"}}, + Bridges: []config.Bridge{{Name: "vmbr0"}}, + } + t.Setenv("JAGACLOUD_SKIP_BRIDGE_CHECK", "1") + svc := Services{ + Tasks: tasks.NewRegistry(), + Libvirt: &fakeLibvirt{}, + LXC: &fakeLXC{}, + Podman: &fakePodman{}, + Store: store, + } + go svc.Tasks.StartWorker(testCtx(t)) + r := chi.NewRouter() + RegisterRoutes(r, cfg, svc) + + _ = store.SaveCT(lxc.Spec{ID: "ct-run", Name: "ct-run", Template: "debian", RootfsPool: "local", RootfsSizeG: 1, Limits: lxc.Limits{CPU: 1, MemoryMB: 256}}) + + for _, action := range []string{"start", "stop"} { + req := httptest.NewRequest(http.MethodPost, "/api/v1/containers/ct-run/"+action, nil) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + if rec.Code != http.StatusAccepted { + t.Fatalf("expected 202 for %s, got %d", action, rec.Code) + } + } +} + func TestCTLifecycleDeleteCleansSpec(t *testing.T) { tmpDir := t.TempDir() store := state.NewStore(filepath.Join(tmpDir, "vm"), filepath.Join(tmpDir, "ct")) diff --git a/pkg/storage/pools.go b/pkg/storage/pools.go index 3ff3ad3..6b95fab 100644 --- a/pkg/storage/pools.go +++ b/pkg/storage/pools.go @@ -1,9 +1,12 @@ package storage import ( + "errors" "fmt" "os" + "os/exec" "path/filepath" + "strings" ) // PoolConfig describes a configured storage pool. @@ -27,7 +30,6 @@ func ResolveVolume(pools []PoolConfig, poolName, vol string) (string, error) { if p.Name != poolName { continue } - // dir pool: join path/vol if p.Type == "dir" { if p.Path == "" { return "", fmt.Errorf("dir pool %s missing path", poolName) @@ -35,8 +37,13 @@ func ResolveVolume(pools []PoolConfig, poolName, vol string) (string, error) { target := filepath.Join(p.Path, vol) return target, nil } - // TODO: lvm/zfs support - return "", fmt.Errorf("pool type %s not yet supported", p.Type) + if p.Type == "lvm" { + return fmt.Sprintf("/dev/%s/%s", p.VG, vol), nil + } + if p.Type == "zfs" { + return fmt.Sprintf("%s/%s", p.Path, vol), nil + } + return "", fmt.Errorf("pool type %s not supported", p.Type) } return "", fmt.Errorf("pool %s not found", poolName) } @@ -56,3 +63,40 @@ func PoolExists(pools []PoolConfig, name string) bool { } return false } + +// CreateVolume creates a volume in the given pool (dir or LVM). ZFS is stubbed. +func CreateVolume(pool PoolConfig, name string, sizeGB int) (string, error) { + if name == "" { + return "", errors.New("volume name required") + } + switch pool.Type { + case "dir": + if pool.Path == "" { + return "", fmt.Errorf("dir pool %s missing path", pool.Name) + } + target := filepath.Join(pool.Path, name) + if _, err := os.Stat(target); err == nil { + return target, nil + } + cmd := exec.Command("qemu-img", "create", "-f", "qcow2", target, fmt.Sprintf("%dG", sizeGB)) + if err := cmd.Run(); err != nil { + return "", err + } + return target, nil + case "lvm": + if pool.VG == "" { + return "", fmt.Errorf("lvm pool %s missing vg", pool.Name) + } + target := fmt.Sprintf("/dev/%s/%s", pool.VG, name) + args := []string{"lvcreate", "-L", fmt.Sprintf("%dG", sizeGB), "-n", name, pool.VG} + if err := exec.Command("bash", "-lc", strings.Join(args, " ")).Run(); err != nil { + return "", err + } + return target, nil + case "zfs": + // Stub: in practice use `zfs create -V pool/vol` + return "", fmt.Errorf("zfs volume creation not implemented") + default: + return "", fmt.Errorf("unsupported pool type %s", pool.Type) + } +}