commit 248346d9808fe3e4accf88e3ce8087cb3613e35f Author: Quentin Dufour Date: Wed Sep 22 16:42:08 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70f2c46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nuage diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9d952db --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.deuxfleurs.fr/quentin/nuage + +go 1.16 + +require github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0a50fd5 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7 h1:Do8ksLD4Nr3pA0x0hnLOLftZgkiTDvwPDShRTUxtXpE= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/inventory.txt b/inventory.txt new file mode 100644 index 0000000..d711fdc --- /dev/null +++ b/inventory.txt @@ -0,0 +1,2 @@ +fr-par-1 dev1-s debian_bullseye géronimo +pl-waw-1 dev1-s debian_bullseye gorinémo diff --git a/main.go b/main.go new file mode 100644 index 0000000..c1eb224 --- /dev/null +++ b/main.go @@ -0,0 +1,256 @@ +package main + +import ( + "io" + "os" + "fmt" + "errors" + "time" + + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/scaleway-sdk-go/api/instance/v1" +) + +const PARALLELIZE = 16 +const instanceNotFound = Error("instance not found") + +func main() { + if len(os.Args) < 2 { + usage() + } + + var err error + switch os.Args[1] { + case "spawn": + err = spawn() + case "run": + err = run() + case "destroy": + err = destroy() + } + + if err != nil { + fmt.Fprintf(os.Stderr, "cmd failed: %v\n", err) + os.Exit(1) + } +} + +func usage() { + var programName = "swtool" + if len(os.Args) > 0 { + programName = os.Args[0] + } + fmt.Fprintf(os.Stderr, "Usage: %v (spawn|run|destroy)\n", programName) + os.Exit(1) +} + +/** + * Errors + */ +type Error string + +func (e Error) Error() string { + return string(e) +} + +/** + * Parser logic + */ + +type instanceReceiver interface { + onInstance(zone, machine, image, name string) error +} + +func passInstanceTo(r io.Reader, d instanceReceiver) error { + com := make(chan error, PARALLELIZE) + count := 0 + failed := 0 + + for { + var zone, machine, image, name string + n, err := fmt.Fscanf(r, "%s %s %s %s", &zone, &machine, &image, &name) + if err == io.EOF || n == 0 { + break + } else if err != nil { + return err + } else if n != 4 { + return errors.New(fmt.Sprintf("Wrong number of values (got %d, expected 4)\n", n)) + } + + go func(zone, machine, image, name string) { + err := d.onInstance(zone, machine, image, name) + if err != nil { + fmt.Fprintf(os.Stderr, "❌ Operation failed for %v (%v, %v, %v): %v\n", name, zone, machine, image, err) + } + com <- err + }(zone, machine, image, name) + count += 1 + } + + for count > 0 { + err := <- com + count -= 1 + if err != nil { + failed += 1 + } + fmt.Fprintf(os.Stdout, "ℹ️ Waiting for %v more servers\n", count) + } + + if failed > 0 { + return errors.New(fmt.Sprintf("%d operations failed", failed)) + } + return nil +} + + +/** + * Spawner utility + */ +type spawner struct { + api *instance.API +} +func newSpawner() (spawner, error) { + client, err := getClient() + if err != nil { + return spawner{}, err + } + + return spawner{ + api: instance.NewAPI(client), + }, nil +} + +func (sp *spawner) getInstanceByName(zone scw.Zone, name string) (*instance.Server, error) { + lr, err := sp.api.ListServers(&instance.ListServersRequest{ + Zone: zone, + Name: &name, + }) + if err != nil { + return nil, err + } + for _, s := range lr.Servers { + if s.Name == name { + return s, nil + } + } + return nil, instanceNotFound +} + +func (sp *spawner) onInstance(zone, machine, image, name string) error { + + z, err := scw.ParseZone(zone) + if err != nil { + return err + } + + targetServer, err := sp.getInstanceByName(z, name) + + if err == nil { + ip := parseIP(targetServer) + if targetServer.State == instance.ServerStateRunning { + fmt.Fprintf(os.Stdout, "🟣 Found %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip) + return nil + } + } else if err == instanceNotFound { + // Create a new server + createRes, err := sp.api.CreateServer(&instance.CreateServerRequest{ + Zone: z, + Name: name, + CommercialType: machine, + Image: image, + DynamicIPRequired: scw.BoolPtr(true), + }) + if err != nil { + return err + } + targetServer = createRes.Server + } else { + return err + } + + timeout := 5 * time.Minute + err = sp.api.ServerActionAndWait(&instance.ServerActionAndWaitRequest{ + ServerID: targetServer.ID, + Action: instance.ServerActionPoweron, + Zone: z, + Timeout: &timeout, + }) + if err != nil { + return err + } + + targetServer, err = sp.getInstanceByName(z, name) + if err != nil { + return err + } + + ip := parseIP(targetServer) + fmt.Fprintf(os.Stdout, "✅ Started %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip) + return nil +} + + +func parseIP(s *instance.Server) string { + ip := "(no address)" + if s.PublicIP != nil { + ip = s.PublicIP.Address.String() + if s.PublicIP.Dynamic { + ip += " (dynamic)" + } + } else if s.PrivateIP != nil { + ip = fmt.Sprintf("%v %v", s.PrivateIP, "(private)") + } + return ip +} + + +/** + * Commands + */ + +func spawn() error { + sp, err := newSpawner() + if err != nil { + return err + } + + err = passInstanceTo(os.Stdin, &sp) + if err != nil { + return err + } + + return nil +} + +func run() error { + return nil +} + +func destroy() error { + return nil +} + + + +func getClient() (*scw.Client, error) { + // Get config + // Install scw: https://github.com/scaleway/scaleway-cli + // And run `scw init` to create the config file + config, err := scw.LoadConfig() + if err != nil { + return nil, err + } + + // Use active profile + profile, err := config.GetActiveProfile() + if err != nil { + return nil, err + } + + // We use the default profile + client, err := scw.NewClient( + scw.WithProfile(profile), + scw.WithEnv(), // env variable may overwrite profile values + ) + return client, err +}