1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
13
"github.com/juju/errors"
14
"github.com/juju/utils"
15
"google.golang.org/api/compute/v1"
16
"google.golang.org/api/googleapi"
19
const diskTypesBase = "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/diskTypes/%s"
21
// These are attempt strategies used in waitOperation.
23
// TODO(ericsnow) Tune the timeouts and delays.
24
// TODO(katco): 2016-08-09: lp:1611427
25
attemptsLong = utils.AttemptStrategy{
26
Total: 5 * time.Minute,
27
Delay: 2 * time.Second,
31
func convertRawAPIError(err error) error {
32
if err2, ok := err.(*googleapi.Error); ok {
33
if err2.Code == http.StatusNotFound {
34
return errors.NewNotFound(err, "")
44
func (rc *rawConn) GetProject(projectID string) (*compute.Project, error) {
45
call := rc.Projects.Get(projectID)
46
proj, err := call.Do()
47
return proj, errors.Trace(err)
50
func (rc *rawConn) GetInstance(projectID, zone, id string) (*compute.Instance, error) {
51
call := rc.Instances.Get(projectID, zone, id)
52
inst, err := call.Do()
53
return inst, errors.Trace(err)
56
func (rc *rawConn) ListInstances(projectID, prefix string, statuses ...string) ([]*compute.Instance, error) {
57
call := rc.Instances.AggregatedList(projectID)
58
call = call.Filter("name eq " + prefix + ".*")
60
var results []*compute.Instance
62
rawResult, err := call.Do()
64
return nil, errors.Trace(err)
67
for _, instList := range rawResult.Items {
68
for _, inst := range instList.Instances {
69
if !checkInstStatus(inst, statuses) {
72
results = append(results, inst)
75
if rawResult.NextPageToken == "" {
78
call = call.PageToken(rawResult.NextPageToken)
83
func checkInstStatus(inst *compute.Instance, statuses []string) bool {
84
if len(statuses) == 0 {
87
for _, status := range statuses {
88
if inst.Status == status {
95
func (rc *rawConn) AddInstance(projectID, zoneName string, spec *compute.Instance) error {
96
call := rc.Instances.Insert(projectID, zoneName, spec)
97
operation, err := call.Do()
99
// We are guaranteed the insert failed at the point.
100
return errors.Annotate(err, "sending new instance request")
103
err = rc.waitOperation(projectID, operation, attemptsLong)
104
return errors.Trace(err)
107
func (rc *rawConn) RemoveInstance(projectID, zone, id string) error {
108
call := rc.Instances.Delete(projectID, zone, id)
109
operation, err := call.Do()
111
return errors.Trace(err)
114
err = rc.waitOperation(projectID, operation, attemptsLong)
115
return errors.Trace(err)
118
func (rc *rawConn) GetFirewall(projectID, name string) (*compute.Firewall, error) {
119
call := rc.Firewalls.List(projectID)
120
call = call.Filter("name eq " + name)
121
firewallList, err := call.Do()
123
return nil, errors.Annotate(err, "while getting firewall from GCE")
126
if len(firewallList.Items) == 0 {
127
return nil, errors.NotFoundf("firewall %q", name)
129
return firewallList.Items[0], nil
132
func (rc *rawConn) AddFirewall(projectID string, firewall *compute.Firewall) error {
133
call := rc.Firewalls.Insert(projectID, firewall)
134
operation, err := call.Do()
136
return errors.Trace(err)
139
err = rc.waitOperation(projectID, operation, attemptsLong)
140
return errors.Trace(err)
143
func (rc *rawConn) UpdateFirewall(projectID, name string, firewall *compute.Firewall) error {
144
call := rc.Firewalls.Update(projectID, name, firewall)
145
operation, err := call.Do()
147
return errors.Trace(err)
150
err = rc.waitOperation(projectID, operation, attemptsLong)
151
return errors.Trace(err)
154
func (rc *rawConn) RemoveFirewall(projectID, name string) error {
155
call := rc.Firewalls.Delete(projectID, name)
156
operation, err := call.Do()
158
return errors.Trace(convertRawAPIError(err))
161
err = rc.waitOperation(projectID, operation, attemptsLong)
162
return errors.Trace(convertRawAPIError(err))
165
func (rc *rawConn) ListAvailabilityZones(projectID, region string) ([]*compute.Zone, error) {
166
call := rc.Zones.List(projectID)
168
call = call.Filter("name eq " + region + "-.*")
171
var results []*compute.Zone
173
zoneList, err := call.Do()
175
return nil, errors.Trace(err)
178
for _, zone := range zoneList.Items {
179
results = append(results, zone)
181
if zoneList.NextPageToken == "" {
184
call = call.PageToken(zoneList.NextPageToken)
189
func formatDiskType(project, zone string, spec *compute.Disk) {
190
// empty will default in pd-standard
194
// see https://cloud.google.com/compute/docs/reference/latest/disks#resource
195
if strings.HasPrefix(spec.Type, "http") || strings.HasPrefix(spec.Type, "projects") || strings.HasPrefix(spec.Type, "global") {
198
spec.Type = fmt.Sprintf(diskTypesBase, project, zone, spec.Type)
201
func (rc *rawConn) CreateDisk(project, zone string, spec *compute.Disk) error {
202
ds := rc.Service.Disks
203
formatDiskType(project, zone, spec)
204
call := ds.Insert(project, zone, spec)
207
return errors.Annotate(err, "could not create a new disk")
209
return errors.Trace(rc.waitOperation(project, op, attemptsLong))
212
func (rc *rawConn) ListDisks(project, zone string) ([]*compute.Disk, error) {
213
ds := rc.Service.Disks
214
call := ds.List(project, zone)
215
var results []*compute.Disk
217
diskList, err := call.Do()
219
return nil, errors.Trace(err)
221
for _, disk := range diskList.Items {
222
results = append(results, disk)
224
if diskList.NextPageToken == "" {
227
call = call.PageToken(diskList.NextPageToken)
232
func (rc *rawConn) RemoveDisk(project, zone, id string) error {
234
call := ds.Delete(project, zone, id)
237
return errors.Annotatef(err, "could not delete disk %q", id)
239
return errors.Trace(rc.waitOperation(project, op, attemptsLong))
242
func (rc *rawConn) GetDisk(project, zone, id string) (*compute.Disk, error) {
244
call := ds.Get(project, zone, id)
245
disk, err := call.Do()
247
return nil, errors.Annotatef(err, "cannot get disk %q at zone %q in project %q", id, zone, project)
252
func (rc *rawConn) AttachDisk(project, zone, instanceId string, disk *compute.AttachedDisk) error {
253
call := rc.Instances.AttachDisk(project, zone, instanceId, disk)
254
_, err := call.Do() // Perhaps return something from the Op
256
return errors.Annotatef(err, "cannot attach volume into %q", instanceId)
261
func (rc *rawConn) DetachDisk(project, zone, instanceId, diskDeviceName string) error {
262
call := rc.Instances.DetachDisk(project, zone, instanceId, diskDeviceName)
265
return errors.Annotatef(err, "cannot detach volume from %q", instanceId)
270
func (rc *rawConn) InstanceDisks(project, zone, instanceId string) ([]*compute.AttachedDisk, error) {
271
instance, err := rc.GetInstance(project, zone, instanceId)
273
return nil, errors.Annotatef(err, "cannot get instance %q to list its disks", instanceId)
275
return instance.Disks, nil
278
type waitError struct {
279
op *compute.Operation
283
func (err waitError) Error() string {
284
if err.cause != nil {
285
return fmt.Sprintf("GCE operation %q failed: %v", err.op.Name, err.cause)
287
return fmt.Sprintf("GCE operation %q failed", err.op.Name)
290
func isWaitError(err error) bool {
291
_, ok := err.(*waitError)
295
type opDoer interface {
296
Do() (*compute.Operation, error)
299
// checkOperation requests a new copy of the given operation from the
300
// GCE API and returns it. The new copy will have the operation's
302
func (rc *rawConn) checkOperation(projectID string, op *compute.Operation) (*compute.Operation, error) {
305
zoneName := path.Base(op.Zone)
306
call = rc.ZoneOperations.Get(projectID, zoneName, op.Name)
307
} else if op.Region != "" {
308
region := path.Base(op.Region)
309
call = rc.RegionOperations.Get(projectID, region, op.Name)
311
call = rc.GlobalOperations.Get(projectID, op.Name)
314
operation, err := doOpCall(call)
316
return nil, errors.Annotatef(err, "request for GCE operation %q failed", op.Name)
318
return operation, nil
321
var doOpCall = func(call opDoer) (*compute.Operation, error) {
325
// waitOperation waits for the provided operation to reach the "done"
326
// status. It follows the given attempt strategy (e.g. wait time between
327
// attempts) and may time out.
329
// TODO(katco): 2016-08-09: lp:1611427
330
func (rc *rawConn) waitOperation(projectID string, op *compute.Operation, attempts utils.AttemptStrategy) error {
331
// TODO(perrito666) 2016-05-02 lp:1558657
332
started := time.Now()
333
logger.Infof("GCE operation %q, waiting...", op.Name)
334
for a := attempts.Start(); a.Next(); {
335
if op.Status == StatusDone {
340
op, err = rc.checkOperation(projectID, op)
342
return errors.Trace(err)
345
if op.Status != StatusDone {
347
err := errors.Errorf("timed out after %d seconds", time.Now().Sub(started)/time.Second)
348
return waitError{op, err}
351
for _, err := range op.Error.Errors {
352
logger.Errorf("GCE operation error: (%s) %s", err.Code, err.Message)
354
return waitError{op, nil}
357
logger.Infof("GCE operation %q finished", op.Name)