Skip to content

Commit 14925fa

Browse files
cagedmantisgopherbot
authored andcommitted
cmd/bootstrapswarm: add bootstrapswarm used to bootstrap swarming bot
This change adds a bootstrapswarm which bootstraps the swarming bot on two different environments (on GCE and not on GCE). It can be extended in the future to start the swarming client on other clouds as needed. Updates golang/go#60468 Updates golang/go#60640 Change-Id: Iead5f980d27441d3bc6d8161d8baf695a5b55d56 Reviewed-on: https://go-review.googlesource.com/c/build/+/504821 Run-TryBot: Dmitri Shuralyov <[email protected]> Auto-Submit: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]> Reviewed-by: Carlos Amedee <[email protected]>
1 parent 4de57f2 commit 14925fa

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

cmd/bootstrapswarm/bootstrapswarm.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// bootstapswarm will bootstrap the swarming bot depending
6+
// on the environment that it is run on.
7+
//
8+
// On GCE: bootstrapswarm will retrieve authentication credentials
9+
// from the GCE metadata service and use those credentials to download
10+
// the swarming bot. It will then start the swarming bot in a directory
11+
// within the user's home directory.
12+
//
13+
// Requirements:
14+
// - Python3 installed and in the calling user's PATH.
15+
//
16+
// Not on GCE: bootstrapswarm will read the token file and retrieve the
17+
// the luci machine token. It will use that token to authenticate and
18+
// download the swarming bot. It will then start the swarming bot in a
19+
// directory within the user's home directory.
20+
//
21+
// Requirements:
22+
// - Python3 installed and in the calling user's PATH.
23+
// - luci_machine_tokend running as root in a cron job.
24+
// https://chromium.googlesource.com/infra/luci/luci-go/+/refs/heads/main/tokenserver
25+
// Further instructions can be found at https://github.com/golang/go/wiki/DashboardBuilders
26+
// The default locations for the token files should be used if possible:
27+
// Most OS: /var/lib/luci_machine_tokend/token.json
28+
// Windows: C:\luci_machine_tokend\token.json
29+
// - bootstrapswarm should not be run as a privileged user.
30+
package main
31+
32+
import (
33+
"context"
34+
"encoding/json"
35+
"flag"
36+
"fmt"
37+
"io"
38+
"log"
39+
"net/http"
40+
"os"
41+
"os/exec"
42+
"path/filepath"
43+
"runtime"
44+
45+
"cloud.google.com/go/compute/metadata"
46+
)
47+
48+
var (
49+
tokenFilePath = flag.String("token-file-path", defaultTokenLocation(), "Path to the token file (used when not on GCE)")
50+
hostname = flag.String("hostname", os.Getenv("HOSTNAME"), "Hostname of machine to bootstrap (required)")
51+
)
52+
53+
func main() {
54+
flag.Usage = func() {
55+
fmt.Fprintln(os.Stderr, "Usage: bootstrapswarm")
56+
flag.PrintDefaults()
57+
}
58+
flag.Parse()
59+
if *hostname == "" {
60+
flag.Usage()
61+
os.Exit(2)
62+
}
63+
ctx := context.Background()
64+
if err := bootstrap(ctx, *hostname, *tokenFilePath); err != nil {
65+
log.Fatal(err)
66+
}
67+
}
68+
69+
var httpClient = http.DefaultClient
70+
71+
func bootstrap(ctx context.Context, hostname, tokenPath string) error {
72+
httpHeaders := map[string]string{"X-Luci-Swarming-Bot-ID": hostname}
73+
if metadata.OnGCE() {
74+
log.Println("Bootstrapping the swarming bot with GCE authentication")
75+
log.Println("retrieving the GCE VM token")
76+
token, err := retrieveGCEVMToken(ctx)
77+
if err != nil {
78+
return fmt.Errorf("unable to retrieve GCE Machine Token: %w", err)
79+
}
80+
httpHeaders["X-Luci-Gce-Vm-Token"] = token
81+
} else {
82+
log.Println("Bootstrapping the swarming bot with certificate authentication")
83+
log.Println("retrieving the luci-machine-token from the token file")
84+
tokBytes, err := os.ReadFile(tokenPath)
85+
if err != nil {
86+
return fmt.Errorf("unable to read file %q: %w", tokenPath, err)
87+
}
88+
type token struct {
89+
LuciMachineToken string `json:"luci_machine_token"`
90+
}
91+
var tok token
92+
if err := json.Unmarshal(tokBytes, &tok); err != nil {
93+
return fmt.Errorf("unable to unmarshal token %s: %w", tokenPath, err)
94+
}
95+
if tok.LuciMachineToken == "" {
96+
return fmt.Errorf("unable to retrieve machine token from token file %s", tokenPath)
97+
}
98+
httpHeaders["X-Luci-Machine-Token"] = tok.LuciMachineToken
99+
}
100+
log.Println("Downloading the swarming bot")
101+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, `https://chromium-swarm.appspot.com/bot_code`, nil)
102+
if err != nil {
103+
return fmt.Errorf("http.NewRequest: %w", err)
104+
}
105+
for k, v := range httpHeaders {
106+
req.Header.Set(k, v)
107+
}
108+
resp, err := httpClient.Do(req)
109+
if err != nil {
110+
return fmt.Errorf("client.Do: %w", err)
111+
}
112+
defer resp.Body.Close()
113+
if resp.StatusCode != 200 {
114+
return fmt.Errorf("status code %d", resp.StatusCode)
115+
}
116+
botBytes, err := io.ReadAll(resp.Body)
117+
if err != nil {
118+
return fmt.Errorf("io.ReadAll: %w", err)
119+
}
120+
botPath, err := writeToWorkDirectory(botBytes, "swarming_bot.zip")
121+
if err != nil {
122+
return fmt.Errorf("unable to save swarming bot to disk: %w", err)
123+
}
124+
log.Printf("Starting the swarming bot %s", botPath)
125+
cmd := exec.CommandContext(ctx, "python3", botPath, "start_bot")
126+
// swarming client checks the SWARMING_BOT_ID environment variable for hostname overrides.
127+
cmd.Env = append(os.Environ(), fmt.Sprintf("SWARMING_BOT_ID=%s", hostname))
128+
cmd.Stdout = os.Stdout
129+
cmd.Stderr = os.Stderr
130+
if err := cmd.Run(); err != nil {
131+
return fmt.Errorf("command execution %s: %s", cmd, err)
132+
}
133+
return nil
134+
}
135+
136+
// writeToWorkDirectory writes a file to the swarming working directory and returns the path
137+
// to where the file was written.
138+
func writeToWorkDirectory(b []byte, filename string) (string, error) {
139+
homeDir, err := os.UserHomeDir()
140+
if err != nil {
141+
return "", fmt.Errorf("os.UserHomeDir: %w", err)
142+
}
143+
workDir := filepath.Join(homeDir, ".swarming")
144+
if err := os.Mkdir(workDir, 0755); err != nil && !os.IsExist(err) {
145+
return "", fmt.Errorf("os.Mkdir(%s): %w", workDir, err)
146+
}
147+
path := filepath.Join(workDir, filename)
148+
if err = os.WriteFile(path, b, 0644); err != nil {
149+
return "", fmt.Errorf("os.WriteFile(%s): %w", path, err)
150+
}
151+
return path, nil
152+
}
153+
154+
// retrieveGCEVMToken retrieves a GCE VM token from the GCP metadata service.
155+
func retrieveGCEVMToken(ctx context.Context) (string, error) {
156+
const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://chromium-swarm.appspot.com&format=full`
157+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
158+
if err != nil {
159+
return "", fmt.Errorf("http.NewRequest: %w", err)
160+
}
161+
req.Header.Set("Metadata-Flavor", "Google")
162+
resp, err := httpClient.Do(req)
163+
if err != nil {
164+
return "", fmt.Errorf("client.Do: %w", err)
165+
}
166+
defer resp.Body.Close()
167+
if resp.StatusCode != 200 {
168+
return "", fmt.Errorf("status code %d", resp.StatusCode)
169+
}
170+
b, err := io.ReadAll(resp.Body)
171+
if err != nil {
172+
return "", fmt.Errorf("io.ReadAll: %w", err)
173+
}
174+
return string(b), nil
175+
}
176+
177+
func defaultTokenLocation() string {
178+
out := "/var/lib/luci_machine_tokend/token.json"
179+
if runtime.GOOS == "windows" {
180+
return `C:\luci_machine_tokend\token.json`
181+
}
182+
return out
183+
}

0 commit comments

Comments
 (0)