github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/gce/google/raw.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package google
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"path"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/retry"
    16  	"golang.org/x/net/context"
    17  	"google.golang.org/api/compute/v1"
    18  	"google.golang.org/api/googleapi"
    19  )
    20  
    21  const diskTypesBase = "https://www.googleapis.com/compute/v1/projects/%s/zones/%s/diskTypes/%s"
    22  
    23  // These are attempt strategies used in waitOperation.
    24  var (
    25  	// TODO(ericsnow) Tune the timeouts and delays.
    26  	longRetryStrategy = retry.CallArgs{
    27  		Clock:       clock.WallClock,
    28  		Delay:       2 * time.Second,
    29  		MaxDuration: 5 * time.Minute,
    30  	}
    31  )
    32  
    33  func convertRawAPIError(err error) error {
    34  	if err2, ok := err.(*googleapi.Error); ok {
    35  		if err2.Code == http.StatusNotFound {
    36  			return errors.NewNotFound(err, "")
    37  		}
    38  	}
    39  	return err
    40  }
    41  
    42  type rawConn struct {
    43  	*compute.Service
    44  }
    45  
    46  func (rc *rawConn) GetProject(projectID string) (*compute.Project, error) {
    47  	call := rc.Projects.Get(projectID)
    48  	proj, err := call.Do()
    49  	return proj, errors.Trace(err)
    50  }
    51  
    52  func (rc *rawConn) GetInstance(projectID, zone, id string) (*compute.Instance, error) {
    53  	call := rc.Instances.Get(projectID, zone, id)
    54  	inst, err := call.Do()
    55  	return inst, errors.Trace(err)
    56  }
    57  
    58  func (rc *rawConn) ListInstances(projectID, prefix string, statuses ...string) ([]*compute.Instance, error) {
    59  	call := rc.Instances.AggregatedList(projectID)
    60  	call = call.Filter("name eq " + prefix + ".*")
    61  
    62  	var results []*compute.Instance
    63  	for {
    64  		rawResult, err := call.Do()
    65  		if err != nil {
    66  			return nil, errors.Trace(err)
    67  		}
    68  
    69  		for _, instList := range rawResult.Items {
    70  			for _, inst := range instList.Instances {
    71  				if !checkInstStatus(inst, statuses) {
    72  					continue
    73  				}
    74  				results = append(results, inst)
    75  			}
    76  		}
    77  		if rawResult.NextPageToken == "" {
    78  			break
    79  		}
    80  		call = call.PageToken(rawResult.NextPageToken)
    81  	}
    82  	return results, nil
    83  }
    84  
    85  func checkInstStatus(inst *compute.Instance, statuses []string) bool {
    86  	if len(statuses) == 0 {
    87  		return true
    88  	}
    89  	for _, status := range statuses {
    90  		if inst.Status == status {
    91  			return true
    92  		}
    93  	}
    94  	return false
    95  }
    96  
    97  func (rc *rawConn) AddInstance(projectID, zoneName string, spec *compute.Instance) error {
    98  	call := rc.Instances.Insert(projectID, zoneName, spec)
    99  	operation, err := call.Do()
   100  	if err != nil {
   101  		// We are guaranteed the insert failed at the point.
   102  		return errors.Annotate(err, "sending new instance request")
   103  	}
   104  	err = rc.waitOperation(projectID, operation, longRetryStrategy, logOperationErrors)
   105  	return errors.Trace(err)
   106  }
   107  
   108  func (rc *rawConn) RemoveInstance(projectID, zone, id string) error {
   109  	call := rc.Instances.Delete(projectID, zone, id)
   110  	operation, err := call.Do()
   111  	if err != nil {
   112  		return errors.Trace(err)
   113  	}
   114  	err = rc.waitOperation(projectID, operation, longRetryStrategy, returnNotFoundOperationErrors)
   115  	return errors.Trace(err)
   116  }
   117  
   118  func matchesPrefix(firewallName, namePrefix string) bool {
   119  	return firewallName == namePrefix || strings.HasPrefix(firewallName, namePrefix+"-")
   120  }
   121  
   122  func (rc *rawConn) GetFirewalls(projectID, namePrefix string) ([]*compute.Firewall, error) {
   123  	call := rc.Firewalls.List(projectID)
   124  	firewallList, err := call.Do()
   125  	if err != nil {
   126  		return nil, errors.Annotate(err, "while getting firewall from GCE")
   127  	}
   128  
   129  	if len(firewallList.Items) == 0 {
   130  		return nil, errors.NotFoundf("firewall %q", namePrefix)
   131  	}
   132  	var result []*compute.Firewall
   133  	for _, fw := range firewallList.Items {
   134  		if matchesPrefix(fw.Name, namePrefix) {
   135  			result = append(result, fw)
   136  		}
   137  	}
   138  	return result, nil
   139  }
   140  
   141  func (rc *rawConn) AddFirewall(projectID string, firewall *compute.Firewall) error {
   142  	call := rc.Firewalls.Insert(projectID, firewall)
   143  	operation, err := call.Do()
   144  	if err != nil {
   145  		return errors.Trace(err)
   146  	}
   147  	err = rc.waitOperation(projectID, operation, longRetryStrategy, logOperationErrors)
   148  	return errors.Trace(err)
   149  }
   150  
   151  func (rc *rawConn) UpdateFirewall(projectID, name string, firewall *compute.Firewall) error {
   152  	call := rc.Firewalls.Update(projectID, name, firewall)
   153  	operation, err := call.Do()
   154  	if err != nil {
   155  		return errors.Trace(err)
   156  	}
   157  	err = rc.waitOperation(projectID, operation, longRetryStrategy, logOperationErrors)
   158  	return errors.Trace(err)
   159  }
   160  
   161  type handleOperationErrors func(operation *compute.Operation) error
   162  
   163  func returnNotFoundOperationErrors(operation *compute.Operation) error {
   164  	if operation.Error != nil {
   165  		result := waitError{operation, nil}
   166  		for _, err := range operation.Error.Errors {
   167  			if err.Code == "RESOURCE_NOT_FOUND" {
   168  				result.cause = errors.NotFoundf("%v: resource", err.Message)
   169  				continue
   170  			}
   171  			logger.Errorf("GCE operation error: (%s) %s", err.Code, err.Message)
   172  		}
   173  		return result
   174  	}
   175  	return nil
   176  }
   177  
   178  func logOperationErrors(operation *compute.Operation) error {
   179  	if operation.Error != nil {
   180  		for _, err := range operation.Error.Errors {
   181  			logger.Errorf("GCE operation error: (%s) %s", err.Code, err.Message)
   182  		}
   183  		return waitError{operation, nil}
   184  	}
   185  	return nil
   186  }
   187  
   188  func (rc *rawConn) RemoveFirewall(projectID, name string) error {
   189  	call := rc.Firewalls.Delete(projectID, name)
   190  	operation, err := call.Do()
   191  	if err != nil {
   192  		return errors.Trace(convertRawAPIError(err))
   193  	}
   194  
   195  	err = rc.waitOperation(projectID, operation, longRetryStrategy, returnNotFoundOperationErrors)
   196  	return errors.Trace(convertRawAPIError(err))
   197  }
   198  
   199  func (rc *rawConn) ListAvailabilityZones(projectID, region string) ([]*compute.Zone, error) {
   200  	call := rc.Zones.List(projectID)
   201  	if region != "" {
   202  		call = call.Filter("name eq " + region + "-.*")
   203  	}
   204  
   205  	var results []*compute.Zone
   206  	for {
   207  		zoneList, err := call.Do()
   208  		if err != nil {
   209  			return nil, errors.Trace(err)
   210  		}
   211  
   212  		for _, zone := range zoneList.Items {
   213  			results = append(results, zone)
   214  		}
   215  		if zoneList.NextPageToken == "" {
   216  			break
   217  		}
   218  		call = call.PageToken(zoneList.NextPageToken)
   219  	}
   220  	return results, nil
   221  }
   222  
   223  func formatDiskType(project, zone string, spec *compute.Disk) {
   224  	// empty will default in pd-standard
   225  	if spec.Type == "" {
   226  		return
   227  	}
   228  	// see https://cloud.google.com/compute/docs/reference/latest/disks#resource
   229  	if strings.HasPrefix(spec.Type, "http") || strings.HasPrefix(spec.Type, "projects") || strings.HasPrefix(spec.Type, "global") {
   230  		return
   231  	}
   232  	spec.Type = fmt.Sprintf(diskTypesBase, project, zone, spec.Type)
   233  }
   234  
   235  func (rc *rawConn) CreateDisk(project, zone string, spec *compute.Disk) error {
   236  	ds := rc.Service.Disks
   237  	formatDiskType(project, zone, spec)
   238  	call := ds.Insert(project, zone, spec)
   239  	op, err := call.Do()
   240  	if err != nil {
   241  		return errors.Annotate(err, "could not create a new disk")
   242  	}
   243  	return errors.Trace(rc.waitOperation(project, op, longRetryStrategy, logOperationErrors))
   244  }
   245  
   246  func (rc *rawConn) ListDisks(project string) ([]*compute.Disk, error) {
   247  	ds := rc.Service.Disks
   248  	call := ds.AggregatedList(project)
   249  	var results []*compute.Disk
   250  	for {
   251  		diskList, err := call.Do()
   252  		if err != nil {
   253  			return nil, errors.Trace(err)
   254  		}
   255  		for _, list := range diskList.Items {
   256  			results = append(results, list.Disks...)
   257  		}
   258  		if diskList.NextPageToken == "" {
   259  			break
   260  		}
   261  		call = call.PageToken(diskList.NextPageToken)
   262  	}
   263  	return results, nil
   264  }
   265  
   266  func (rc *rawConn) RemoveDisk(project, zone, id string) error {
   267  	ds := rc.Disks
   268  	call := ds.Delete(project, zone, id)
   269  	op, err := call.Do()
   270  	if err != nil {
   271  		return errors.Annotatef(err, "could not delete disk %q", id)
   272  	}
   273  	return errors.Trace(rc.waitOperation(project, op, longRetryStrategy, returnNotFoundOperationErrors))
   274  }
   275  
   276  func (rc *rawConn) GetDisk(project, zone, id string) (*compute.Disk, error) {
   277  	ds := rc.Disks
   278  	call := ds.Get(project, zone, id)
   279  	disk, err := call.Do()
   280  	if err != nil {
   281  		return nil, errors.Annotatef(err, "cannot get disk %q at zone %q in project %q", id, zone, project)
   282  	}
   283  	return disk, nil
   284  }
   285  
   286  func (rc *rawConn) SetDiskLabels(project, zone, id, labelFingerprint string, labels map[string]string) error {
   287  	ds := rc.Service.Disks
   288  	call := ds.SetLabels(project, zone, id, &compute.ZoneSetLabelsRequest{
   289  		LabelFingerprint: labelFingerprint,
   290  		Labels:           labels,
   291  	})
   292  	_, err := call.Do()
   293  	return errors.Trace(err)
   294  }
   295  
   296  func (rc *rawConn) AttachDisk(project, zone, instanceId string, disk *compute.AttachedDisk) error {
   297  	call := rc.Instances.AttachDisk(project, zone, instanceId, disk)
   298  	_, err := call.Do() // Perhaps return something from the Op
   299  	if err != nil {
   300  		return errors.Annotatef(err, "cannot attach volume into %q", instanceId)
   301  	}
   302  	return nil
   303  }
   304  
   305  func (rc *rawConn) DetachDisk(project, zone, instanceId, diskDeviceName string) error {
   306  	call := rc.Instances.DetachDisk(project, zone, instanceId, diskDeviceName)
   307  	_, err := call.Do()
   308  	if err != nil {
   309  		return errors.Annotatef(err, "cannot detach volume from %q", instanceId)
   310  	}
   311  	return nil
   312  }
   313  
   314  func (rc *rawConn) InstanceDisks(project, zone, instanceId string) ([]*compute.AttachedDisk, error) {
   315  	instance, err := rc.GetInstance(project, zone, instanceId)
   316  	if err != nil {
   317  		return nil, errors.Annotatef(err, "cannot get instance %q to list its disks", instanceId)
   318  	}
   319  	return instance.Disks, nil
   320  }
   321  
   322  type waitError struct {
   323  	op    *compute.Operation
   324  	cause error
   325  }
   326  
   327  func (err waitError) Error() string {
   328  	if err.cause != nil {
   329  		return fmt.Sprintf("GCE operation %q failed: %v", err.op.Name, err.cause)
   330  	}
   331  	return fmt.Sprintf("GCE operation %q failed", err.op.Name)
   332  }
   333  
   334  func (err waitError) Cause() error {
   335  	if err.cause != nil {
   336  		return err.cause
   337  	}
   338  	return err
   339  }
   340  
   341  func isWaitError(err error) bool {
   342  	_, ok := err.(*waitError)
   343  	return ok
   344  }
   345  
   346  type opDoer interface {
   347  	Do(...googleapi.CallOption) (*compute.Operation, error)
   348  }
   349  
   350  // checkOperation requests a new copy of the given operation from the
   351  // GCE API and returns it. The new copy will have the operation's
   352  // current status.
   353  func (rc *rawConn) checkOperation(projectID string, op *compute.Operation) (*compute.Operation, error) {
   354  	var call opDoer
   355  	if op.Zone != "" {
   356  		zoneName := path.Base(op.Zone)
   357  		call = rc.ZoneOperations.Get(projectID, zoneName, op.Name)
   358  	} else if op.Region != "" {
   359  		region := path.Base(op.Region)
   360  		call = rc.RegionOperations.Get(projectID, region, op.Name)
   361  	} else {
   362  		call = rc.GlobalOperations.Get(projectID, op.Name)
   363  	}
   364  
   365  	operation, err := doOpCall(call)
   366  	if err != nil {
   367  		return nil, errors.Annotatef(err, "request for GCE operation %q failed", op.Name)
   368  	}
   369  	return operation, nil
   370  }
   371  
   372  var doOpCall = func(call opDoer) (*compute.Operation, error) {
   373  	return call.Do()
   374  }
   375  
   376  // waitOperation waits for the provided operation to reach the "done"
   377  // status. It follows the given attempt strategy (e.g. wait time between
   378  // attempts) and may time out.
   379  func (rc *rawConn) waitOperation(projectID string, op *compute.Operation, retryStrategy retry.CallArgs, f handleOperationErrors) error {
   380  	// TODO(perrito666) 2016-05-02 lp:1558657
   381  	started := time.Now()
   382  	logger.Infof("GCE operation %q, waiting...", op.Name)
   383  
   384  	retryStrategy.IsFatalError = func(err error) bool {
   385  		return !errors.IsNotProvisioned(err)
   386  	}
   387  	retryStrategy.Func = func() error {
   388  		var err error
   389  		op, err = rc.checkOperation(projectID, op)
   390  		if err != nil {
   391  			return err
   392  		}
   393  		if op.Status == StatusDone {
   394  			return nil
   395  		}
   396  		return errors.NewNotProvisioned(nil, "GCE operation not done yet")
   397  	}
   398  	var err error
   399  	if op.Status != StatusDone {
   400  		err = retry.Call(retryStrategy)
   401  	}
   402  
   403  	if retry.IsAttemptsExceeded(err) || retry.IsDurationExceeded(err) {
   404  		// lp:1558657
   405  		err := errors.Annotatef(err, "timed out after %d seconds", time.Now().Sub(started)/time.Second)
   406  		return waitError{op, err}
   407  	}
   408  	if err != nil {
   409  		return errors.Trace(err)
   410  	}
   411  	if err := f(op); err != nil {
   412  		return err
   413  	}
   414  
   415  	logger.Infof("GCE operation %q finished", op.Name)
   416  	return nil
   417  }
   418  
   419  // ListMachineTypes returns a list of machines available in the project and zone provided.
   420  func (rc *rawConn) ListMachineTypes(projectID, zone string) (*compute.MachineTypeList, error) {
   421  	op := rc.MachineTypes.List(projectID, zone)
   422  	machines, err := op.Do()
   423  	if err != nil {
   424  		return nil, errors.Annotatef(err, "listing machine types for project %q and zone %q", projectID, zone)
   425  	}
   426  	return machines, nil
   427  }
   428  
   429  func (rc *rawConn) SetMetadata(projectID, zone, instanceID string, metadata *compute.Metadata) error {
   430  	call := rc.Instances.SetMetadata(projectID, zone, instanceID, metadata)
   431  	op, err := call.Do()
   432  	if err != nil {
   433  		return errors.Trace(err)
   434  	}
   435  	err = rc.waitOperation(projectID, op, longRetryStrategy, logOperationErrors)
   436  	return errors.Trace(err)
   437  }
   438  
   439  func (rc *rawConn) ListSubnetworks(projectID, region string) ([]*compute.Subnetwork, error) {
   440  	ctx := context.Background()
   441  	call := rc.Subnetworks.List(projectID, region)
   442  	var results []*compute.Subnetwork
   443  	err := call.Pages(ctx, func(page *compute.SubnetworkList) error {
   444  		results = append(results, page.Items...)
   445  		return nil
   446  	})
   447  	if err != nil {
   448  		return nil, errors.Trace(err)
   449  	}
   450  	return results, nil
   451  }
   452  
   453  func (rc *rawConn) ListNetworks(projectID string) ([]*compute.Network, error) {
   454  	ctx := context.Background()
   455  	call := rc.Networks.List(projectID)
   456  	var results []*compute.Network
   457  	err := call.Pages(ctx, func(page *compute.NetworkList) error {
   458  		results = append(results, page.Items...)
   459  		return nil
   460  	})
   461  	if err != nil {
   462  		return nil, errors.Trace(err)
   463  	}
   464  	return results, nil
   465  }