forked from juju/juju
/
conn_instance.go
205 lines (184 loc) · 6.3 KB
/
conn_instance.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package google
import (
"path"
"github.com/juju/errors"
"google.golang.org/api/compute/v1"
)
// addInstance sends a request to GCE to add a new instance to the
// connection's project, with the provided instance data and machine
// type. Each of the provided zones is attempted and the first available
// zone is where the instance is provisioned. If no zones are available
// then an error is returned. The instance that was passed in is updated
// with the new instance's data upon success. The call blocks until the
// instance is created or the request fails.
// TODO(ericsnow) Return a new inst.
func (gce *Connection) addInstance(requestedInst *compute.Instance, machineType string, zones []string) error {
for _, zoneName := range zones {
var waitErr error
inst := *requestedInst
inst.MachineType = formatMachineType(zoneName, machineType)
err := gce.raw.AddInstance(gce.projectID, zoneName, &inst)
if isWaitError(err) {
waitErr = err
} else if err != nil {
// We are guaranteed the insert failed at the point.
return errors.Annotate(err, "sending new instance request")
}
// Check if the instance was created.
realized, err := gce.raw.GetInstance(gce.projectID, zoneName, inst.Name)
if err != nil {
if waitErr == nil {
return errors.Trace(err)
}
// Try the next zone.
logger.Errorf("failed to get new instance in zone %q: %v", zoneName, waitErr)
continue
}
// Success!
*requestedInst = *realized
return nil
}
return errors.Errorf("not able to provision in any zone")
}
// AddInstance creates a new instance based on the spec's data and
// returns it. The instance will be created using the provided
// connection and in one of the provided zones.
func (gce *Connection) AddInstance(spec InstanceSpec, zones ...string) (*Instance, error) {
raw := spec.raw()
if err := gce.addInstance(raw, spec.Type, zones); err != nil {
return nil, errors.Trace(err)
}
return newInstance(raw, &spec), nil
}
// Instance gets the up-to-date info about the given instance
// and returns it.
func (gce *Connection) Instance(id, zone string) (Instance, error) {
var result Instance
raw, err := gce.raw.GetInstance(gce.projectID, zone, id)
if err != nil {
return result, errors.Trace(err)
}
result = *newInstance(raw, nil)
return result, nil
}
// Instances sends a request to the GCE API for a list of all instances
// (in the Connection's project) for which the name starts with the
// provided prefix. The result is also limited to those instances with
// one of the specified statuses (if any).
func (gce *Connection) Instances(prefix string, statuses ...string) ([]Instance, error) {
rawInsts, err := gce.raw.ListInstances(gce.projectID, prefix, statuses...)
if err != nil {
return nil, errors.Trace(err)
}
var insts []Instance
for _, rawInst := range rawInsts {
inst := newInstance(rawInst, nil)
insts = append(insts, *inst)
}
return insts, nil
}
// removeInstance sends a request to the GCE API to remove the instance
// with the provided ID (in the specified zone). The call blocks until
// the instance is removed (or the request fails).
func (gce *Connection) removeInstance(id, zone string) error {
err := gce.raw.RemoveInstance(gce.projectID, zone, id)
if err != nil {
// TODO(ericsnow) Try removing the firewall anyway?
return errors.Trace(err)
}
fwname := id
err = gce.raw.RemoveFirewall(gce.projectID, fwname)
if errors.IsNotFound(err) {
return nil
}
if err != nil {
return errors.Trace(err)
}
return nil
}
// RemoveInstances sends a request to the GCE API to terminate all
// instances (in the Connection's project) that match one of the
// provided IDs. If a prefix is provided, only IDs that start with the
// prefix will be considered. The call blocks until all the instances
// are removed or the request fails.
func (gce *Connection) RemoveInstances(prefix string, ids ...string) error {
if len(ids) == 0 {
return nil
}
instances, err := gce.Instances(prefix)
if err != nil {
return errors.Annotatef(err, "while removing instances %v", ids)
}
// TODO(ericsnow) Remove instances in parallel?
var failed []string
for _, instID := range ids {
for _, inst := range instances {
if inst.ID == instID {
zoneName := path.Base(inst.InstanceSummary.ZoneName)
if err := gce.removeInstance(instID, zoneName); err != nil {
failed = append(failed, instID)
logger.Errorf("while removing instance %q: %v", instID, err)
}
break
}
}
}
if len(failed) != 0 {
return errors.Errorf("some instance removals failed: %v", failed)
}
return nil
}
// UpdateMetadata sets the metadata key to the specified value for
// all of the instance ids given. The call blocks until all
// of the instances are updated or the request fails.
func (gce *Connection) UpdateMetadata(key, value string, ids ...string) error {
if len(ids) == 0 {
return nil
}
instances, err := gce.raw.ListInstances(gce.projectID, "")
if err != nil {
return errors.Annotatef(err, "updating metadata for instances %v", ids)
}
var failed []string
for _, instID := range ids {
for _, inst := range instances {
if inst.Name == instID {
if err := gce.updateInstanceMetadata(inst, key, value); err != nil {
failed = append(failed, instID)
logger.Errorf("while updating metadata for instance %q (%v=%q): %v",
instID, key, value, err)
}
break
}
}
}
if len(failed) != 0 {
return errors.Errorf("some metadata updates failed: %v", failed)
}
return nil
}
func (gce *Connection) updateInstanceMetadata(instance *compute.Instance, key, value string) error {
metadata := instance.Metadata
existingItem := findMetadataItem(metadata.Items, key)
if existingItem != nil && existingItem.Value == value {
// The value's already right.
return nil
} else if existingItem == nil {
metadata.Items = append(metadata.Items, &compute.MetadataItems{Key: key, Value: value})
} else {
existingItem.Value = value
}
// The GCE API won't accept a full URL for the zone (lp:1667172).
zoneName := path.Base(instance.Zone)
return errors.Trace(gce.raw.SetMetadata(gce.projectID, zoneName, instance.Name, metadata))
}
func findMetadataItem(items []*compute.MetadataItems, key string) *compute.MetadataItems {
for _, item := range items {
if item.Key == key {
return item
}
}
return nil
}