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

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package openstack
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"net/url"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/go-goose/goose/v5/cinder"
    14  	gooseerrors "github.com/go-goose/goose/v5/errors"
    15  	"github.com/go-goose/goose/v5/identity"
    16  	"github.com/go-goose/goose/v5/nova"
    17  	"github.com/juju/collections/set"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/schema"
    20  	"github.com/juju/utils/v3"
    21  
    22  	"github.com/juju/juju/core/instance"
    23  	"github.com/juju/juju/environs/context"
    24  	"github.com/juju/juju/environs/tags"
    25  	"github.com/juju/juju/provider/common"
    26  	"github.com/juju/juju/storage"
    27  )
    28  
    29  const (
    30  	CinderProviderType = storage.ProviderType("cinder")
    31  
    32  	cinderVolumeType = "volume-type"
    33  
    34  	// autoAssignedMountPoint specifies the value to pass in when
    35  	// you'd like Cinder to automatically assign a mount point.
    36  	autoAssignedMountPoint = ""
    37  
    38  	volumeStatusAvailable = "available"
    39  	volumeStatusDeleting  = "deleting"
    40  	volumeStatusError     = "error"
    41  	volumeStatusInUse     = "in-use"
    42  )
    43  
    44  var cinderConfigFields = schema.Fields{
    45  	cinderVolumeType: schema.String(),
    46  }
    47  
    48  var cinderConfigChecker = schema.FieldMap(
    49  	cinderConfigFields,
    50  	schema.Defaults{
    51  		cinderVolumeType: schema.Omit,
    52  	},
    53  )
    54  
    55  type cinderConfig struct {
    56  	volumeType string
    57  }
    58  
    59  func newCinderConfig(attrs map[string]interface{}) (*cinderConfig, error) {
    60  	out, err := cinderConfigChecker.Coerce(attrs, nil)
    61  	if err != nil {
    62  		return nil, errors.Annotate(err, "validating Cinder storage config")
    63  	}
    64  	coerced := out.(map[string]interface{})
    65  	volumeType, _ := coerced[cinderVolumeType].(string)
    66  	cinderConfig := &cinderConfig{
    67  		volumeType: volumeType,
    68  	}
    69  	return cinderConfig, nil
    70  }
    71  
    72  // StorageProviderTypes implements storage.ProviderRegistry.
    73  func (e *Environ) StorageProviderTypes() ([]storage.ProviderType, error) {
    74  	var types []storage.ProviderType
    75  	if _, err := e.cinderProvider(); err == nil {
    76  		types = append(types, CinderProviderType)
    77  	} else if !errors.IsNotSupported(err) {
    78  		return nil, errors.Trace(err)
    79  	}
    80  	return types, nil
    81  }
    82  
    83  // StorageProvider implements storage.ProviderRegistry.
    84  func (e *Environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    85  	if t != CinderProviderType {
    86  		return nil, errors.NotFoundf("storage provider %q", t)
    87  	}
    88  	return e.cinderProvider()
    89  }
    90  
    91  func (e *Environ) cinderProvider() (*cinderProvider, error) {
    92  	storageAdapter, err := newOpenstackStorage(e)
    93  	if err != nil {
    94  		return nil, errors.Trace(err)
    95  	}
    96  	return &cinderProvider{
    97  		storageAdapter: storageAdapter,
    98  		envName:        e.name,
    99  		modelUUID:      e.uuid,
   100  		namespace:      e.namespace,
   101  		zonedEnv:       e,
   102  	}, nil
   103  }
   104  
   105  var newOpenstackStorage = func(env *Environ) (OpenstackStorage, error) {
   106  	env.ecfgMutex.Lock()
   107  	defer env.ecfgMutex.Unlock()
   108  
   109  	client := env.clientUnlocked
   110  	if env.volumeURL == nil {
   111  		url, err := getVolumeEndpointURL(client, env.cloudUnlocked.Region)
   112  		if IsNotFoundError(err) {
   113  			// No volume endpoint found; Cinder is not supported.
   114  			return nil, errors.NotSupportedf("volumes")
   115  		} else if err != nil {
   116  			return nil, errors.Trace(err)
   117  		}
   118  		env.volumeURL = url
   119  		logger.Debugf("volume URL: %v", url)
   120  	}
   121  
   122  	// TODO (stickupkid): Move this to the ClientFactory.
   123  	// We shouldn't have another wrapper around an existing client.
   124  	cinderCl := cinderClient{cinder.Basic(env.volumeURL, client.TenantId(), client.Token)}
   125  
   126  	cloudSpec := env.cloudUnlocked
   127  	if len(cloudSpec.CACertificates) > 0 {
   128  		cinderCl = cinderClient{cinder.BasicTLSConfig(
   129  			env.volumeURL,
   130  			client.TenantId(),
   131  			client.Token,
   132  			tlsConfig(cloudSpec.CACertificates)),
   133  		}
   134  	}
   135  
   136  	return &openstackStorageAdapter{
   137  		cinderCl,
   138  		novaClient{env.novaUnlocked},
   139  	}, nil
   140  }
   141  
   142  type cinderProvider struct {
   143  	storageAdapter OpenstackStorage
   144  	envName        string
   145  	modelUUID      string
   146  	namespace      instance.Namespace
   147  	zonedEnv       common.ZonedEnviron
   148  }
   149  
   150  var _ storage.Provider = (*cinderProvider)(nil)
   151  
   152  var cinderAttempt = utils.AttemptStrategy{
   153  	Total: 1 * time.Minute,
   154  	Delay: 5 * time.Second,
   155  }
   156  
   157  // VolumeSource implements storage.Provider.
   158  func (p *cinderProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) {
   159  	if err := p.ValidateConfig(providerConfig); err != nil {
   160  		return nil, err
   161  	}
   162  	source := &cinderVolumeSource{
   163  		storageAdapter: p.storageAdapter,
   164  		envName:        p.envName,
   165  		modelUUID:      p.modelUUID,
   166  		namespace:      p.namespace,
   167  		zonedEnv:       p.zonedEnv,
   168  	}
   169  	return source, nil
   170  }
   171  
   172  // FilesystemSource implements storage.Provider.
   173  func (p *cinderProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
   174  	return nil, errors.NotSupportedf("filesystems")
   175  }
   176  
   177  // Supports implements storage.Provider.
   178  func (p *cinderProvider) Supports(kind storage.StorageKind) bool {
   179  	switch kind {
   180  	case storage.StorageKindBlock:
   181  		return true
   182  	}
   183  	return false
   184  }
   185  
   186  // Scope implements storage.Provider.
   187  func (s *cinderProvider) Scope() storage.Scope {
   188  	return storage.ScopeEnviron
   189  }
   190  
   191  func (p *cinderProvider) ValidateForK8s(map[string]any) error {
   192  	return errors.NotValidf("storage provider type %q", CinderProviderType)
   193  }
   194  
   195  // ValidateConfig implements storage.Provider.
   196  func (p *cinderProvider) ValidateConfig(cfg *storage.Config) error {
   197  	// TODO(axw) 2015-05-01 #1450737
   198  	// Reject attempts to create non-persistent volumes.
   199  	_, err := newCinderConfig(cfg.Attrs())
   200  	return errors.Trace(err)
   201  }
   202  
   203  // Dynamic implements storage.Provider.
   204  func (p *cinderProvider) Dynamic() bool {
   205  	return true
   206  }
   207  
   208  // Releasable is defined on the Provider interface.
   209  func (*cinderProvider) Releasable() bool {
   210  	return true
   211  }
   212  
   213  // DefaultPools implements storage.Provider.
   214  func (p *cinderProvider) DefaultPools() []*storage.Config {
   215  	return nil
   216  }
   217  
   218  type cinderVolumeSource struct {
   219  	storageAdapter OpenstackStorage
   220  	envName        string // non unique, informational only
   221  	modelUUID      string
   222  	namespace      instance.Namespace
   223  	zonedEnv       common.ZonedEnviron
   224  }
   225  
   226  var _ storage.VolumeSource = (*cinderVolumeSource)(nil)
   227  
   228  // CreateVolumes implements storage.VolumeSource.
   229  func (s *cinderVolumeSource) CreateVolumes(
   230  	ctx context.ProviderCallContext, args []storage.VolumeParams,
   231  ) ([]storage.CreateVolumesResult, error) {
   232  	results := make([]storage.CreateVolumesResult, len(args))
   233  	for i, arg := range args {
   234  		volume, err := s.createVolume(ctx, arg)
   235  		if err != nil {
   236  			results[i].Error = errors.Trace(err)
   237  			if denied := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx); denied {
   238  				// If it is an unauthorised error, no need to continue since we will 100% fail...
   239  				break
   240  			}
   241  			continue
   242  		}
   243  		results[i].Volume = volume
   244  	}
   245  	return results, nil
   246  }
   247  
   248  func (s *cinderVolumeSource) createVolume(
   249  	ctx context.ProviderCallContext, arg storage.VolumeParams) (*storage.Volume, error) {
   250  	cinderConfig, err := newCinderConfig(arg.Attributes)
   251  	if err != nil {
   252  		return nil, errors.Trace(err)
   253  	}
   254  
   255  	var metadata interface{}
   256  	if len(arg.ResourceTags) > 0 {
   257  		metadata = arg.ResourceTags
   258  	}
   259  
   260  	az, err := s.availabilityZoneForVolume(ctx, arg.Tag.Id(), arg.Attachment)
   261  	if err != nil {
   262  		return nil, errors.Trace(err)
   263  	}
   264  	cinderVolume, err := s.storageAdapter.CreateVolume(cinder.CreateVolumeVolumeParams{
   265  		// The Cinder documentation incorrectly states the
   266  		// size parameter is in GB. It is actually GiB.
   267  		Size:             int(math.Ceil(float64(arg.Size / 1024))),
   268  		Name:             resourceName(s.namespace, s.envName, arg.Tag.String()),
   269  		VolumeType:       cinderConfig.volumeType,
   270  		AvailabilityZone: az,
   271  		Metadata:         metadata,
   272  	})
   273  	if err != nil {
   274  		return nil, errors.Trace(err)
   275  	}
   276  
   277  	// The response may (will?) come back before the volume transitions to
   278  	// "creating", in which case it will not have a size or status.
   279  	// Wait for the volume to transition, so we can record its actual size.
   280  	volumeId := cinderVolume.ID
   281  	cinderVolume, err = waitVolume(s.storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
   282  		return v.Status != "", nil
   283  	})
   284  	if err != nil {
   285  		if err := s.storageAdapter.DeleteVolume(volumeId); err != nil {
   286  			logger.Warningf("destroying volume %s: %s", volumeId, err)
   287  		}
   288  		return nil, errors.Errorf("waiting for volume to be provisioned: %s", err)
   289  	}
   290  	logger.Debugf("created volume: %+v", cinderVolume)
   291  	return &storage.Volume{Tag: arg.Tag, VolumeInfo: cinderToJujuVolumeInfo(cinderVolume)}, nil
   292  }
   293  
   294  func (s *cinderVolumeSource) availabilityZoneForVolume(
   295  	ctx context.ProviderCallContext, volName string, attachment *storage.VolumeAttachmentParams,
   296  ) (string, error) {
   297  	// If this volume is being attached to an instance, attempt to provision
   298  	// the storage in the same availability zone.
   299  	// This helps to avoid a situation with all storage residing in a single
   300  	// AZ that upon failure would effectively take down attached instances
   301  	// whatever zone they were in.
   302  	// However, we first attempt to query the possible volume availability zones.
   303  	// If the API is old and does not support explicit volume AZs, or volumes can
   304  	// only be provisioned in say the default "nova" zone and the instance is in
   305  	// a different zone, we won't attempt to use the instance zone because that
   306  	// won't work and we'll get a 400 error back.
   307  	if attachment == nil || attachment.InstanceId == "" {
   308  		return "", nil
   309  	}
   310  
   311  	volumeZones, err := s.storageAdapter.ListVolumeAvailabilityZones()
   312  	if err != nil && !gooseerrors.IsNotImplemented(err) {
   313  		logger.Infof("block volume zones not supported, not using availability zone for volume %q", volName)
   314  		return "", errors.Trace(err)
   315  	}
   316  	vZones := set.NewStrings()
   317  	for _, vz := range volumeZones {
   318  		if vz.State.Available {
   319  			vZones.Add(vz.Name)
   320  		}
   321  	}
   322  	if vZones.Size() == 0 {
   323  		logger.Infof("no block volume zones defined, not using availability zone for volume %q", volName)
   324  		return "", nil
   325  	}
   326  	logger.Debugf("possible block volume zones: %v", vZones.SortedValues())
   327  	aZones, err := s.zonedEnv.InstanceAvailabilityZoneNames(ctx, []instance.Id{attachment.InstanceId})
   328  	if err != nil {
   329  		return "", errors.Trace(err)
   330  	}
   331  	if len(aZones) == 0 {
   332  		// All instances should have an availability zone.
   333  		// The default is "nova" so something is wrong if nothing
   334  		// is returned from this call.
   335  		logger.Warningf("no availability zone detected for instance %q", attachment.InstanceId)
   336  		return "", nil
   337  	}
   338  	// Only choose an AZ from the instance if there's a matching volume AZ.
   339  	var az string
   340  	for _, az = range aZones {
   341  		break
   342  	}
   343  	if vZones.Contains(az) {
   344  		logger.Debugf("using availability zone %q to create cinder volume %q", az, volName)
   345  		return az, nil
   346  	}
   347  	logger.Warningf("no compatible availability zone detected for volume %q", volName)
   348  	return "", nil
   349  }
   350  
   351  // ListVolumes is specified on the storage.VolumeSource interface.
   352  func (s *cinderVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) {
   353  	cinderVolumes, err := modelCinderVolumes(s.storageAdapter, s.modelUUID)
   354  	if err != nil {
   355  		handleCredentialError(err, ctx)
   356  		return nil, errors.Trace(err)
   357  	}
   358  	return volumeInfoToVolumeIds(cinderToJujuVolumeInfos(cinderVolumes)), nil
   359  }
   360  
   361  // modelCinderVolumes returns all of the cinder volumes for the model.
   362  func modelCinderVolumes(storageAdapter OpenstackStorage, modelUUID string) ([]cinder.Volume, error) {
   363  	return cinderVolumes(storageAdapter, func(v *cinder.Volume) bool {
   364  		return v.Metadata[tags.JujuModel] == modelUUID
   365  	})
   366  }
   367  
   368  // controllerCinderVolumes returns all of the cinder volumes for the model.
   369  func controllerCinderVolumes(storageAdapter OpenstackStorage, controllerUUID string) ([]cinder.Volume, error) {
   370  	return cinderVolumes(storageAdapter, func(v *cinder.Volume) bool {
   371  		return v.Metadata[tags.JujuController] == controllerUUID
   372  	})
   373  }
   374  
   375  // cinderVolumes returns all of the cinder volumes matching the given predicate.
   376  func cinderVolumes(storageAdapter OpenstackStorage, pred func(*cinder.Volume) bool) ([]cinder.Volume, error) {
   377  	allCinderVolumes, err := storageAdapter.GetVolumesDetail()
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	var matching []cinder.Volume
   382  	for _, v := range allCinderVolumes {
   383  		if pred(&v) {
   384  			matching = append(matching, v)
   385  		}
   386  	}
   387  	return matching, nil
   388  }
   389  
   390  func volumeInfoToVolumeIds(volumes []storage.VolumeInfo) []string {
   391  	volumeIds := make([]string, len(volumes))
   392  	for i, volume := range volumes {
   393  		volumeIds[i] = volume.VolumeId
   394  	}
   395  	return volumeIds
   396  }
   397  
   398  // DescribeVolumes implements storage.VolumeSource.
   399  func (s *cinderVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) {
   400  	// In most cases, it is quicker to get all volumes and loop
   401  	// locally than to make several round-trips to the provider.
   402  	cinderVolumes, err := s.storageAdapter.GetVolumesDetail()
   403  	if err != nil {
   404  		handleCredentialError(err, ctx)
   405  		return nil, errors.Trace(err)
   406  	}
   407  	volumesById := make(map[string]*cinder.Volume)
   408  	for i, volume := range cinderVolumes {
   409  		volumesById[volume.ID] = &cinderVolumes[i]
   410  	}
   411  	results := make([]storage.DescribeVolumesResult, len(volumeIds))
   412  	for i, volumeId := range volumeIds {
   413  		cinderVolume, ok := volumesById[volumeId]
   414  		if !ok {
   415  			results[i].Error = errors.NotFoundf("volume %q", volumeId)
   416  			continue
   417  		}
   418  		info := cinderToJujuVolumeInfo(cinderVolume)
   419  		results[i].VolumeInfo = &info
   420  	}
   421  	return results, nil
   422  }
   423  
   424  // DestroyVolumes implements storage.VolumeSource.
   425  func (s *cinderVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   426  	return foreachVolume(ctx, s.storageAdapter, volumeIds, destroyVolume), nil
   427  }
   428  
   429  // ReleaseVolumes implements storage.VolumeSource.
   430  func (s *cinderVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   431  	return foreachVolume(ctx, s.storageAdapter, volumeIds, releaseVolume), nil
   432  }
   433  
   434  func foreachVolume(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, volumeIds []string, f func(context.ProviderCallContext, OpenstackStorage, string) error) []error {
   435  	var wg sync.WaitGroup
   436  	wg.Add(len(volumeIds))
   437  	results := make([]error, len(volumeIds))
   438  	for i, volumeId := range volumeIds {
   439  		go func(i int, volumeId string) {
   440  			defer wg.Done()
   441  			results[i] = f(ctx, storageAdapter, volumeId)
   442  		}(i, volumeId)
   443  	}
   444  	wg.Wait()
   445  	return results
   446  }
   447  
   448  func destroyVolume(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, volumeId string) error {
   449  	logger.Debugf("destroying volume %q", volumeId)
   450  	// Volumes must not be in-use when destroying. A volume may
   451  	// still be in-use when the instance it is attached to is
   452  	// in the process of being terminated.
   453  	var issuedDetach bool
   454  	volume, err := waitVolume(storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
   455  		switch v.Status {
   456  		default:
   457  			// Not ready for deletion; keep waiting.
   458  			return false, nil
   459  		case volumeStatusAvailable, volumeStatusDeleting, volumeStatusError:
   460  			return true, nil
   461  		case volumeStatusInUse:
   462  			// Detach below.
   463  			break
   464  		}
   465  		// Volume is still attached, so detach it.
   466  		if !issuedDetach {
   467  			args := make([]storage.VolumeAttachmentParams, len(v.Attachments))
   468  			for i, a := range v.Attachments {
   469  				args[i].VolumeId = volumeId
   470  				args[i].InstanceId = instance.Id(a.ServerId)
   471  			}
   472  			if len(args) > 0 {
   473  				results := detachVolumes(ctx, storageAdapter, args)
   474  				for _, err := range results {
   475  					if err != nil {
   476  						return false, errors.Trace(err)
   477  					}
   478  				}
   479  			}
   480  			issuedDetach = true
   481  		}
   482  		return false, nil
   483  	})
   484  	if err != nil {
   485  		if IsNotFoundError(err) {
   486  			// The volume wasn't found; nothing
   487  			// to destroy, so we're done.
   488  			return nil
   489  		}
   490  		handleCredentialError(err, ctx)
   491  		return errors.Trace(err)
   492  	}
   493  	if volume.Status == volumeStatusDeleting {
   494  		// Already being deleted, nothing to do.
   495  		return nil
   496  	}
   497  	if err := storageAdapter.DeleteVolume(volumeId); err != nil {
   498  		handleCredentialError(err, ctx)
   499  		return errors.Trace(err)
   500  	}
   501  	return nil
   502  }
   503  
   504  func releaseVolume(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, volumeId string) error {
   505  	logger.Debugf("releasing volume %q", volumeId)
   506  	_, err := waitVolume(storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
   507  		switch v.Status {
   508  		case volumeStatusAvailable, volumeStatusError:
   509  			return true, nil
   510  		case volumeStatusDeleting:
   511  			return false, errors.New("volume is being deleted")
   512  		case volumeStatusInUse:
   513  			return false, errors.New("volume still in-use")
   514  		}
   515  		// Not ready for releasing; keep waiting.
   516  		return false, nil
   517  	})
   518  	if err != nil {
   519  		handleCredentialError(err, ctx)
   520  		return errors.Annotatef(err, "cannot release volume %q", volumeId)
   521  	}
   522  	// Drop the model and controller tags from the volume.
   523  	tags := map[string]string{
   524  		tags.JujuModel:      "",
   525  		tags.JujuController: "",
   526  	}
   527  	_, err = storageAdapter.SetVolumeMetadata(volumeId, tags)
   528  	handleCredentialError(err, ctx)
   529  	return errors.Annotate(err, "tagging volume")
   530  }
   531  
   532  // ValidateVolumeParams implements storage.VolumeSource.
   533  func (s *cinderVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   534  	_, err := newCinderConfig(params.Attributes)
   535  	return errors.Trace(err)
   536  }
   537  
   538  // AttachVolumes implements storage.VolumeSource.
   539  func (s *cinderVolumeSource) AttachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   540  	results := make([]storage.AttachVolumesResult, len(args))
   541  	for i, arg := range args {
   542  		attachment, err := s.attachVolume(arg)
   543  		if err != nil {
   544  			results[i].Error = errors.Trace(err)
   545  			if denial := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx); denial {
   546  				// We do not want to continue here as we'll 100% fail if we got unauthorised error.
   547  				break
   548  			}
   549  			continue
   550  		}
   551  		results[i].VolumeAttachment = attachment
   552  	}
   553  	return results, nil
   554  }
   555  
   556  func (s *cinderVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) {
   557  	// Check to see if the volume is already attached.
   558  	existingAttachments, err := s.storageAdapter.ListVolumeAttachments(string(arg.InstanceId))
   559  	if err != nil {
   560  		return nil, err
   561  	}
   562  	novaAttachment := findAttachment(arg.VolumeId, existingAttachments)
   563  	if novaAttachment == nil {
   564  		// A volume must be "available" before it can be attached.
   565  		if _, err := waitVolume(s.storageAdapter, arg.VolumeId, func(v *cinder.Volume) (bool, error) {
   566  			return v.Status == "available", nil
   567  		}); err != nil {
   568  			return nil, errors.Annotate(err, "waiting for volume to become available")
   569  		}
   570  		novaAttachment, err = s.storageAdapter.AttachVolume(
   571  			string(arg.InstanceId),
   572  			arg.VolumeId,
   573  			autoAssignedMountPoint,
   574  		)
   575  		if err != nil {
   576  			return nil, err
   577  		}
   578  	}
   579  	if novaAttachment.Device == nil {
   580  		return nil, errors.Errorf("device not assigned to volume attachment")
   581  	}
   582  	return &storage.VolumeAttachment{
   583  		Volume:  arg.Volume,
   584  		Machine: arg.Machine,
   585  		VolumeAttachmentInfo: storage.VolumeAttachmentInfo{
   586  			DeviceName: (*novaAttachment.Device)[len("/dev/"):],
   587  		},
   588  	}, nil
   589  }
   590  
   591  // ImportVolume is part of the storage.VolumeImporter interface.
   592  func (s *cinderVolumeSource) ImportVolume(ctx context.ProviderCallContext, volumeId string, resourceTags map[string]string) (storage.VolumeInfo, error) {
   593  	volume, err := s.storageAdapter.GetVolume(volumeId)
   594  	if err != nil {
   595  		handleCredentialError(err, ctx)
   596  		return storage.VolumeInfo{}, errors.Annotate(err, "getting volume")
   597  	}
   598  	if volume.Status != volumeStatusAvailable {
   599  		return storage.VolumeInfo{}, errors.Errorf(
   600  			"cannot import volume %q with status %q", volumeId, volume.Status,
   601  		)
   602  	}
   603  	if _, err := s.storageAdapter.SetVolumeMetadata(volumeId, resourceTags); err != nil {
   604  		handleCredentialError(err, ctx)
   605  		return storage.VolumeInfo{}, errors.Annotatef(err, "tagging volume %q", volumeId)
   606  	}
   607  	return cinderToJujuVolumeInfo(volume), nil
   608  }
   609  
   610  func waitVolume(
   611  	storageAdapter OpenstackStorage,
   612  	volumeId string,
   613  	pred func(*cinder.Volume) (bool, error),
   614  ) (*cinder.Volume, error) {
   615  	for a := cinderAttempt.Start(); a.Next(); {
   616  		volume, err := storageAdapter.GetVolume(volumeId)
   617  		if err != nil {
   618  			return nil, errors.Annotate(err, "getting volume")
   619  		}
   620  		ok, err := pred(volume)
   621  		if err != nil {
   622  			return nil, errors.Trace(err)
   623  		}
   624  		if ok {
   625  			return volume, nil
   626  		}
   627  	}
   628  	return nil, errors.New("timed out")
   629  }
   630  
   631  // DetachVolumes implements storage.VolumeSource.
   632  func (s *cinderVolumeSource) DetachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]error, error) {
   633  	return detachVolumes(ctx, s.storageAdapter, args), nil
   634  }
   635  
   636  func detachVolumes(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, args []storage.VolumeAttachmentParams) []error {
   637  	results := make([]error, len(args))
   638  	for i, arg := range args {
   639  		if err := detachVolume(
   640  			string(arg.InstanceId),
   641  			arg.VolumeId,
   642  			storageAdapter,
   643  		); err != nil {
   644  			handleCredentialError(err, ctx)
   645  			results[i] = errors.Annotatef(
   646  				err, "detaching volume %s from server %s",
   647  				arg.VolumeId, arg.InstanceId,
   648  			)
   649  			continue
   650  		}
   651  	}
   652  	return results
   653  }
   654  
   655  func cinderToJujuVolumeInfos(volumes []cinder.Volume) []storage.VolumeInfo {
   656  	out := make([]storage.VolumeInfo, len(volumes))
   657  	for i, v := range volumes {
   658  		out[i] = cinderToJujuVolumeInfo(&v)
   659  	}
   660  	return out
   661  }
   662  
   663  func cinderToJujuVolumeInfo(volume *cinder.Volume) storage.VolumeInfo {
   664  	return storage.VolumeInfo{
   665  		VolumeId:   volume.ID,
   666  		Size:       uint64(volume.Size * 1024),
   667  		Persistent: true,
   668  	}
   669  }
   670  
   671  func detachVolume(instanceId, volumeId string, storageAdapter OpenstackStorage) error {
   672  	err := storageAdapter.DetachVolume(instanceId, volumeId)
   673  	if err != nil && !IsNotFoundError(err) {
   674  		return errors.Trace(err)
   675  	}
   676  	// The volume was successfully detached, or was
   677  	// already detached (i.e. NotFound error case).
   678  	return nil
   679  }
   680  
   681  func IsNotFoundError(err error) bool {
   682  	return errors.IsNotFound(err) || gooseerrors.IsNotFound(err)
   683  }
   684  
   685  func findAttachment(volId string, attachments []nova.VolumeAttachment) *nova.VolumeAttachment {
   686  	for _, attachment := range attachments {
   687  		if attachment.VolumeId == volId {
   688  			return &attachment
   689  		}
   690  	}
   691  	return nil
   692  }
   693  
   694  type OpenstackStorage interface {
   695  	GetVolume(volumeId string) (*cinder.Volume, error)
   696  	GetVolumesDetail() ([]cinder.Volume, error)
   697  	DeleteVolume(volumeId string) error
   698  	CreateVolume(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error)
   699  	AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error)
   700  	DetachVolume(serverId, attachmentId string) error
   701  	ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error)
   702  	SetVolumeMetadata(volumeId string, metadata map[string]string) (map[string]string, error)
   703  	ListVolumeAvailabilityZones() ([]cinder.AvailabilityZone, error)
   704  }
   705  
   706  type endpointResolver interface {
   707  	Authenticate() error
   708  	IsAuthenticated() bool
   709  	EndpointsForRegion(region string) identity.ServiceURLs
   710  }
   711  
   712  func getVolumeEndpointURL(client endpointResolver, region string) (*url.URL, error) {
   713  	if !client.IsAuthenticated() {
   714  		if err := authenticateClient(client); err != nil {
   715  			return nil, errors.Trace(err)
   716  		}
   717  	}
   718  	endpointMap := client.EndpointsForRegion(region)
   719  
   720  	// Different versions of block storage in OpenStack have different entries
   721  	// in the service catalog.  Find the most recent version.  If it does exist,
   722  	// fall back to older ones.
   723  	endpoint, ok := endpointMap["volumev3"]
   724  	if ok {
   725  		return url.Parse(endpoint)
   726  	}
   727  	logger.Debugf(`endpoint "volumev3" not found for %q region, trying "volumev2"`, region)
   728  	endpoint, ok = endpointMap["volumev2"]
   729  	if ok {
   730  		return url.Parse(endpoint)
   731  	}
   732  	logger.Debugf(`endpoint "volumev2" not found for %q region, trying "volume"`, region)
   733  	endpoint, ok = endpointMap["volume"]
   734  	if ok {
   735  		return url.Parse(endpoint)
   736  	}
   737  	return nil, errors.NotFoundf(`endpoint "volume" in region %q`, region)
   738  }
   739  
   740  type openstackStorageAdapter struct {
   741  	cinderClient
   742  	novaClient
   743  }
   744  
   745  type cinderClient struct {
   746  	*cinder.Client
   747  }
   748  
   749  type novaClient struct {
   750  	*nova.Client
   751  }
   752  
   753  // CreateVolume is part of the OpenstackStorage interface.
   754  func (ga *openstackStorageAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) {
   755  	resp, err := ga.cinderClient.CreateVolume(args)
   756  	if err != nil {
   757  		return nil, err
   758  	}
   759  	return &resp.Volume, nil
   760  }
   761  
   762  // GetVolumesDetail is part of the OpenstackStorage interface.
   763  func (ga *openstackStorageAdapter) GetVolumesDetail() ([]cinder.Volume, error) {
   764  	resp, err := ga.cinderClient.GetVolumesDetail()
   765  	if err != nil {
   766  		return nil, err
   767  	}
   768  	return resp.Volumes, nil
   769  }
   770  
   771  // GetVolume is part of the OpenstackStorage interface.
   772  func (ga *openstackStorageAdapter) GetVolume(volumeId string) (*cinder.Volume, error) {
   773  	resp, err := ga.cinderClient.GetVolume(volumeId)
   774  	if err != nil {
   775  		if IsNotFoundError(err) {
   776  			return nil, errors.NotFoundf("volume %q", volumeId)
   777  		}
   778  		return nil, err
   779  	}
   780  	return &resp.Volume, nil
   781  }
   782  
   783  // SetVolumeMetadata is part of the OpenstackStorage interface.
   784  func (ga *openstackStorageAdapter) SetVolumeMetadata(volumeId string, metadata map[string]string) (map[string]string, error) {
   785  	return ga.cinderClient.SetVolumeMetadata(volumeId, metadata)
   786  }
   787  
   788  // DeleteVolume is part of the OpenstackStorage interface.
   789  func (ga *openstackStorageAdapter) DeleteVolume(volumeId string) error {
   790  	if err := ga.cinderClient.DeleteVolume(volumeId); err != nil {
   791  		if IsNotFoundError(err) {
   792  			return errors.NotFoundf("volume %q", volumeId)
   793  		}
   794  		return err
   795  	}
   796  	return nil
   797  }
   798  
   799  // DetachVolume is part of the OpenstackStorage interface.
   800  func (ga *openstackStorageAdapter) DetachVolume(serverId, attachmentId string) error {
   801  	if err := ga.novaClient.DetachVolume(serverId, attachmentId); err != nil {
   802  		if IsNotFoundError(err) {
   803  			return errors.NewNotFound(nil,
   804  				fmt.Sprintf("volume %q is not attached to server %q",
   805  					attachmentId, serverId,
   806  				),
   807  			)
   808  		}
   809  		return err
   810  	}
   811  	return nil
   812  }