diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a6abcc --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +APP=node-agent +PKG=./... + +.PHONY: build fmt tidy test + +build: + go build -o bin/$(APP) ./cmd/node-agent + +fmt: + go fmt $(PKG) + +tidy: + go mod tidy + +test: + go test $(PKG) diff --git a/pkg/api/handlers.go b/pkg/api/handlers.go index 02e8410..92b9af9 100644 --- a/pkg/api/handlers.go +++ b/pkg/api/handlers.go @@ -140,6 +140,29 @@ func handleDeleteVM(cfg config.Config, svc Services) http.HandlerFunc { func lifecycleVM(cfg config.Config, svc Services, action string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") + // Ensure VM exists either in store or runtime + spec, specErr := svc.Store.LoadVM(id) + runtimeExists := false + if specErr != nil { + if vms, err := svc.Libvirt.ListVMs(); err == nil { + for _, vm := range vms { + if vm.ID == id || vm.Name == id { + runtimeExists = true + break + } + } + } + } + if specErr != nil && !runtimeExists { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "vm not found"}) + return + } + if specErr == nil { + if err := validators.CheckStoragePoolsVM(spec.Disks, 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) defer unlock() @@ -259,6 +282,28 @@ func handleDeleteCT(cfg config.Config, svc Services) http.HandlerFunc { func lifecycleCT(cfg config.Config, svc Services, action string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") + spec, specErr := svc.Store.LoadCT(id) + runtimeExists := false + if specErr != nil { + if cts, err := svc.LXC.List(); err == nil { + for _, ct := range cts { + if ct.ID == id || ct.Name == id { + runtimeExists = true + break + } + } + } + } + if specErr != nil && !runtimeExists { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "ct not found"}) + return + } + if specErr == nil { + if err := validators.CheckStoragePoolsCT(spec, cfg); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) + return + } + } taskID := enqueueWork(svc.Tasks, "ct."+action, func(ctx context.Context) (interface{}, error) { unlock := svc.StoreLock(id) defer unlock() diff --git a/pkg/compute/libvirt/virsh_client.go b/pkg/compute/libvirt/virsh_client.go index 6427514..5d3806e 100644 --- a/pkg/compute/libvirt/virsh_client.go +++ b/pkg/compute/libvirt/virsh_client.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "io/fs" "os" "os/exec" "path" @@ -293,7 +292,7 @@ func buildIsoPureGo(outPath, userDataPath, metaDataPath string) error { dirRec := buildDirRecord(0, 0, 0, true) // current dir dirRec = append(dirRec, buildDirRecord(0, 0, 0, true)...) // parent (same) for _, e := range entries { - dirRec = append(dirRec, buildDirRecord(byteLen(e.name), e.start, e.size, false, e.name)...) + dirRec = append(dirRec, buildDirRecord(byte(len(e.name)), e.start, e.size, false, e.name)...) } dirRec = padTo(dirRec, sectorSize) if err := writeAt(f, rootSector*int64(sectorSize), dirRec); err != nil { diff --git a/pkg/state/store_test.go b/pkg/state/store_test.go new file mode 100644 index 0000000..0e25f71 --- /dev/null +++ b/pkg/state/store_test.go @@ -0,0 +1,10 @@ +package state + +import "testing" + +func TestTrimExt(t *testing.T) { + got := trimExt("foo.yaml") + if got != "foo" { + t.Fatalf("expected foo, got %s", got) + } +} diff --git a/pkg/validators/validators_test.go b/pkg/validators/validators_test.go new file mode 100644 index 0000000..d1f45b6 --- /dev/null +++ b/pkg/validators/validators_test.go @@ -0,0 +1,20 @@ +package validators + +import ( + "testing" + + "jagacloud/node-agent/pkg/compute/libvirt" + "jagacloud/node-agent/pkg/config" +) + +func TestPoolExists(t *testing.T) { + cfg := config.Config{StoragePools: []config.StoragePool{{Name: "local"}}} + err := CheckStoragePoolsVM([]libvirt.DiskSpec{{Pool: "local"}}, cfg) + if err != nil { + t.Fatalf("expected pool to be valid: %v", err) + } + err = CheckStoragePoolsVM([]libvirt.DiskSpec{{Pool: "missing"}}, cfg) + if err == nil { + t.Fatalf("expected error for missing pool") + } +} diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 0000000..9da42c4 --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +API="${API:-http://127.0.0.1:8000/api/v1}" +TOKEN="${TOKEN:-changeme}" +HDR=(-H "Authorization: Bearer ${TOKEN}") + +vm_id=${VM_ID:-smoke-vm} +ct_id=${CT_ID:-smoke-ct} + +create_vm() { + curl -sfS "${HDR[@]}" -X POST "${API}/vms" \ + -H 'Content-Type: application/json' \ + -d "{\"id\":\"${vm_id}\",\"name\":\"${vm_id}\",\"cpus\":1,\"memory_mb\":512,\"disks\":[{\"name\":\"root\",\"size_gb\":1,\"pool\":\"local\"}],\"nics\":[{\"bridge\":\"vmbr0\"}]}" +} + +create_ct() { + curl -sfS "${HDR[@]}" -X POST "${API}/containers" \ + -H 'Content-Type: application/json' \ + -d "{\"id\":\"${ct_id}\",\"name\":\"${ct_id}\",\"template\":\"debian-bookworm\",\"rootfs_pool\":\"local\",\"rootfs_size_g\":1,\"nics\":[{\"bridge\":\"vmbr0\"}],\"limits\":{\"cpus\":1,\"memory_mb\":256},\"unprivileged\":true}" +} + +cmd=${1:-} +case "$cmd" in + create-vm) create_vm ;; + create-ct) create_ct ;; + list) + curl -sfS "${HDR[@]}" "${API}/vms" | jq . + curl -sfS "${HDR[@]}" "${API}/containers" | jq . + ;; + *) + echo "Usage: API=... TOKEN=... $0 {create-vm|create-ct|list}" >&2 + exit 1 + ;; +esac