diff --git a/internal/dispatcher/agent/vm_commands.go b/internal/dispatcher/agent/vm_commands.go index 6b50629..9b42289 100644 --- a/internal/dispatcher/agent/vm_commands.go +++ b/internal/dispatcher/agent/vm_commands.go @@ -20,15 +20,16 @@ type VMDisk struct { } type StartVMCommand struct { - Name string - Subnet string - IP string - Disks []VMDisk - Memory int - CPUs int - UEFI bool - Password string - SSHKey string + Name string + Subnet string + IP string + Disks []VMDisk + Memory int + CPUs int + UEFI bool + Password string + SSHKey string + Documents map[string]string } func (c StartVMCommand) Key() string { return "vm/" + c.Name } @@ -66,6 +67,11 @@ func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error { if c.SSHKey != "" { kv.AddInDB(db, "vm/"+c.Name+"/sshkey", c.SSHKey) } + for doc, content := range c.Documents { + if err := kv.AddInDB(db, "vm/"+c.Name+"/metadata/"+doc, content); err != nil { + return fmt.Errorf("store metadata %s: %w", doc, err) + } + } return nil } diff --git a/internal/dispatcher/agent/vm_commands_test.go b/internal/dispatcher/agent/vm_commands_test.go index 2747b0f..cd483f4 100644 --- a/internal/dispatcher/agent/vm_commands_test.go +++ b/internal/dispatcher/agent/vm_commands_test.go @@ -4,6 +4,7 @@ import ( "testing" "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" ) // --- StartVMCommand.Prepare : écriture des disques en DB --- @@ -173,3 +174,98 @@ func TestStartVMCommand_Prepare_Duplicate(t *testing.T) { t.Error("Prepare devrait échouer si la VM existe déjà") } } + +// --- StartVMCommand.Prepare : documents cloud-init --- + +func prepareWithDocuments(t *testing.T, docs map[string]string) *badger.DB { + t.Helper() + _, db := newTestDispatcher(t) + kv.AddInDB(db, "subnet/sn-1/state", "running") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1") + + cmd := StartVMCommand{ + Name: "vm-doc", + Subnet: "sn-1", + IP: "10.0.0.5", + Disks: []VMDisk{{Path: "/data/root.qcow2", Dev: "vda"}}, + Documents: docs, + } + if err := cmd.Prepare(db, nil); err != nil { + t.Fatalf("Prepare a échoué : %v", err) + } + return db +} + +func TestStartVMCommand_Prepare_StoresDocuments(t *testing.T) { + userData := "#cloud-config\npackages:\n - nginx\n" + db := prepareWithDocuments(t, map[string]string{"user-data": userData}) + + got, err := kv.GetFromDB(db, "vm/vm-doc/metadata/user-data") + if err != nil { + t.Fatalf("clé metadata/user-data absente : %v", err) + } + if got != userData { + t.Errorf("user-data attendu %q, obtenu %q", userData, got) + } +} + +func TestStartVMCommand_Prepare_NoDocumentsWritesNoKey(t *testing.T) { + db := prepareWithDocuments(t, nil) + + entries, err := kv.ListByPrefix(db, "vm/vm-doc/metadata/") + if err != nil { + t.Fatalf("ListByPrefix : %v", err) + } + if len(entries) != 0 { + t.Errorf("aucun document fourni, aucune clé ne doit être écrite : %v", entries) + } +} + +func TestStartVMCommand_Prepare_EmptyDocumentIsStored(t *testing.T) { + db := prepareWithDocuments(t, map[string]string{"vendor-data": ""}) + + entries, err := kv.ListByPrefix(db, "vm/vm-doc/metadata/") + if err != nil { + t.Fatalf("ListByPrefix : %v", err) + } + if _, ok := entries["vm/vm-doc/metadata/vendor-data"]; !ok { + t.Error("un document explicitement vide doit être stocké : c'est une demande de ne rien servir, pas une absence de demande") + } +} + +func TestStartVMCommand_Prepare_AllDocumentKinds(t *testing.T) { + docs := map[string]string{ + "user-data": "u", + "meta-data": "m", + "network-config": "n", + "vendor-data": "v", + } + db := prepareWithDocuments(t, docs) + + for doc, want := range docs { + got, err := kv.GetFromDB(db, "vm/vm-doc/metadata/"+doc) + if err != nil { + t.Errorf("clé metadata/%s absente : %v", doc, err) + continue + } + if got != want { + t.Errorf("%s attendu %q, obtenu %q", doc, want, got) + } + } +} + +func TestDeleteInDB_RemovesMetadataDocuments(t *testing.T) { + db := prepareWithDocuments(t, map[string]string{"user-data": "u", "vendor-data": "v"}) + + if err := kv.DeleteInDB(db, "vm/vm-doc"); err != nil { + t.Fatalf("DeleteInDB : %v", err) + } + + entries, err := kv.ListByPrefix(db, "vm/vm-doc/") + if err != nil { + t.Fatalf("ListByPrefix : %v", err) + } + if len(entries) != 0 { + t.Errorf("la suppression de la VM doit emporter ses documents : %v", entries) + } +} diff --git a/internal/vm/create.go b/internal/vm/create.go index 74ed0bb..7ddc04c 100644 --- a/internal/vm/create.go +++ b/internal/vm/create.go @@ -42,12 +42,13 @@ func StartVM(db *badger.DB, name string, cfg *configuration.Config) error { } if err := metadata.StartMetadata(metadata.NoCloudConfig{ - Name: name, - VpcName: d.vpcName, - BindIP: d.interfaceIP, - BindPort: d.metadataPort, - Password: d.password, - SSHKEY: d.sshkey, + Name: name, + VpcName: d.vpcName, + BindIP: d.interfaceIP, + BindPort: d.metadataPort, + Password: d.password, + SSHKEY: d.sshkey, + Documents: d.documents, }, cfg, false); err != nil { return fmt.Errorf("start metadata: %w", err) } diff --git a/internal/vm/data.go b/internal/vm/data.go index 3435fb1..b4b9327 100644 --- a/internal/vm/data.go +++ b/internal/vm/data.go @@ -31,6 +31,7 @@ type vmData struct { uefi bool password string sshkey string + documents map[string]string } func loadVM(db *badger.DB, name string) (vmData, error) { @@ -124,5 +125,17 @@ func loadVM(db *badger.DB, name string) (vmData, error) { d.password, _ = kv.GetFromDB(db, "vm/"+name+"/password") d.sshkey, _ = kv.GetFromDB(db, "vm/"+name+"/sshkey") + docPrefix := "vm/" + name + "/metadata/" + docEntries, err := kv.ListByPrefix(db, docPrefix) + if err != nil { + return d, fmt.Errorf("list metadata documents: %w", err) + } + if len(docEntries) > 0 { + d.documents = make(map[string]string, len(docEntries)) + for key, content := range docEntries { + d.documents[strings.TrimPrefix(key, docPrefix)] = content + } + } + return d, nil } diff --git a/internal/vm/data_test.go b/internal/vm/data_test.go new file mode 100644 index 0000000..9c04934 --- /dev/null +++ b/internal/vm/data_test.go @@ -0,0 +1,90 @@ +package vm + +import ( + "testing" + + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" +) + +func newVMInDB(t *testing.T) *badger.DB { + t.Helper() + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + kv.AddInDB(db, "vm/vm-1/subnet", "sn-000001") + kv.AddInDB(db, "subnet/sn-000001/vpc", "vp-admin") + kv.AddInDB(db, "subnet/sn-000001/interface_ip", "10.1.1.1") + kv.AddInDB(db, "subnet/sn-000001/dhcp/10.1.1.2", "00:22:33:00:01:02") + kv.AddInDB(db, "vm/vm-1/ip", "10.1.1.2") + kv.AddInDB(db, "vm/vm-1/metadata_port", "8081") + kv.AddInDB(db, "vm/vm-1/disk/vda", "/data/root.qcow2") + kv.AddInDB(db, "vm/vm-1/memory", "2048") + kv.AddInDB(db, "vm/vm-1/cpus", "2") + return db +} + +func TestLoadVM_NoDocuments(t *testing.T) { + db := newVMInDB(t) + + d, err := loadVM(db, "vm-1") + if err != nil { + t.Fatalf("loadVM : %v", err) + } + if d.documents != nil { + t.Errorf("aucun document en base, la map doit rester nil : %v", d.documents) + } +} + +func TestLoadVM_ReadsDocuments(t *testing.T) { + db := newVMInDB(t) + userData := "#cloud-config\npackages:\n - nginx\n" + kv.AddInDB(db, "vm/vm-1/metadata/user-data", userData) + kv.AddInDB(db, "vm/vm-1/metadata/network-config", "version: 2\n") + + d, err := loadVM(db, "vm-1") + if err != nil { + t.Fatalf("loadVM : %v", err) + } + if len(d.documents) != 2 { + t.Fatalf("2 documents attendus, obtenu %d : %v", len(d.documents), d.documents) + } + if d.documents["user-data"] != userData { + t.Errorf("user-data altéré au passage en base :\nattendu %q\nobtenu %q", userData, d.documents["user-data"]) + } + if d.documents["network-config"] != "version: 2\n" { + t.Errorf("network-config inattendu : %q", d.documents["network-config"]) + } +} + +func TestLoadVM_DocumentKeysAreStrippedOfPrefix(t *testing.T) { + db := newVMInDB(t) + kv.AddInDB(db, "vm/vm-1/metadata/vendor-data", "v") + + d, err := loadVM(db, "vm-1") + if err != nil { + t.Fatalf("loadVM : %v", err) + } + for key := range d.documents { + if key != "vendor-data" { + t.Errorf("clé de document attendue %q, obtenue %q — le préfixe doit être retiré", "vendor-data", key) + } + } +} + +func TestLoadVM_EmptyDocumentIsPreserved(t *testing.T) { + db := newVMInDB(t) + kv.AddInDB(db, "vm/vm-1/metadata/user-data", "") + + d, err := loadVM(db, "vm-1") + if err != nil { + t.Fatalf("loadVM : %v", err) + } + content, ok := d.documents["user-data"] + if !ok { + t.Fatal("un document vide doit survivre au passage en base : c'est une demande de ne rien servir") + } + if content != "" { + t.Errorf("contenu attendu vide, obtenu %q", content) + } +}