blob: a7ae477e1aa0d0c579ae33f34a8764364c743a1f [file] [log] [blame]
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -08001// Copyright 2015 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// +build extdep
6
7package buildlet
8
9import (
10 "crypto/tls"
11 "errors"
12 "fmt"
13 "net/http"
14 "strings"
15 "time"
16
Andrew Gerrandfa8373a2015-01-21 17:25:37 +110017 "golang.org/x/build/dashboard"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080018 "golang.org/x/oauth2"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080019 "google.golang.org/api/compute/v1"
20)
21
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -080022// VMOpts control how new VMs are started.
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080023type VMOpts struct {
24 // Zone is the GCE zone to create the VM in. Required.
25 Zone string
26
27 // ProjectID is the GCE project ID. Required.
28 ProjectID string
29
30 // TLS optionally specifies the TLS keypair to use.
31 // If zero, http without auth is used.
32 TLS KeyPair
33
34 // Optional description of the VM.
35 Description string
36
37 // Optional metadata to put on the instance.
38 Meta map[string]string
39
40 // DeleteIn optionally specifies a duration at which
Brad Fitzpatrick874c0832015-01-16 12:59:14 -080041
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -080042 // to delete the VM.
43 DeleteIn time.Duration
44
45 // OnInstanceRequested optionally specifies a hook to run synchronously
46 // after the computeService.Instances.Insert call, but before
47 // waiting for its operation to proceed.
48 OnInstanceRequested func()
49
50 // OnInstanceCreated optionally specifies a hook to run synchronously
51 // after the instance operation succeeds.
52 OnInstanceCreated func()
53
54 // OnInstanceCreated optionally specifies a hook to run synchronously
55 // after the computeService.Instances.Get call.
56 OnGotInstanceInfo func()
57}
58
59// StartNewVM boots a new VM on GCE and returns a buildlet client
60// configured to speak to it.
61func StartNewVM(ts oauth2.TokenSource, instName, builderType string, opts VMOpts) (*Client, error) {
62 computeService, _ := compute.New(oauth2.NewClient(oauth2.NoContext, ts))
63
64 conf, ok := dashboard.Builders[builderType]
65 if !ok {
66 return nil, fmt.Errorf("invalid builder type %q", builderType)
67 }
68
69 zone := opts.Zone
70 if zone == "" {
71 // TODO: automatic? maybe that's not useful.
72 // For now just return an error.
73 return nil, errors.New("buildlet: missing required Zone option")
74 }
75 projectID := opts.ProjectID
76 if projectID == "" {
77 return nil, errors.New("buildlet: missing required ProjectID option")
78 }
79
80 prefix := "https://www.googleapis.com/compute/v1/projects/" + projectID
81 machType := prefix + "/zones/" + zone + "/machineTypes/" + conf.MachineType()
82
83 instance := &compute.Instance{
84 Name: instName,
85 Description: opts.Description,
86 MachineType: machType,
87 Disks: []*compute.AttachedDisk{
88 {
89 AutoDelete: true,
90 Boot: true,
91 Type: "PERSISTENT",
92 InitializeParams: &compute.AttachedDiskInitializeParams{
93 DiskName: instName,
94 SourceImage: "https://www.googleapis.com/compute/v1/projects/" + projectID + "/global/images/" + conf.VMImage,
95 DiskType: "https://www.googleapis.com/compute/v1/projects/" + projectID + "/zones/" + zone + "/diskTypes/pd-ssd",
96 },
97 },
98 },
99 Tags: &compute.Tags{
100 // Warning: do NOT list "http-server" or "allow-ssh" (our
101 // project's custom tag to allow ssh access) here; the
102 // buildlet provides full remote code execution.
103 // The https-server is authenticated, though.
104 Items: []string{"https-server"},
105 },
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800106 Metadata: &compute.Metadata{},
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800107 NetworkInterfaces: []*compute.NetworkInterface{
108 &compute.NetworkInterface{
109 AccessConfigs: []*compute.AccessConfig{
110 &compute.AccessConfig{
111 Type: "ONE_TO_ONE_NAT",
112 Name: "External NAT",
113 },
114 },
115 Network: prefix + "/global/networks/default",
116 },
117 },
118 }
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800119 addMeta := func(key, value string) {
120 instance.Metadata.Items = append(instance.Metadata.Items, &compute.MetadataItems{
121 Key: key,
122 Value: value,
123 })
124 }
125 // The buildlet-binary-url is the URL of the buildlet binary
126 // which the VMs are configured to download at boot and run.
127 // This lets us/ update the buildlet more easily than
128 // rebuilding the whole VM image.
129 addMeta("buildlet-binary-url",
130 "http://storage.googleapis.com/go-builder-data/buildlet."+conf.GOOS()+"-"+conf.GOARCH())
131 addMeta("builder-type", builderType)
132 if !opts.TLS.IsZero() {
133 addMeta("tls-cert", opts.TLS.CertPEM)
134 addMeta("tls-key", opts.TLS.KeyPEM)
135 addMeta("password", opts.TLS.Password())
136 }
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800137
138 if opts.DeleteIn != 0 {
139 // In case the VM gets away from us (generally: if the
140 // coordinator dies while a build is running), then we
141 // set this attribute of when it should be killed so
142 // we can kill it later when the coordinator is
143 // restarted. The cleanUpOldVMs goroutine loop handles
144 // that killing.
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800145 addMeta("delete-at", fmt.Sprint(time.Now().Add(opts.DeleteIn).Unix()))
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800146 }
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800147
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800148 for k, v := range opts.Meta {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800149 addMeta(k, v)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800150 }
151
152 op, err := computeService.Instances.Insert(projectID, zone, instance).Do()
153 if err != nil {
154 return nil, fmt.Errorf("Failed to create instance: %v", err)
155 }
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800156 condRun(opts.OnInstanceRequested)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800157 createOp := op.Name
158
159 // Wait for instance create operation to succeed.
160OpLoop:
161 for {
162 time.Sleep(2 * time.Second)
163 op, err := computeService.ZoneOperations.Get(projectID, zone, createOp).Do()
164 if err != nil {
165 return nil, fmt.Errorf("Failed to get op %s: %v", createOp, err)
166 }
167 switch op.Status {
168 case "PENDING", "RUNNING":
169 continue
170 case "DONE":
171 if op.Error != nil {
172 for _, operr := range op.Error.Errors {
173 return nil, fmt.Errorf("Error creating instance: %+v", operr)
174 }
175 return nil, errors.New("Failed to start.")
176 }
177 break OpLoop
178 default:
179 return nil, fmt.Errorf("Unknown create status %q: %+v", op.Status, op)
180 }
181 }
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800182 condRun(opts.OnInstanceCreated)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800183
184 inst, err := computeService.Instances.Get(projectID, zone, instName).Do()
185 if err != nil {
186 return nil, fmt.Errorf("Error getting instance %s details after creation: %v", instName, err)
187 }
188
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800189 // Finds its internal and/or external IP addresses.
190 intIP, extIP := instanceIPs(inst)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800191
192 // Wait for it to boot and its buildlet to come up.
193 var buildletURL string
194 var ipPort string
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800195 if !opts.TLS.IsZero() {
196 if extIP == "" {
197 return nil, errors.New("didn't find its external IP address")
198 }
199 buildletURL = "https://" + extIP
200 ipPort = extIP + ":443"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800201 } else {
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800202 if intIP == "" {
203 return nil, errors.New("didn't find its internal IP address")
204 }
205 buildletURL = "http://" + intIP
206 ipPort = intIP + ":80"
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800207 }
Brad Fitzpatrickf8c24842015-01-16 09:54:03 -0800208 condRun(opts.OnGotInstanceInfo)
Brad Fitzpatrickf3c01932015-01-15 16:29:16 -0800209
210 const timeout = 90 * time.Second
211 var alive bool
212 impatientClient := &http.Client{
213 Timeout: 5 * time.Second,
214 Transport: &http.Transport{
215 TLSClientConfig: &tls.Config{
216 InsecureSkipVerify: true,
217 },
218 },
219 }
220 deadline := time.Now().Add(timeout)
221 try := 0
222 for time.Now().Before(deadline) {
223 try++
224 res, err := impatientClient.Get(buildletURL)
225 if err != nil {
226 time.Sleep(1 * time.Second)
227 continue
228 }
229 res.Body.Close()
230 if res.StatusCode != 200 {
231 return nil, fmt.Errorf("buildlet returned HTTP status code %d on try number %d", res.StatusCode, try)
232 }
233 alive = true
234 break
235 }
236 if !alive {
237 return nil, fmt.Errorf("buildlet didn't come up in %v", timeout)
238 }
239
240 return NewClient(ipPort, opts.TLS), nil
241}
Brad Fitzpatrick874c0832015-01-16 12:59:14 -0800242
243// DestroyVM sends a request to delete a VM. Actual VM description is
244// currently (2015-01-19) very slow for no good reason. This function
245// returns once it's been requested, not when it's done.
246func DestroyVM(ts oauth2.TokenSource, proj, zone, instance string) error {
247 computeService, _ := compute.New(oauth2.NewClient(oauth2.NoContext, ts))
248 _, err := computeService.Instances.Delete(proj, zone, instance).Do()
249 return err
250}
251
252type VM struct {
253 // Name is the name of the GCE VM instance.
254 // For example, it's of the form "mote-bradfitz-plan9-386-foo",
255 // and not "plan9-386-foo".
256 Name string
257 IPPort string
258 TLS KeyPair
259 Type string
260}
261
262// ListVMs lists all VMs.
263func ListVMs(ts oauth2.TokenSource, proj, zone string) ([]VM, error) {
264 var vms []VM
265 computeService, _ := compute.New(oauth2.NewClient(oauth2.NoContext, ts))
266
267 // TODO(bradfitz): paging over results if more than 500
268 list, err := computeService.Instances.List(proj, zone).Do()
269 if err != nil {
270 return nil, err
271 }
272 for _, inst := range list.Items {
273 if inst.Metadata == nil {
274 // Defensive. Not seen in practice.
275 continue
276 }
277 meta := map[string]string{}
278 for _, it := range inst.Metadata.Items {
279 meta[it.Key] = it.Value
280 }
281 builderType := meta["builder-type"]
282 if builderType == "" {
283 continue
284 }
285 vm := VM{
286 Name: inst.Name,
287 Type: builderType,
288 TLS: KeyPair{
289 CertPEM: meta["tls-cert"],
290 KeyPEM: meta["tls-key"],
291 },
292 }
293 _, extIP := instanceIPs(inst)
294 if extIP == "" || vm.TLS.IsZero() {
295 continue
296 }
297 vm.IPPort = extIP + ":443"
298 vms = append(vms, vm)
299 }
300 return vms, nil
301}
302
303func instanceIPs(inst *compute.Instance) (intIP, extIP string) {
304 for _, iface := range inst.NetworkInterfaces {
305 if strings.HasPrefix(iface.NetworkIP, "10.") {
306 intIP = iface.NetworkIP
307 }
308 for _, accessConfig := range iface.AccessConfigs {
309 if accessConfig.Type == "ONE_TO_ONE_NAT" {
310 extIP = accessConfig.NatIP
311 }
312 }
313 }
314 return
315}