add more codes
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"jagacloud/node-agent/pkg/config"
|
||||
"jagacloud/node-agent/pkg/containers/lxc"
|
||||
"jagacloud/node-agent/pkg/containers/podman"
|
||||
"jagacloud/node-agent/pkg/storage"
|
||||
"jagacloud/node-agent/pkg/tasks"
|
||||
"jagacloud/node-agent/pkg/validators"
|
||||
)
|
||||
@@ -88,6 +89,14 @@ func handleCreateVM(cfg config.Config, svc Services) http.HandlerFunc {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := svc.Store.SaveVM(spec); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
@@ -162,6 +171,12 @@ func lifecycleVM(cfg config.Config, svc Services, action string) http.HandlerFun
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// runtime-only VM: attempt pool validation via virsh and config
|
||||
if err := validators.CheckStoragePoolsRuntime([]string{}, cfg); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
taskID := enqueueWork(svc.Tasks, "vm."+action, func(ctx context.Context) (interface{}, error) {
|
||||
unlock := svc.StoreLock(id)
|
||||
@@ -234,6 +249,11 @@ func handleCreateCT(cfg config.Config, svc Services) http.HandlerFunc {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if spec.RootfsPool != "" && spec.RootfsSizeG > 0 && spec.RootfsPath == "" {
|
||||
if path, err := storage.ResolveVolume(toPoolConfigs(cfg.StoragePools), spec.RootfsPool, spec.ID+"-rootfs"); err == nil {
|
||||
spec.RootfsPath = path
|
||||
}
|
||||
}
|
||||
if err := svc.Store.SaveCT(spec); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
@@ -474,3 +494,17 @@ func validateVM(spec libvirt.VMSpec, cfg config.Config) error {
|
||||
// storage pools validated elsewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
// toPoolConfigs converts config pools to storage pool configs.
|
||||
func toPoolConfigs(p []config.StoragePool) []storage.PoolConfig {
|
||||
out := make([]storage.PoolConfig, 0, len(p))
|
||||
for _, sp := range p {
|
||||
out = append(out, storage.PoolConfig{
|
||||
Name: sp.Name,
|
||||
Type: sp.Type,
|
||||
Path: sp.Path,
|
||||
VG: sp.Path, // reuse Path for now; real config should split
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
260
pkg/api/handlers_test.go
Normal file
260
pkg/api/handlers_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"jagacloud/node-agent/pkg/compute/libvirt"
|
||||
"jagacloud/node-agent/pkg/config"
|
||||
"jagacloud/node-agent/pkg/containers/lxc"
|
||||
"jagacloud/node-agent/pkg/containers/podman"
|
||||
"jagacloud/node-agent/pkg/state"
|
||||
"jagacloud/node-agent/pkg/tasks"
|
||||
)
|
||||
|
||||
func TestVMCreateAndListUsesStore(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)
|
||||
|
||||
body := []byte(`{"id":"vm-1","name":"vm-1","cpus":1,"memory_mb":512,"disks":[{"name":"root","size_gb":1,"pool":"local"}],"nics":[{"bridge":"vmbr0"}]}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/vms", bytes.NewBuffer(body))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusAccepted {
|
||||
t.Fatalf("expected 202, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// allow worker to process
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/vms", nil)
|
||||
listRec := httptest.NewRecorder()
|
||||
r.ServeHTTP(listRec, listReq)
|
||||
if listRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", listRec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCTCreateAndListUsesStore(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)
|
||||
|
||||
body := []byte(`{"id":"ct-1","name":"ct-1","template":"debian","rootfs_pool":"local","rootfs_size_g":1,"nics":[{"bridge":"vmbr0"}],"limits":{"cpus":1,"memory_mb":256}}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/containers", bytes.NewBuffer(body))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusAccepted {
|
||||
t.Fatalf("expected 202, got %d", rec.Code)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/containers", nil)
|
||||
listRec := httptest.NewRecorder()
|
||||
r.ServeHTTP(listRec, listReq)
|
||||
if listRec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", listRec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMLifecycleMissingID(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
store := state.NewStore(filepath.Join(tmpDir, "vm"), filepath.Join(tmpDir, "ct"))
|
||||
cfg := config.Config{}
|
||||
svc := Services{
|
||||
Tasks: tasks.NewRegistry(),
|
||||
Libvirt: &fakeLibvirt{},
|
||||
LXC: &fakeLXC{},
|
||||
Podman: &fakePodman{},
|
||||
Store: store,
|
||||
}
|
||||
r := chi.NewRouter()
|
||||
RegisterRoutes(r, cfg, svc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/vms/missing/start", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMLifecycleDeleteCleansSpec(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)
|
||||
|
||||
// create spec directly
|
||||
_ = store.SaveVM(libvirt.VMSpec{ID: "vm-del", Name: "vm-del", CPU: 1, MemoryMB: 512, Disks: []libvirt.DiskSpec{{Name: "root", Pool: "local", SizeGB: 1}}})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/vms/vm-del/delete", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusAccepted {
|
||||
t.Fatalf("expected 202, got %d", rec.Code)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if _, err := store.LoadVM("vm-del"); err == nil {
|
||||
t.Fatalf("expected spec to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMLifecycleStartStop(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.SaveVM(libvirt.VMSpec{ID: "vm-run", Name: "vm-run", CPU: 1, MemoryMB: 512, Disks: []libvirt.DiskSpec{{Name: "root", Pool: "local", SizeGB: 1}}})
|
||||
|
||||
for _, action := range []string{"start", "stop"} {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/vms/vm-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"))
|
||||
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-del", Name: "ct-del", Template: "debian", RootfsPool: "local", RootfsSizeG: 1, Limits: lxc.Limits{CPU: 1, MemoryMB: 256}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/containers/ct-del/delete", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusAccepted {
|
||||
t.Fatalf("expected 202, got %d", rec.Code)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if _, err := store.LoadCT("ct-del"); err == nil {
|
||||
t.Fatalf("expected CT spec to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// testCtx returns a cancellable context for workers.
|
||||
func testCtx(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
ctx, _ := context.WithCancel(context.Background())
|
||||
return ctx
|
||||
}
|
||||
|
||||
type fakeLibvirt struct{}
|
||||
|
||||
func (f *fakeLibvirt) ListVMs() ([]libvirt.VM, error) {
|
||||
return []libvirt.VM{{ID: "vm-1", Name: "vm-1", Status: "running"}}, nil
|
||||
}
|
||||
func (f *fakeLibvirt) CreateVM(spec libvirt.VMSpec) (libvirt.VM, error) {
|
||||
return libvirt.VM{ID: spec.ID, Name: spec.Name, Status: "running"}, nil
|
||||
}
|
||||
func (f *fakeLibvirt) StartVM(id string) error { return nil }
|
||||
func (f *fakeLibvirt) StopVM(id string) error { return nil }
|
||||
func (f *fakeLibvirt) RebootVM(id string) error { return nil }
|
||||
func (f *fakeLibvirt) DeleteVM(id string) error { return nil }
|
||||
|
||||
type fakeLXC struct{}
|
||||
|
||||
func (f *fakeLXC) List() ([]lxc.Container, error) {
|
||||
return []lxc.Container{{ID: "ct-1", Name: "ct-1", Status: "running", Unpriv: true}}, nil
|
||||
}
|
||||
func (f *fakeLXC) Create(spec lxc.Spec) (lxc.Container, error) {
|
||||
return lxc.Container{ID: spec.ID, Name: spec.Name, Status: "stopped", Unpriv: spec.Unprivileged}, nil
|
||||
}
|
||||
func (f *fakeLXC) Start(id string) error { return nil }
|
||||
func (f *fakeLXC) Stop(id string) error { return nil }
|
||||
func (f *fakeLXC) Delete(id string) error { return nil }
|
||||
|
||||
type fakePodman struct{}
|
||||
|
||||
func (f *fakePodman) List(ctID string) ([]podman.OCIContainer, error) { return nil, nil }
|
||||
func (f *fakePodman) Create(ctID string, spec podman.CreateSpec) (podman.OCIContainer, error) {
|
||||
return podman.OCIContainer{ID: "oci-1", Image: spec.Image, Status: "created"}, nil
|
||||
}
|
||||
func (f *fakePodman) Start(ctID, cid string) error { return nil }
|
||||
func (f *fakePodman) Stop(ctID, cid string) error { return nil }
|
||||
func (f *fakePodman) Delete(ctID, cid string) error { return nil }
|
||||
Reference in New Issue
Block a user