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
+}