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

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package gce
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils/v3"
    14  
    15  	"github.com/juju/juju/environs/context"
    16  	"github.com/juju/juju/environs/tags"
    17  	"github.com/juju/juju/provider/gce/google"
    18  	"github.com/juju/juju/storage"
    19  )
    20  
    21  const (
    22  	storageProviderType = storage.ProviderType("gce")
    23  )
    24  
    25  // StorageProviderTypes implements storage.ProviderRegistry.
    26  func (env *environ) StorageProviderTypes() ([]storage.ProviderType, error) {
    27  	return []storage.ProviderType{storageProviderType}, nil
    28  }
    29  
    30  // StorageProvider implements storage.ProviderRegistry.
    31  func (env *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    32  	if t == storageProviderType {
    33  		return &storageProvider{env}, nil
    34  	}
    35  	return nil, errors.NotFoundf("storage provider %q", t)
    36  }
    37  
    38  type storageProvider struct {
    39  	env *environ
    40  }
    41  
    42  var _ storage.Provider = (*storageProvider)(nil)
    43  
    44  func (g *storageProvider) ValidateForK8s(map[string]any) error {
    45  	// no validation required
    46  	return nil
    47  }
    48  
    49  func (g *storageProvider) ValidateConfig(cfg *storage.Config) error {
    50  	return nil
    51  }
    52  
    53  func (g *storageProvider) Supports(k storage.StorageKind) bool {
    54  	return k == storage.StorageKindBlock
    55  }
    56  
    57  func (g *storageProvider) Scope() storage.Scope {
    58  	return storage.ScopeEnviron
    59  }
    60  
    61  func (g *storageProvider) Dynamic() bool {
    62  	return true
    63  }
    64  
    65  func (e *storageProvider) Releasable() bool {
    66  	return true
    67  }
    68  
    69  func (g *storageProvider) DefaultPools() []*storage.Config {
    70  	// TODO(perrito666) Add explicit pools.
    71  	return nil
    72  }
    73  
    74  func (g *storageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
    75  	return nil, errors.NotSupportedf("filesystems")
    76  }
    77  
    78  type volumeSource struct {
    79  	gce       gceConnection
    80  	envName   string // non-unique, informational only
    81  	modelUUID string
    82  }
    83  
    84  func (g *storageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) {
    85  	environConfig := g.env.Config()
    86  	source := &volumeSource{
    87  		gce:       g.env.gce,
    88  		envName:   environConfig.Name(),
    89  		modelUUID: environConfig.UUID(),
    90  	}
    91  	return source, nil
    92  }
    93  
    94  type instanceCache map[string]google.Instance
    95  
    96  func (c instanceCache) update(gceClient gceConnection, ctx context.ProviderCallContext, ids ...string) error {
    97  	if len(ids) == 1 {
    98  		if _, ok := c[ids[0]]; ok {
    99  			return nil
   100  		}
   101  	}
   102  	idMap := make(map[string]int, len(ids))
   103  	for _, id := range ids {
   104  		idMap[id] = 0
   105  	}
   106  	instances, err := gceClient.Instances("", google.StatusRunning)
   107  	if err != nil {
   108  		return google.HandleCredentialError(errors.Annotate(err, "querying instance details"), ctx)
   109  	}
   110  	for _, instance := range instances {
   111  		if _, ok := idMap[instance.ID]; !ok {
   112  			continue
   113  		}
   114  		c[instance.ID] = instance
   115  	}
   116  	return nil
   117  }
   118  
   119  func (c instanceCache) get(id string) (google.Instance, error) {
   120  	inst, ok := c[id]
   121  	if !ok {
   122  		return google.Instance{}, errors.Errorf("cannot attach to non-running instance %v", id)
   123  	}
   124  	return inst, nil
   125  }
   126  
   127  func (v *volumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) {
   128  	results := make([]storage.CreateVolumesResult, len(params))
   129  	instanceIds := set.NewStrings()
   130  	for i, p := range params {
   131  		if err := v.ValidateVolumeParams(p); err != nil {
   132  			results[i].Error = err
   133  			continue
   134  		}
   135  		instanceIds.Add(string(p.Attachment.InstanceId))
   136  	}
   137  
   138  	instances := make(instanceCache)
   139  	if instanceIds.Size() > 1 {
   140  		if err := instances.update(v.gce, ctx, instanceIds.Values()...); err != nil {
   141  			logger.Debugf("querying running instances: %v", err)
   142  			// We ignore the error, because we don't want an invalid
   143  			// InstanceId reference from one VolumeParams to prevent
   144  			// the creation of another volume.
   145  			// ... Unless the error is due to an invalid credential, in which case, continuing with this call
   146  			// is pointless and creates an unnecessary churn: we know all calls will fail with the same error.
   147  			if google.HasDenialStatusCode(err) {
   148  				return results, err
   149  			}
   150  		}
   151  	}
   152  
   153  	for i, p := range params {
   154  		if results[i].Error != nil {
   155  			continue
   156  		}
   157  		volume, attachment, err := v.createOneVolume(ctx, p, instances)
   158  		if err != nil {
   159  			results[i].Error = err
   160  			logger.Errorf("could not create one volume (or attach it): %v", err)
   161  			// ... Unless the error is due to an invalid credential, in which case, continuing with this call
   162  			// is pointless and creates an unnecessary churn: we know all calls will fail with the same error.
   163  			if google.HasDenialStatusCode(err) {
   164  				return results, err
   165  			}
   166  			continue
   167  		}
   168  		results[i].Volume = volume
   169  		results[i].VolumeAttachment = attachment
   170  	}
   171  	return results, nil
   172  }
   173  
   174  // mibToGib converts mebibytes to gibibytes.
   175  // GCE expects GiB, we work in MiB; round up
   176  // to nearest GiB.
   177  func mibToGib(m uint64) uint64 {
   178  	return (m + 1023) / 1024
   179  }
   180  
   181  func nameVolume(zone string) (string, error) {
   182  	volumeUUID, err := utils.NewUUID()
   183  	if err != nil {
   184  		return "", errors.Annotate(err, "cannot generate uuid to name the volume")
   185  	}
   186  	// type-zone-uuid
   187  	volumeName := fmt.Sprintf("%s--%s", zone, volumeUUID.String())
   188  	return volumeName, nil
   189  }
   190  
   191  func (v *volumeSource) createOneVolume(ctx context.ProviderCallContext, p storage.VolumeParams, instances instanceCache) (volume *storage.Volume, volumeAttachment *storage.VolumeAttachment, err error) {
   192  	var volumeName, zone string
   193  	defer func() {
   194  		if err == nil || volumeName == "" {
   195  			return
   196  		}
   197  		if err := v.gce.RemoveDisk(zone, volumeName); err != nil {
   198  			logger.Errorf("error cleaning up volume %v: %v", volumeName, google.HandleCredentialError(err, ctx))
   199  		}
   200  	}()
   201  
   202  	instId := string(p.Attachment.InstanceId)
   203  	if err := instances.update(v.gce, ctx, instId); err != nil {
   204  		return nil, nil, errors.Annotatef(err, "cannot add %q to instance cache", instId)
   205  	}
   206  	inst, err := instances.get(instId)
   207  	if err != nil {
   208  		// Can't create the volume without the instance,
   209  		// because we need to know what its AZ is.
   210  		return nil, nil, errors.Annotatef(err, "cannot obtain %q from instance cache", instId)
   211  	}
   212  	persistentType, ok := p.Attributes["type"].(google.DiskType)
   213  	if !ok {
   214  		persistentType = google.DiskPersistentStandard
   215  	}
   216  
   217  	zone = inst.ZoneName
   218  	volumeName, err = nameVolume(zone)
   219  	if err != nil {
   220  		return nil, nil, errors.Annotate(err, "cannot create a new volume name")
   221  	}
   222  	// TODO(perrito666) the volumeName is arbitrary and it was crafted this
   223  	// way to help solve the need to have zone all over the place.
   224  	disk := google.DiskSpec{
   225  		SizeHintGB:         mibToGib(p.Size),
   226  		Name:               volumeName,
   227  		PersistentDiskType: persistentType,
   228  		Labels:             resourceTagsToDiskLabels(p.ResourceTags),
   229  	}
   230  
   231  	gceDisks, err := v.gce.CreateDisks(zone, []google.DiskSpec{disk})
   232  	if err != nil {
   233  		return nil, nil, google.HandleCredentialError(errors.Annotate(err, "cannot create disk"), ctx)
   234  	}
   235  	if len(gceDisks) != 1 {
   236  		return nil, nil, errors.New(fmt.Sprintf("unexpected number of disks created: %d", len(gceDisks)))
   237  	}
   238  	gceDisk := gceDisks[0]
   239  
   240  	attachedDisk, err := v.attachOneVolume(ctx, gceDisk.Name, google.ModeRW, inst.ID)
   241  	if err != nil {
   242  		return nil, nil, errors.Annotatef(err, "attaching %q to %q", gceDisk.Name, instId)
   243  	}
   244  
   245  	volume = &storage.Volume{
   246  		p.Tag,
   247  		storage.VolumeInfo{
   248  			VolumeId:   gceDisk.Name,
   249  			Size:       gceDisk.Size,
   250  			Persistent: true,
   251  		},
   252  	}
   253  
   254  	volumeAttachment = &storage.VolumeAttachment{
   255  		p.Tag,
   256  		p.Attachment.Machine,
   257  		storage.VolumeAttachmentInfo{
   258  			DeviceLink: fmt.Sprintf(
   259  				"/dev/disk/by-id/google-%s",
   260  				attachedDisk.DeviceName,
   261  			),
   262  		},
   263  	}
   264  
   265  	return volume, volumeAttachment, nil
   266  }
   267  
   268  func (v *volumeSource) DestroyVolumes(ctx context.ProviderCallContext, volNames []string) ([]error, error) {
   269  	return v.foreachVolume(ctx, volNames, v.destroyOneVolume), nil
   270  }
   271  
   272  func (v *volumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volNames []string) ([]error, error) {
   273  	return v.foreachVolume(ctx, volNames, v.releaseOneVolume), nil
   274  }
   275  
   276  func (v *volumeSource) foreachVolume(ctx context.ProviderCallContext, volNames []string, f func(context.ProviderCallContext, string) error) []error {
   277  	var wg sync.WaitGroup
   278  	wg.Add(len(volNames))
   279  	results := make([]error, len(volNames))
   280  	for i, volumeName := range volNames {
   281  		go func(i int, volumeName string) {
   282  			defer wg.Done()
   283  			results[i] = f(ctx, volumeName)
   284  		}(i, volumeName)
   285  	}
   286  	wg.Wait()
   287  	return results
   288  }
   289  
   290  func parseVolumeId(volName string) (string, string, error) {
   291  	idRest := strings.SplitN(volName, "--", 2)
   292  	if len(idRest) != 2 {
   293  		return "", "", errors.New(fmt.Sprintf("malformed volume id %q", volName))
   294  	}
   295  	zone := idRest[0]
   296  	volumeUUID := idRest[1]
   297  	return zone, volumeUUID, nil
   298  }
   299  
   300  func isValidVolume(volumeName string) bool {
   301  	_, _, err := parseVolumeId(volumeName)
   302  	return err == nil
   303  }
   304  
   305  func (v *volumeSource) destroyOneVolume(ctx context.ProviderCallContext, volName string) error {
   306  	zone, _, err := parseVolumeId(volName)
   307  	if err != nil {
   308  		return errors.Annotatef(err, "invalid volume id %q", volName)
   309  	}
   310  	if err := v.gce.RemoveDisk(zone, volName); err != nil {
   311  		return google.HandleCredentialError(errors.Annotatef(err, "cannot destroy volume %q", volName), ctx)
   312  	}
   313  	return nil
   314  }
   315  
   316  func (v *volumeSource) releaseOneVolume(ctx context.ProviderCallContext, volName string) error {
   317  	zone, _, err := parseVolumeId(volName)
   318  	if err != nil {
   319  		return errors.Annotatef(err, "invalid volume id %q", volName)
   320  	}
   321  	disk, err := v.gce.Disk(zone, volName)
   322  	if err != nil {
   323  		return google.HandleCredentialError(errors.Trace(err), ctx)
   324  	}
   325  	switch disk.Status {
   326  	case google.StatusReady, google.StatusFailed:
   327  	default:
   328  		return errors.Errorf(
   329  			"cannot release volume %q with status %q",
   330  			volName, disk.Status,
   331  		)
   332  	}
   333  	if len(disk.AttachedInstances) > 0 {
   334  		return errors.Errorf(
   335  			"cannot release volume %q, attached to instances %q",
   336  			volName, disk.AttachedInstances,
   337  		)
   338  	}
   339  	delete(disk.Labels, tags.JujuController)
   340  	delete(disk.Labels, tags.JujuModel)
   341  	if err := v.gce.SetDiskLabels(zone, volName, disk.LabelFingerprint, disk.Labels); err != nil {
   342  		return google.HandleCredentialError(errors.Annotatef(err, "cannot remove labels from volume %q", volName), ctx)
   343  	}
   344  	return nil
   345  }
   346  
   347  func (v *volumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) {
   348  	var volumes []string
   349  	disks, err := v.gce.Disks()
   350  	if err != nil {
   351  		return nil, google.HandleCredentialError(errors.Trace(err), ctx)
   352  	}
   353  	for _, disk := range disks {
   354  		if !isValidVolume(disk.Name) {
   355  			continue
   356  		}
   357  		if disk.Labels[tags.JujuModel] != v.modelUUID {
   358  			continue
   359  		}
   360  		volumes = append(volumes, disk.Name)
   361  	}
   362  	return volumes, nil
   363  }
   364  
   365  // ImportVolume is specified on the storage.VolumeImporter interface.
   366  func (v *volumeSource) ImportVolume(ctx context.ProviderCallContext, volName string, tags map[string]string) (storage.VolumeInfo, error) {
   367  	zone, _, err := parseVolumeId(volName)
   368  	if err != nil {
   369  		return storage.VolumeInfo{}, errors.Annotatef(err, "cannot get volume %q", volName)
   370  	}
   371  	disk, err := v.gce.Disk(zone, volName)
   372  	if err != nil {
   373  		return storage.VolumeInfo{}, google.HandleCredentialError(errors.Annotatef(err, "cannot get volume %q", volName), ctx)
   374  	}
   375  	if disk.Status != google.StatusReady {
   376  		return storage.VolumeInfo{}, errors.Errorf(
   377  			"cannot import volume %q with status %q",
   378  			volName, disk.Status,
   379  		)
   380  	}
   381  	if disk.Labels == nil {
   382  		disk.Labels = make(map[string]string)
   383  	}
   384  	for k, v := range resourceTagsToDiskLabels(tags) {
   385  		disk.Labels[k] = v
   386  	}
   387  	if err := v.gce.SetDiskLabels(zone, volName, disk.LabelFingerprint, disk.Labels); err != nil {
   388  		return storage.VolumeInfo{}, google.HandleCredentialError(errors.Annotatef(err, "cannot update labels on volume %q", volName), ctx)
   389  	}
   390  	return storage.VolumeInfo{
   391  		VolumeId:   disk.Name,
   392  		Size:       disk.Size,
   393  		Persistent: true,
   394  	}, nil
   395  }
   396  
   397  func (v *volumeSource) DescribeVolumes(ctx context.ProviderCallContext, volNames []string) ([]storage.DescribeVolumesResult, error) {
   398  	results := make([]storage.DescribeVolumesResult, len(volNames))
   399  	for i, vol := range volNames {
   400  		res, err := v.describeOneVolume(ctx, vol)
   401  		if err != nil {
   402  			return nil, errors.Annotate(err, "cannot describe volumes")
   403  		}
   404  		results[i] = res
   405  	}
   406  	return results, nil
   407  }
   408  
   409  func (v *volumeSource) describeOneVolume(ctx context.ProviderCallContext, volName string) (storage.DescribeVolumesResult, error) {
   410  	zone, _, err := parseVolumeId(volName)
   411  	if err != nil {
   412  		return storage.DescribeVolumesResult{}, errors.Annotatef(err, "cannot describe %q", volName)
   413  	}
   414  	disk, err := v.gce.Disk(zone, volName)
   415  	if err != nil {
   416  		return storage.DescribeVolumesResult{}, google.HandleCredentialError(errors.Annotatef(err, "cannot get volume %q", volName), ctx)
   417  	}
   418  	desc := storage.DescribeVolumesResult{
   419  		&storage.VolumeInfo{
   420  			Size:     disk.Size,
   421  			VolumeId: disk.Name,
   422  		},
   423  		nil,
   424  	}
   425  	return desc, nil
   426  }
   427  
   428  // TODO(perrito666) These rules are yet to be defined.
   429  func (v *volumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   430  	return nil
   431  }
   432  
   433  func (v *volumeSource) AttachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   434  	results := make([]storage.AttachVolumesResult, len(attachParams))
   435  	for i, attachment := range attachParams {
   436  		volumeName := attachment.VolumeId
   437  		mode := google.ModeRW
   438  		if attachment.ReadOnly {
   439  			mode = google.ModeRW
   440  		}
   441  		instanceId := attachment.InstanceId
   442  		attached, err := v.attachOneVolume(ctx, volumeName, mode, string(instanceId))
   443  		if err != nil {
   444  			logger.Errorf("could not attach %q to %q: %v", volumeName, instanceId, err)
   445  			results[i].Error = err
   446  			// ... Unless the error is due to an invalid credential, in which case, continuing with this call
   447  			// is pointless and creates an unnecessary churn: we know all calls will fail with the same error.
   448  			if google.HasDenialStatusCode(err) {
   449  				return results, err
   450  			}
   451  			continue
   452  		}
   453  		results[i].VolumeAttachment = &storage.VolumeAttachment{
   454  			attachment.Volume,
   455  			attachment.Machine,
   456  			storage.VolumeAttachmentInfo{
   457  				DeviceLink: fmt.Sprintf(
   458  					"/dev/disk/by-id/google-%s",
   459  					attached.DeviceName,
   460  				),
   461  			},
   462  		}
   463  	}
   464  	return results, nil
   465  }
   466  
   467  func (v *volumeSource) attachOneVolume(ctx context.ProviderCallContext, volumeName string, mode google.DiskMode, instanceId string) (*google.AttachedDisk, error) {
   468  	zone, _, err := parseVolumeId(volumeName)
   469  	if err != nil {
   470  		return nil, errors.Annotate(err, "invalid volume name")
   471  	}
   472  	instanceDisks, err := v.gce.InstanceDisks(zone, instanceId)
   473  	if err != nil {
   474  		return nil, google.HandleCredentialError(errors.Annotate(err, "cannot verify if the disk is already in the instance"), ctx)
   475  	}
   476  	// Is it already attached?
   477  	for _, disk := range instanceDisks {
   478  		if disk.VolumeName == volumeName {
   479  			return disk, nil
   480  		}
   481  	}
   482  
   483  	attachment, err := v.gce.AttachDisk(zone, volumeName, instanceId, mode)
   484  	if err != nil {
   485  		return nil, google.HandleCredentialError(errors.Annotate(err, "cannot attach volume"), ctx)
   486  	}
   487  	return attachment, nil
   488  }
   489  
   490  func (v *volumeSource) DetachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) {
   491  	result := make([]error, len(attachParams))
   492  	for i, volumeAttachment := range attachParams {
   493  		err := v.detachOneVolume(ctx, volumeAttachment)
   494  		if google.HasDenialStatusCode(err) {
   495  			// no need to continue as we'll keep getting the same invalid credential error.
   496  			return result, err
   497  		}
   498  		result[i] = err
   499  	}
   500  	return result, nil
   501  }
   502  
   503  func (v *volumeSource) detachOneVolume(ctx context.ProviderCallContext, attachParam storage.VolumeAttachmentParams) error {
   504  	instId := attachParam.InstanceId
   505  	volumeName := attachParam.VolumeId
   506  	zone, _, err := parseVolumeId(volumeName)
   507  	if err != nil {
   508  		return errors.Annotatef(err, "%q is not a valid volume id", volumeName)
   509  	}
   510  	return google.HandleCredentialError(v.gce.DetachDisk(zone, string(instId), volumeName), ctx)
   511  }
   512  
   513  // resourceTagsToDiskLabels translates a set of
   514  // resource tags, provided by Juju, to disk labels.
   515  func resourceTagsToDiskLabels(in map[string]string) map[string]string {
   516  	out := make(map[string]string)
   517  	for k, v := range in {
   518  		// Only the controller and model UUID tags are carried
   519  		// over, as they're known not to conflict with GCE's
   520  		// rules regarding label values.
   521  		switch k {
   522  		case tags.JujuController, tags.JujuModel:
   523  			out[k] = v
   524  		}
   525  	}
   526  	return out
   527  }