cmd/scaleway: add -list, -list-all flags, show status, reboot wedged machines
Change-Id: Iacb3c84011330fdc7f3dfb168c33d81aec9b58cd
Reviewed-on: https://go-review.googlesource.com/46570
Reviewed-by: Andrew Bonventre <andybons@google.com>
diff --git a/cmd/scaleway/scaleway.go b/cmd/scaleway/scaleway.go
index f1fc8bb..1fb835f 100644
--- a/cmd/scaleway/scaleway.go
+++ b/cmd/scaleway/scaleway.go
@@ -17,7 +17,12 @@
"net/http"
"os"
"path/filepath"
+ "sort"
"strings"
+ "time"
+
+ "go4.org/types"
+ revtype "golang.org/x/build/types"
)
var (
@@ -27,10 +32,16 @@
num = flag.Int("n", 0, "Number of servers to create; if zero, defaults to a value as a function of --staging")
tags = flag.String("tags", "", "Comma-separated list of tags. The build key tags should be of the form 'buildkey_linux-arm_HEXHEXHEXHEXHEX'. If empty, it's automatic.")
staging = flag.Bool("staging", false, "If true, deploy staging instances (with staging names and tags) instead of prod.")
+ listAll = flag.Bool("list-all", false, "If true, list all (prod, staging, other) current Scaleway servers and stop without making changes.")
+ list = flag.Bool("list", false, "If true, list all prod (or staging, if -staging) servers, including missing ones.")
)
-// ctype is the Commercial Type of server we use for the builders.
-const ctype = "C1"
+const (
+ // ctype is the Commercial Type of server we use for the builders.
+ ctype = "C1"
+
+ scalewayAPIBase = "https://api.scaleway.com"
+)
func main() {
flag.Parse()
@@ -45,7 +56,7 @@
if *staging {
*num = 5
} else {
- *num = 20
+ *num = 50
}
}
if *token == "" {
@@ -62,46 +73,97 @@
if err != nil {
log.Fatal(err)
}
+ var names []string
servers := map[string]*Server{}
for _, s := range serverList {
servers[s.Name] = s
+ names = append(names, s.Name)
+ }
+ sort.Strings(names)
+ if *listAll {
+ for _, name := range names {
+ s := servers[name]
+ fmt.Printf("%s: %v, id=%v, state=%s, created=%v, modified=%v, image=%v\n",
+ name, s.PublicIP, s.ID, s.State, s.CreationDate, s.ModificationDate, s.Image)
+ }
+ return
+ }
+ for i := 1; i <= *num; i++ {
+ name := serverName(i)
+ if _, ok := servers[name]; !ok {
+ servers[name] = &Server{Name: name}
+ names = append(names, name)
+ }
+ }
+ sort.Strings(names)
+
+ for name, revBuilder := range getConnectedMachines() {
+ if _, ok := servers[name]; !ok {
+ log.Printf("Machine connected to farmer.golang.org is unknown to scaleway: %v; ignoring", name)
+ continue
+ }
+ servers[name].Connected = revBuilder
+ }
+
+ if *list {
+ for _, name := range names {
+ s := servers[name]
+ status := "NOT_CONNECTED"
+ if s.Connected != nil {
+ status = "ok"
+ }
+ fmt.Printf("%s: %s, %v, id=%v, state=%s, created=%v, modified=%v, image=%v\n",
+ name, status, s.PublicIP, s.ID, s.State, s.CreationDate, s.ModificationDate, s.Image)
+ }
}
for i := 1; i <= *num; i++ {
- name := fmt.Sprintf("scaleway-prod-%02d", i)
+ name := serverName(i)
+ server := servers[name]
+ if server.Connected != nil {
+ continue
+ }
+ if server.State == "running" {
+ if time.Time(server.ModificationDate).Before(time.Now().Add(15 * time.Minute)) {
+ log.Printf("rebooting old running-but-disconnected %q server...", name)
+ err := cl.serverAction(server.ID, "reboot")
+ log.Printf("reboot(%q): %v", name, err)
+ continue
+ }
+ // Started recently. Maybe still booting.
+ continue
+ }
+ if server.State != "" {
+ log.Printf("server %q in state %q; not creating", name, server.State)
+ continue
+ }
+ tags := strings.Split(*tags, ",")
if *staging {
- name = fmt.Sprintf("scaleway-staging-%02d", i)
+ tags = append(tags, "staging")
}
- _, ok := servers[name]
- if !ok {
- tags := strings.Split(*tags, ",")
- if *staging {
- tags = append(tags, "staging")
- }
- body, err := json.Marshal(createServerRequest{
- Org: *org,
- Name: name,
- Image: *image,
- CommercialType: ctype,
- Tags: tags,
- })
- if err != nil {
- log.Fatal(err)
- }
- log.Printf("Doing req %q for token %q", body, *token)
- req, err := http.NewRequest("POST", "https://api.scaleway.com/servers", bytes.NewReader(body))
- if err != nil {
- log.Fatal(err)
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-Auth-Token", *token)
- res, err := http.DefaultClient.Do(req)
- if err != nil {
- log.Fatal(err)
- }
- log.Printf("Create of %v: %v", i, res.Status)
- res.Body.Close()
+ body, err := json.Marshal(createServerRequest{
+ Org: *org,
+ Name: name,
+ Image: *image,
+ CommercialType: ctype,
+ Tags: tags,
+ })
+ if err != nil {
+ log.Fatal(err)
}
+ log.Printf("Doing req %q for token %q", body, *token)
+ req, err := http.NewRequest("POST", scalewayAPIBase+"/servers", bytes.NewReader(body))
+ if err != nil {
+ log.Fatal(err)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Auth-Token", *token)
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Printf("Create of %v: %v", i, res.Status)
+ res.Body.Close()
}
serverList, err = cl.Servers()
@@ -113,7 +175,7 @@
continue
}
if s.State == "stopped" {
- log.Printf("Powering on %s = %v", s.ID, cl.PowerOn(s.ID))
+ log.Printf("Powering on %s (%s) = %v", s.Name, s.ID, cl.PowerOn(s.ID))
}
}
}
@@ -130,12 +192,31 @@
Token string
}
+// Delete deletes a server. It needs to be powered off in "stopped" state first.
+//
+// This is currently unused. An earlier version of this tool used it briefly before
+// changing to use the reboot action. We might want this later.
+func (c *Client) Delete(serverID string) error {
+ req, _ := http.NewRequest("DELETE", scalewayAPIBase+"/servers/"+serverID, nil)
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Auth-Token", c.Token)
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("error deleting %s: %v", serverID, res.Status)
+ }
+ return nil
+}
+
func (c *Client) PowerOn(serverID string) error {
return c.serverAction(serverID, "poweron")
}
func (c *Client) serverAction(serverID, action string) error {
- req, _ := http.NewRequest("POST", "https://api.scaleway.com/servers/"+serverID+"/action", strings.NewReader(fmt.Sprintf(`{"action":"%s"}`, action)))
+ req, _ := http.NewRequest("POST", scalewayAPIBase+"/servers/"+serverID+"/action", strings.NewReader(fmt.Sprintf(`{"action":"%s"}`, action)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Auth-Token", c.Token)
res, err := http.DefaultClient.Do(req)
@@ -144,21 +225,21 @@
}
defer res.Body.Close()
if res.StatusCode/100 != 2 {
- return fmt.Errorf("Error doing %q on %s: %v", action, serverID, res.Status)
+ return fmt.Errorf("error doing %q on %s: %v", action, serverID, res.Status)
}
return nil
}
func (c *Client) Servers() ([]*Server, error) {
- req, _ := http.NewRequest("GET", "https://api.scaleway.com/servers", nil)
+ req, _ := http.NewRequest("GET", scalewayAPIBase+"/servers", nil)
req.Header.Set("X-Auth-Token", c.Token)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
- if res.StatusCode != 200 {
- return nil, fmt.Errorf("Failed to get Server list: %v", res.Status)
+ if res.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to get Server list: %v", res.Status)
}
var jres struct {
Servers []*Server `json:"servers"`
@@ -168,13 +249,19 @@
}
type Server struct {
- ID string `json:"id"`
- Name string `json:"name"`
- PublicIP *IP `json:"public_ip"`
- PrivateIP string `json:"private_ip"`
- Tags []string `json:"tags"`
- State string `json:"state"`
- Image *Image `json:"image"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ PublicIP *IP `json:"public_ip"`
+ PrivateIP string `json:"private_ip"`
+ Tags []string `json:"tags"`
+ State string `json:"state"`
+ Image *Image `json:"image"`
+ CreationDate types.Time3339 `json:"creation_date"`
+ ModificationDate types.Time3339 `json:"modification_date"`
+
+ // Connected is non-nil if the server is connected to farmer.golang.org.
+ // This does not come from the Scaleway API.
+ Connected *revtype.ReverseBuilder `json:"-"`
}
type Image struct {
@@ -182,11 +269,25 @@
Name string `json:"name"`
}
+func (im *Image) String() string {
+ if im == nil {
+ return "<no Image>"
+ }
+ return im.ID
+}
+
type IP struct {
ID string `json:"id"`
Address string `json:"address"`
}
+func (ip *IP) String() string {
+ if ip == nil {
+ return "<no IP>"
+ }
+ return ip.Address
+}
+
// defaultBuilderTags returns the default value of the "tags" flag.
// It returns a comma-separated list of builder tags (each of the form buildkey_$(BUILDER)_$(SECRETHEX)).
func defaultBuilderTags(baseKeyFile string) string {
@@ -203,3 +304,31 @@
}
return strings.Join(tags, ",")
}
+
+func serverName(i int) string {
+ if *staging {
+ return fmt.Sprintf("scaleway-staging-%02d", i)
+ }
+ return fmt.Sprintf("scaleway-prod-%02d", i)
+}
+
+func getConnectedMachines() map[string]*revtype.ReverseBuilder {
+ const reverseURL = "https://farmer.golang.org/status/reverse.json"
+ res, err := http.Get(reverseURL)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer res.Body.Close()
+ if res.StatusCode != http.StatusOK {
+ log.Fatal("getting %s: %s", reverseURL, res.Status)
+ }
+ var jres revtype.ReverseBuilderStatus
+ if err := json.NewDecoder(res.Body).Decode(&jres); err != nil {
+ log.Fatalf("reading %s: %v", reverseURL, err)
+ }
+ st := jres.HostTypes["host-linux-arm-scaleway"]
+ if st == nil {
+ return nil
+ }
+ return st.Machines
+}