diff --git a/internal/netif/tap_linux.go b/internal/netif/tap_linux.go index a5abc9c..15313af 100644 --- a/internal/netif/tap_linux.go +++ b/internal/netif/tap_linux.go @@ -26,3 +26,9 @@ func CreateTap(tapID int, bridgeName, vpcName string) error { return LinkSetUp(name) }) } + +func DeleteTap(tapID int, vpcName string) error { + return netns.Call(vpcName, func() error { + return DeleteLink(fmt.Sprintf("tap%d", tapID)) + }) +} diff --git a/internal/netif/tap_other.go b/internal/netif/tap_other.go index a81fefb..18afcbe 100644 --- a/internal/netif/tap_other.go +++ b/internal/netif/tap_other.go @@ -7,3 +7,7 @@ import "errors" func CreateTap(_ int, _, _ string) error { return errors.New("netif: tap not supported on this platform") } + +func DeleteTap(_ int, _ string) error { + return errors.New("netif: tap not supported on this platform") +} diff --git a/internal/vm/create.go b/internal/vm/create.go new file mode 100644 index 0000000..3fc22af --- /dev/null +++ b/internal/vm/create.go @@ -0,0 +1,108 @@ +package vm + +import ( + "fmt" + "strconv" + + "git.g3e.fr/syonad/two/internal/metadata" + "git.g3e.fr/syonad/two/internal/netif" + "git.g3e.fr/syonad/two/internal/netns" + "git.g3e.fr/syonad/two/internal/qemu" + "git.g3e.fr/syonad/two/pkg/db/kv" + + "github.com/dgraph-io/badger/v4" +) + +func StartVM(db *badger.DB, name string) error { + state, err := kv.GetFromDB(db, "vm/"+name+"/state") + if err != nil { + return err + } + if state != "starting" { + return nil + } + + vpcName, err := kv.GetFromDB(db, "vm/"+name+"/vpc") + if err != nil { + return fmt.Errorf("get vpc: %w", err) + } + + bridge, err := kv.GetFromDB(db, "vm/"+name+"/bridge") + if err != nil { + return fmt.Errorf("get bridge: %w", err) + } + + tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") + if err != nil { + return fmt.Errorf("get tap_id: %w", err) + } + tapID, err := strconv.Atoi(tapIDStr) + if err != nil { + return fmt.Errorf("parse tap_id: %w", err) + } + + mac, err := kv.GetFromDB(db, "vm/"+name+"/mac") + if err != nil { + return fmt.Errorf("get mac: %w", err) + } + + volumePath, err := kv.GetFromDB(db, "vm/"+name+"/volume_path") + if err != nil { + return fmt.Errorf("get volume_path: %w", err) + } + + memoryStr, err := kv.GetFromDB(db, "vm/"+name+"/memory") + if err != nil { + return fmt.Errorf("get memory: %w", err) + } + memory, _ := strconv.Atoi(memoryStr) + + cpusStr, err := kv.GetFromDB(db, "vm/"+name+"/cpus") + if err != nil { + return fmt.Errorf("get cpus: %w", err) + } + cpus, _ := strconv.Atoi(cpusStr) + + bindIP, err := kv.GetFromDB(db, "vm/"+name+"/metadata_bind_ip") + if err != nil { + return fmt.Errorf("get metadata_bind_ip: %w", err) + } + + bindPort, err := kv.GetFromDB(db, "vm/"+name+"/metadata_bind_port") + if err != nil { + return fmt.Errorf("get metadata_bind_port: %w", err) + } + + password, _ := kv.GetFromDB(db, "vm/"+name+"/password") + sshkey, _ := kv.GetFromDB(db, "vm/"+name+"/sshkey") + + if err := netif.CreateTap(tapID, bridge, vpcName); err != nil { + return fmt.Errorf("create tap: %w", err) + } + + if err := metadata.StartMetadata(metadata.NoCloudConfig{ + Name: name, + VpcName: vpcName, + BindIP: bindIP, + BindPort: bindPort, + Password: password, + SSHKEY: sshkey, + }, db, false); err != nil { + return fmt.Errorf("start metadata: %w", err) + } + + if err := netns.Call(d.vpcName, func() error { + return qemu.Start(qemu.Config{ + Name: name, + TapID: d.tapID, + Mac: d.mac, + VolumePath: d.volumePath, + Memory: d.memory, + CPUs: d.cpus, + }) + }); err != nil { + return fmt.Errorf("start qemu: %w", err) + } + + return kv.AddInDB(db, "vm/"+name+"/state", "started") +} diff --git a/internal/vm/delete.go b/internal/vm/delete.go new file mode 100644 index 0000000..d3f376c --- /dev/null +++ b/internal/vm/delete.go @@ -0,0 +1,71 @@ +package vm + +import ( + "fmt" + "strconv" + "time" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/metadata" + "git.g3e.fr/syonad/two/internal/netif" + "git.g3e.fr/syonad/two/internal/qmp" + "git.g3e.fr/syonad/two/pkg/db/kv" + + "github.com/dgraph-io/badger/v4" +) + +func StopVM(db *badger.DB, name string, cfg *configuration.Config) error { + state, err := kv.GetFromDB(db, "vm/"+name+"/state") + if err != nil { + return err + } + if state != "stopping" { + return nil + } + + vpcName, err := kv.GetFromDB(db, "vm/"+name+"/vpc") + if err != nil { + return fmt.Errorf("get vpc: %w", err) + } + + tapIDStr, err := kv.GetFromDB(db, "vm/"+name+"/tap_id") + if err != nil { + return fmt.Errorf("get tap_id: %w", err) + } + tapID, err := strconv.Atoi(tapIDStr) + if err != nil { + return fmt.Errorf("parse tap_id: %w", err) + } + + socketPath := fmt.Sprintf("/tmp/%s.qmp-sock", name) + + if _, err := qmp.Send(socketPath, []string{`{"execute":"system_powerdown"}`}); err != nil { + return fmt.Errorf("qmp system_powerdown: %w", err) + } + + // attendre l'arrêt effectif de la VM ; forcer via quit après timeout + timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second) + poll := time.Duration(cfg.Dispatcher.PollSeconds) * time.Second + stopped := false + for !stopped { + select { + case <-timeout: + qmp.Send(socketPath, []string{`{"execute":"quit"}`}) + stopped = true + case <-time.After(poll): + if _, err := qmp.Send(socketPath, nil); err != nil { + stopped = true + } + } + } + + if err := metadata.StopMetadata(name, db, false); err != nil { + return fmt.Errorf("stop metadata: %w", err) + } + + if err := netif.DeleteTap(tapID, vpcName); err != nil { + return fmt.Errorf("delete tap: %w", err) + } + + return kv.AddInDB(db, "vm/"+name+"/state", "stopped") +}