Skip to content

Commit 51d6777

Browse files
committed
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 <[email protected]>
1 parent 16ad677 commit 51d6777

File tree

1 file changed

+176
-47
lines changed

1 file changed

+176
-47
lines changed

cmd/scaleway/scaleway.go

Lines changed: 176 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import (
1717
"net/http"
1818
"os"
1919
"path/filepath"
20+
"sort"
2021
"strings"
22+
"time"
23+
24+
"go4.org/types"
25+
revtype "golang.org/x/build/types"
2126
)
2227

2328
var (
@@ -27,10 +32,16 @@ var (
2732
num = flag.Int("n", 0, "Number of servers to create; if zero, defaults to a value as a function of --staging")
2833
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.")
2934
staging = flag.Bool("staging", false, "If true, deploy staging instances (with staging names and tags) instead of prod.")
35+
listAll = flag.Bool("list-all", false, "If true, list all (prod, staging, other) current Scaleway servers and stop without making changes.")
36+
list = flag.Bool("list", false, "If true, list all prod (or staging, if -staging) servers, including missing ones.")
3037
)
3138

32-
// ctype is the Commercial Type of server we use for the builders.
33-
const ctype = "C1"
39+
const (
40+
// ctype is the Commercial Type of server we use for the builders.
41+
ctype = "C1"
42+
43+
scalewayAPIBase = "https://api.scaleway.com"
44+
)
3445

3546
func main() {
3647
flag.Parse()
@@ -45,7 +56,7 @@ func main() {
4556
if *staging {
4657
*num = 5
4758
} else {
48-
*num = 20
59+
*num = 50
4960
}
5061
}
5162
if *token == "" {
@@ -62,46 +73,97 @@ func main() {
6273
if err != nil {
6374
log.Fatal(err)
6475
}
76+
var names []string
6577
servers := map[string]*Server{}
6678
for _, s := range serverList {
6779
servers[s.Name] = s
80+
names = append(names, s.Name)
81+
}
82+
sort.Strings(names)
83+
if *listAll {
84+
for _, name := range names {
85+
s := servers[name]
86+
fmt.Printf("%s: %v, id=%v, state=%s, created=%v, modified=%v, image=%v\n",
87+
name, s.PublicIP, s.ID, s.State, s.CreationDate, s.ModificationDate, s.Image)
88+
}
89+
return
6890
}
69-
7091
for i := 1; i <= *num; i++ {
71-
name := fmt.Sprintf("scaleway-prod-%02d", i)
72-
if *staging {
73-
name = fmt.Sprintf("scaleway-staging-%02d", i)
92+
name := serverName(i)
93+
if _, ok := servers[name]; !ok {
94+
servers[name] = &Server{Name: name}
95+
names = append(names, name)
7496
}
75-
_, ok := servers[name]
76-
if !ok {
77-
tags := strings.Split(*tags, ",")
78-
if *staging {
79-
tags = append(tags, "staging")
80-
}
81-
body, err := json.Marshal(createServerRequest{
82-
Org: *org,
83-
Name: name,
84-
Image: *image,
85-
CommercialType: ctype,
86-
Tags: tags,
87-
})
88-
if err != nil {
89-
log.Fatal(err)
90-
}
91-
log.Printf("Doing req %q for token %q", body, *token)
92-
req, err := http.NewRequest("POST", "https://api.scaleway.com/servers", bytes.NewReader(body))
93-
if err != nil {
94-
log.Fatal(err)
97+
}
98+
sort.Strings(names)
99+
100+
for name, revBuilder := range getConnectedMachines() {
101+
if _, ok := servers[name]; !ok {
102+
log.Printf("Machine connected to farmer.golang.org is unknown to scaleway: %v; ignoring", name)
103+
continue
104+
}
105+
servers[name].Connected = revBuilder
106+
}
107+
108+
if *list {
109+
for _, name := range names {
110+
s := servers[name]
111+
status := "NOT_CONNECTED"
112+
if s.Connected != nil {
113+
status = "ok"
95114
}
96-
req.Header.Set("Content-Type", "application/json")
97-
req.Header.Set("X-Auth-Token", *token)
98-
res, err := http.DefaultClient.Do(req)
99-
if err != nil {
100-
log.Fatal(err)
115+
fmt.Printf("%s: %s, %v, id=%v, state=%s, created=%v, modified=%v, image=%v\n",
116+
name, status, s.PublicIP, s.ID, s.State, s.CreationDate, s.ModificationDate, s.Image)
117+
}
118+
}
119+
120+
for i := 1; i <= *num; i++ {
121+
name := serverName(i)
122+
server := servers[name]
123+
if server.Connected != nil {
124+
continue
125+
}
126+
if server.State == "running" {
127+
if time.Time(server.ModificationDate).Before(time.Now().Add(15 * time.Minute)) {
128+
log.Printf("rebooting old running-but-disconnected %q server...", name)
129+
err := cl.serverAction(server.ID, "reboot")
130+
log.Printf("reboot(%q): %v", name, err)
131+
continue
101132
}
102-
log.Printf("Create of %v: %v", i, res.Status)
103-
res.Body.Close()
133+
// Started recently. Maybe still booting.
134+
continue
135+
}
136+
if server.State != "" {
137+
log.Printf("server %q in state %q; not creating", name, server.State)
138+
continue
139+
}
140+
tags := strings.Split(*tags, ",")
141+
if *staging {
142+
tags = append(tags, "staging")
143+
}
144+
body, err := json.Marshal(createServerRequest{
145+
Org: *org,
146+
Name: name,
147+
Image: *image,
148+
CommercialType: ctype,
149+
Tags: tags,
150+
})
151+
if err != nil {
152+
log.Fatal(err)
153+
}
154+
log.Printf("Doing req %q for token %q", body, *token)
155+
req, err := http.NewRequest("POST", scalewayAPIBase+"/servers", bytes.NewReader(body))
156+
if err != nil {
157+
log.Fatal(err)
158+
}
159+
req.Header.Set("Content-Type", "application/json")
160+
req.Header.Set("X-Auth-Token", *token)
161+
res, err := http.DefaultClient.Do(req)
162+
if err != nil {
163+
log.Fatal(err)
104164
}
165+
log.Printf("Create of %v: %v", i, res.Status)
166+
res.Body.Close()
105167
}
106168

107169
serverList, err = cl.Servers()
@@ -113,7 +175,7 @@ func main() {
113175
continue
114176
}
115177
if s.State == "stopped" {
116-
log.Printf("Powering on %s = %v", s.ID, cl.PowerOn(s.ID))
178+
log.Printf("Powering on %s (%s) = %v", s.Name, s.ID, cl.PowerOn(s.ID))
117179
}
118180
}
119181
}
@@ -130,12 +192,31 @@ type Client struct {
130192
Token string
131193
}
132194

195+
// Delete deletes a server. It needs to be powered off in "stopped" state first.
196+
//
197+
// This is currently unused. An earlier version of this tool used it briefly before
198+
// changing to use the reboot action. We might want this later.
199+
func (c *Client) Delete(serverID string) error {
200+
req, _ := http.NewRequest("DELETE", scalewayAPIBase+"/servers/"+serverID, nil)
201+
req.Header.Set("Content-Type", "application/json")
202+
req.Header.Set("X-Auth-Token", c.Token)
203+
res, err := http.DefaultClient.Do(req)
204+
if err != nil {
205+
return err
206+
}
207+
defer res.Body.Close()
208+
if res.StatusCode != http.StatusNoContent {
209+
return fmt.Errorf("error deleting %s: %v", serverID, res.Status)
210+
}
211+
return nil
212+
}
213+
133214
func (c *Client) PowerOn(serverID string) error {
134215
return c.serverAction(serverID, "poweron")
135216
}
136217

137218
func (c *Client) serverAction(serverID, action string) error {
138-
req, _ := http.NewRequest("POST", "https://api.scaleway.com/servers/"+serverID+"/action", strings.NewReader(fmt.Sprintf(`{"action":"%s"}`, action)))
219+
req, _ := http.NewRequest("POST", scalewayAPIBase+"/servers/"+serverID+"/action", strings.NewReader(fmt.Sprintf(`{"action":"%s"}`, action)))
139220
req.Header.Set("Content-Type", "application/json")
140221
req.Header.Set("X-Auth-Token", c.Token)
141222
res, err := http.DefaultClient.Do(req)
@@ -144,21 +225,21 @@ func (c *Client) serverAction(serverID, action string) error {
144225
}
145226
defer res.Body.Close()
146227
if res.StatusCode/100 != 2 {
147-
return fmt.Errorf("Error doing %q on %s: %v", action, serverID, res.Status)
228+
return fmt.Errorf("error doing %q on %s: %v", action, serverID, res.Status)
148229
}
149230
return nil
150231
}
151232

152233
func (c *Client) Servers() ([]*Server, error) {
153-
req, _ := http.NewRequest("GET", "https://api.scaleway.com/servers", nil)
234+
req, _ := http.NewRequest("GET", scalewayAPIBase+"/servers", nil)
154235
req.Header.Set("X-Auth-Token", c.Token)
155236
res, err := http.DefaultClient.Do(req)
156237
if err != nil {
157238
return nil, err
158239
}
159240
defer res.Body.Close()
160-
if res.StatusCode != 200 {
161-
return nil, fmt.Errorf("Failed to get Server list: %v", res.Status)
241+
if res.StatusCode != http.StatusOK {
242+
return nil, fmt.Errorf("failed to get Server list: %v", res.Status)
162243
}
163244
var jres struct {
164245
Servers []*Server `json:"servers"`
@@ -168,25 +249,45 @@ func (c *Client) Servers() ([]*Server, error) {
168249
}
169250

170251
type Server struct {
171-
ID string `json:"id"`
172-
Name string `json:"name"`
173-
PublicIP *IP `json:"public_ip"`
174-
PrivateIP string `json:"private_ip"`
175-
Tags []string `json:"tags"`
176-
State string `json:"state"`
177-
Image *Image `json:"image"`
252+
ID string `json:"id"`
253+
Name string `json:"name"`
254+
PublicIP *IP `json:"public_ip"`
255+
PrivateIP string `json:"private_ip"`
256+
Tags []string `json:"tags"`
257+
State string `json:"state"`
258+
Image *Image `json:"image"`
259+
CreationDate types.Time3339 `json:"creation_date"`
260+
ModificationDate types.Time3339 `json:"modification_date"`
261+
262+
// Connected is non-nil if the server is connected to farmer.golang.org.
263+
// This does not come from the Scaleway API.
264+
Connected *revtype.ReverseBuilder `json:"-"`
178265
}
179266

180267
type Image struct {
181268
ID string `json:"id"`
182269
Name string `json:"name"`
183270
}
184271

272+
func (im *Image) String() string {
273+
if im == nil {
274+
return "<no Image>"
275+
}
276+
return im.ID
277+
}
278+
185279
type IP struct {
186280
ID string `json:"id"`
187281
Address string `json:"address"`
188282
}
189283

284+
func (ip *IP) String() string {
285+
if ip == nil {
286+
return "<no IP>"
287+
}
288+
return ip.Address
289+
}
290+
190291
// defaultBuilderTags returns the default value of the "tags" flag.
191292
// It returns a comma-separated list of builder tags (each of the form buildkey_$(BUILDER)_$(SECRETHEX)).
192293
func defaultBuilderTags(baseKeyFile string) string {
@@ -203,3 +304,31 @@ func defaultBuilderTags(baseKeyFile string) string {
203304
}
204305
return strings.Join(tags, ",")
205306
}
307+
308+
func serverName(i int) string {
309+
if *staging {
310+
return fmt.Sprintf("scaleway-staging-%02d", i)
311+
}
312+
return fmt.Sprintf("scaleway-prod-%02d", i)
313+
}
314+
315+
func getConnectedMachines() map[string]*revtype.ReverseBuilder {
316+
const reverseURL = "https://farmer.golang.org/status/reverse.json"
317+
res, err := http.Get(reverseURL)
318+
if err != nil {
319+
log.Fatal(err)
320+
}
321+
defer res.Body.Close()
322+
if res.StatusCode != http.StatusOK {
323+
log.Fatal("getting %s: %s", reverseURL, res.Status)
324+
}
325+
var jres revtype.ReverseBuilderStatus
326+
if err := json.NewDecoder(res.Body).Decode(&jres); err != nil {
327+
log.Fatalf("reading %s: %v", reverseURL, err)
328+
}
329+
st := jres.HostTypes["host-linux-arm-scaleway"]
330+
if st == nil {
331+
return nil
332+
}
333+
return st.Machines
334+
}

0 commit comments

Comments
 (0)