github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"math"
     8  	"net/url"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils"
    14  	"gopkg.in/goose.v1/cinder"
    15  	"gopkg.in/goose.v1/identity"
    16  	"gopkg.in/goose.v1/nova"
    17  
    18  	"github.com/juju/juju/environs/tags"
    19  	"github.com/juju/juju/instance"
    20  	"github.com/juju/juju/storage"
    21  )
    22  
    23  const (
    24  	CinderProviderType = storage.ProviderType("cinder")
    25  	// autoAssignedMountPoint specifies the value to pass in when
    26  	// you'd like Cinder to automatically assign a mount point.
    27  	autoAssignedMountPoint = ""
    28  
    29  	volumeStatusAvailable = "available"
    30  	volumeStatusDeleting  = "deleting"
    31  	volumeStatusError     = "error"
    32  	volumeStatusInUse     = "in-use"
    33  )
    34  
    35  // StorageProviderTypes implements storage.ProviderRegistry.
    36  func (env *Environ) StorageProviderTypes() ([]storage.ProviderType, error) {
    37  	var types []storage.ProviderType
    38  	if _, err := env.cinderProvider(); err == nil {
    39  		types = append(types, CinderProviderType)
    40  	} else if !errors.IsNotSupported(err) {
    41  		return nil, errors.Trace(err)
    42  	}
    43  	return types, nil
    44  }
    45  
    46  // StorageProvider implements storage.ProviderRegistry.
    47  func (env *Environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    48  	if t != CinderProviderType {
    49  		return nil, errors.NotFoundf("storage provider %q", t)
    50  	}
    51  	return env.cinderProvider()
    52  }
    53  
    54  func (env *Environ) cinderProvider() (*cinderProvider, error) {
    55  	storageAdapter, err := newOpenstackStorage(env)
    56  	if err != nil {
    57  		return nil, errors.Trace(err)
    58  	}
    59  	return &cinderProvider{storageAdapter, env.name, env.uuid}, nil
    60  }
    61  
    62  var newOpenstackStorage = func(env *Environ) (OpenstackStorage, error) {
    63  	env.ecfgMutex.Lock()
    64  	defer env.ecfgMutex.Unlock()
    65  
    66  	if env.volumeURL == nil {
    67  		url, err := getVolumeEndpointURL(env.client, env.cloud.Region)
    68  		if errors.IsNotFound(err) {
    69  			// No volume endpoint found; Cinder is not supported.
    70  			return nil, errors.NotSupportedf("volumes")
    71  		} else if err != nil {
    72  			return nil, errors.Trace(err)
    73  		}
    74  		env.volumeURL = url
    75  		logger.Debugf("volume URL: %v", url)
    76  	}
    77  
    78  	return &openstackStorageAdapter{
    79  		cinderClient{cinder.Basic(env.volumeURL, env.client.TenantId(), env.client.Token)},
    80  		novaClient{env.novaUnlocked},
    81  	}, nil
    82  }
    83  
    84  type cinderProvider struct {
    85  	storageAdapter OpenstackStorage
    86  	envName        string
    87  	modelUUID      string
    88  }
    89  
    90  var _ storage.Provider = (*cinderProvider)(nil)
    91  
    92  var cinderAttempt = utils.AttemptStrategy{
    93  	Total: 1 * time.Minute,
    94  	Delay: 5 * time.Second,
    95  }
    96  
    97  // VolumeSource implements storage.Provider.
    98  func (p *cinderProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) {
    99  	if err := p.ValidateConfig(providerConfig); err != nil {
   100  		return nil, err
   101  	}
   102  	source := &cinderVolumeSource{
   103  		storageAdapter: p.storageAdapter,
   104  		envName:        p.envName,
   105  		modelUUID:      p.modelUUID,
   106  	}
   107  	return source, nil
   108  }
   109  
   110  // FilesystemSource implements storage.Provider.
   111  func (p *cinderProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
   112  	return nil, errors.NotSupportedf("filesystems")
   113  }
   114  
   115  // Supports implements storage.Provider.
   116  func (p *cinderProvider) Supports(kind storage.StorageKind) bool {
   117  	switch kind {
   118  	case storage.StorageKindBlock:
   119  		return true
   120  	}
   121  	return false
   122  }
   123  
   124  // Scope implements storage.Provider.
   125  func (s *cinderProvider) Scope() storage.Scope {
   126  	return storage.ScopeEnviron
   127  }
   128  
   129  // ValidateConfig implements storage.Provider.
   130  func (p *cinderProvider) ValidateConfig(cfg *storage.Config) error {
   131  	// TODO(axw) 2015-05-01 #1450737
   132  	// Reject attempts to create non-persistent volumes.
   133  	return nil
   134  }
   135  
   136  // Dynamic implements storage.Provider.
   137  func (p *cinderProvider) Dynamic() bool {
   138  	return true
   139  }
   140  
   141  // DefaultPools implements storage.Provider.
   142  func (p *cinderProvider) DefaultPools() []*storage.Config {
   143  	return nil
   144  }
   145  
   146  type cinderVolumeSource struct {
   147  	storageAdapter OpenstackStorage
   148  	envName        string // non unique, informational only
   149  	modelUUID      string
   150  }
   151  
   152  var _ storage.VolumeSource = (*cinderVolumeSource)(nil)
   153  
   154  // CreateVolumes implements storage.VolumeSource.
   155  func (s *cinderVolumeSource) CreateVolumes(args []storage.VolumeParams) ([]storage.CreateVolumesResult, error) {
   156  	results := make([]storage.CreateVolumesResult, len(args))
   157  	for i, arg := range args {
   158  		volume, err := s.createVolume(arg)
   159  		if err != nil {
   160  			results[i].Error = errors.Trace(err)
   161  			continue
   162  		}
   163  		results[i].Volume = volume
   164  	}
   165  	return results, nil
   166  }
   167  
   168  func (s *cinderVolumeSource) createVolume(arg storage.VolumeParams) (*storage.Volume, error) {
   169  	var metadata interface{}
   170  	if len(arg.ResourceTags) > 0 {
   171  		metadata = arg.ResourceTags
   172  	}
   173  	cinderVolume, err := s.storageAdapter.CreateVolume(cinder.CreateVolumeVolumeParams{
   174  		// The Cinder documentation incorrectly states the
   175  		// size parameter is in GB. It is actually GiB.
   176  		Size: int(math.Ceil(float64(arg.Size / 1024))),
   177  		Name: resourceName(arg.Tag, s.envName),
   178  		// TODO(axw) use the AZ of the initially attached machine.
   179  		AvailabilityZone: "",
   180  		Metadata:         metadata,
   181  	})
   182  	if err != nil {
   183  		return nil, errors.Trace(err)
   184  	}
   185  
   186  	// The response may (will?) come back before the volume transitions to
   187  	// "creating", in which case it will not have a size or status. Wait for
   188  	// the volume to transition, so we can record its actual size.
   189  	volumeId := cinderVolume.ID
   190  	cinderVolume, err = waitVolume(s.storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
   191  		return v.Status != "", nil
   192  	})
   193  	if err != nil {
   194  		if err := s.storageAdapter.DeleteVolume(volumeId); err != nil {
   195  			logger.Warningf("destroying volume %s: %s", volumeId, err)
   196  		}
   197  		return nil, errors.Errorf("waiting for volume to be provisioned: %s", err)
   198  	}
   199  	logger.Debugf("created volume: %+v", cinderVolume)
   200  	return &storage.Volume{arg.Tag, cinderToJujuVolumeInfo(cinderVolume)}, nil
   201  }
   202  
   203  // ListVolumes is specified on the storage.VolumeSource interface.
   204  func (s *cinderVolumeSource) ListVolumes() ([]string, error) {
   205  	volumes, err := listVolumes(s.storageAdapter, func(v *cinder.Volume) bool {
   206  		return v.Metadata[tags.JujuModel] == s.modelUUID
   207  	})
   208  	if err != nil {
   209  		return nil, errors.Trace(err)
   210  	}
   211  	return volumeInfoToVolumeIds(volumes), nil
   212  }
   213  
   214  func volumeInfoToVolumeIds(volumes []storage.VolumeInfo) []string {
   215  	volumeIds := make([]string, len(volumes))
   216  	for i, volume := range volumes {
   217  		volumeIds[i] = volume.VolumeId
   218  	}
   219  	return volumeIds
   220  }
   221  
   222  // listVolumes returns all of the volumes matching the given function.
   223  func listVolumes(storageAdapter OpenstackStorage, match func(*cinder.Volume) bool) ([]storage.VolumeInfo, error) {
   224  	cinderVolumes, err := storageAdapter.GetVolumesDetail()
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  	volumes := make([]storage.VolumeInfo, 0, len(cinderVolumes))
   229  	for _, volume := range cinderVolumes {
   230  		if !match(&volume) {
   231  			continue
   232  		}
   233  		volumes = append(volumes, cinderToJujuVolumeInfo(&volume))
   234  	}
   235  	return volumes, nil
   236  }
   237  
   238  // DescribeVolumes implements storage.VolumeSource.
   239  func (s *cinderVolumeSource) DescribeVolumes(volumeIds []string) ([]storage.DescribeVolumesResult, error) {
   240  	// In most cases, it is quicker to get all volumes and loop
   241  	// locally than to make several round-trips to the provider.
   242  	cinderVolumes, err := s.storageAdapter.GetVolumesDetail()
   243  	if err != nil {
   244  		return nil, errors.Trace(err)
   245  	}
   246  	volumesById := make(map[string]*cinder.Volume)
   247  	for i, volume := range cinderVolumes {
   248  		volumesById[volume.ID] = &cinderVolumes[i]
   249  	}
   250  	results := make([]storage.DescribeVolumesResult, len(volumeIds))
   251  	for i, volumeId := range volumeIds {
   252  		cinderVolume, ok := volumesById[volumeId]
   253  		if !ok {
   254  			results[i].Error = errors.NotFoundf("volume %q", volumeId)
   255  			continue
   256  		}
   257  		info := cinderToJujuVolumeInfo(cinderVolume)
   258  		results[i].VolumeInfo = &info
   259  	}
   260  	return results, nil
   261  }
   262  
   263  // DestroyVolumes implements storage.VolumeSource.
   264  func (s *cinderVolumeSource) DestroyVolumes(volumeIds []string) ([]error, error) {
   265  	return destroyVolumes(s.storageAdapter, volumeIds), nil
   266  }
   267  
   268  func destroyVolumes(storageAdapter OpenstackStorage, volumeIds []string) []error {
   269  	var wg sync.WaitGroup
   270  	wg.Add(len(volumeIds))
   271  	results := make([]error, len(volumeIds))
   272  	for i, volumeId := range volumeIds {
   273  		go func(i int, volumeId string) {
   274  			defer wg.Done()
   275  			results[i] = destroyVolume(storageAdapter, volumeId)
   276  		}(i, volumeId)
   277  	}
   278  	wg.Wait()
   279  	return results
   280  }
   281  
   282  func destroyVolume(storageAdapter OpenstackStorage, volumeId string) error {
   283  	logger.Debugf("destroying volume %q", volumeId)
   284  	// Volumes must not be in-use when destroying. A volume may
   285  	// still be in-use when the instance it is attached to is
   286  	// in the process of being terminated.
   287  	var issuedDetach bool
   288  	volume, err := waitVolume(storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
   289  		switch v.Status {
   290  		default:
   291  			// Not ready for deletion; keep waiting.
   292  			return false, nil
   293  		case volumeStatusAvailable, volumeStatusDeleting, volumeStatusError:
   294  			return true, nil
   295  		case volumeStatusInUse:
   296  			// Detach below.
   297  			break
   298  		}
   299  		// Volume is still attached, so detach it.
   300  		if !issuedDetach {
   301  			args := make([]storage.VolumeAttachmentParams, len(v.Attachments))
   302  			for i, a := range v.Attachments {
   303  				args[i].VolumeId = volumeId
   304  				args[i].InstanceId = instance.Id(a.ServerId)
   305  			}
   306  			if len(args) > 0 {
   307  				results, err := detachVolumes(storageAdapter, args)
   308  				if err != nil {
   309  					return false, errors.Trace(err)
   310  				}
   311  				for _, err := range results {
   312  					if err != nil {
   313  						return false, errors.Trace(err)
   314  					}
   315  				}
   316  			}
   317  			issuedDetach = true
   318  		}
   319  		return false, nil
   320  	})
   321  	if err != nil {
   322  		return errors.Trace(err)
   323  	}
   324  	if volume.Status == volumeStatusDeleting {
   325  		// Already being deleted, nothing to do.
   326  		return nil
   327  	}
   328  	if err := storageAdapter.DeleteVolume(volumeId); err != nil {
   329  		return errors.Trace(err)
   330  	}
   331  	return nil
   332  }
   333  
   334  // ValidateVolumeParams implements storage.VolumeSource.
   335  func (s *cinderVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   336  	return nil
   337  }
   338  
   339  // AttachVolumes implements storage.VolumeSource.
   340  func (s *cinderVolumeSource) AttachVolumes(args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   341  	results := make([]storage.AttachVolumesResult, len(args))
   342  	for i, arg := range args {
   343  		attachment, err := s.attachVolume(arg)
   344  		if err != nil {
   345  			results[i].Error = errors.Trace(err)
   346  			continue
   347  		}
   348  		results[i].VolumeAttachment = attachment
   349  	}
   350  	return results, nil
   351  }
   352  
   353  func (s *cinderVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) {
   354  	// Check to see if the volume is already attached.
   355  	existingAttachments, err := s.storageAdapter.ListVolumeAttachments(string(arg.InstanceId))
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	novaAttachment := findAttachment(arg.VolumeId, existingAttachments)
   360  	if novaAttachment == nil {
   361  		// A volume must be "available" before it can be attached.
   362  		if _, err := waitVolume(s.storageAdapter, arg.VolumeId, func(v *cinder.Volume) (bool, error) {
   363  			return v.Status == "available", nil
   364  		}); err != nil {
   365  			return nil, errors.Annotate(err, "waiting for volume to become available")
   366  		}
   367  		novaAttachment, err = s.storageAdapter.AttachVolume(
   368  			string(arg.InstanceId),
   369  			arg.VolumeId,
   370  			autoAssignedMountPoint,
   371  		)
   372  		if err != nil {
   373  			return nil, err
   374  		}
   375  	}
   376  	return &storage.VolumeAttachment{
   377  		arg.Volume,
   378  		arg.Machine,
   379  		storage.VolumeAttachmentInfo{
   380  			DeviceName: novaAttachment.Device[len("/dev/"):],
   381  		},
   382  	}, nil
   383  }
   384  
   385  func waitVolume(
   386  	storageAdapter OpenstackStorage,
   387  	volumeId string,
   388  	pred func(*cinder.Volume) (bool, error),
   389  ) (*cinder.Volume, error) {
   390  	for a := cinderAttempt.Start(); a.Next(); {
   391  		volume, err := storageAdapter.GetVolume(volumeId)
   392  		if err != nil {
   393  			return nil, errors.Annotate(err, "getting volume")
   394  		}
   395  		ok, err := pred(volume)
   396  		if err != nil {
   397  			return nil, errors.Trace(err)
   398  		}
   399  		if ok {
   400  			return volume, nil
   401  		}
   402  	}
   403  	return nil, errors.New("timed out")
   404  }
   405  
   406  // DetachVolumes implements storage.VolumeSource.
   407  func (s *cinderVolumeSource) DetachVolumes(args []storage.VolumeAttachmentParams) ([]error, error) {
   408  	return detachVolumes(s.storageAdapter, args)
   409  }
   410  
   411  func detachVolumes(storageAdapter OpenstackStorage, args []storage.VolumeAttachmentParams) ([]error, error) {
   412  	results := make([]error, len(args))
   413  	for i, arg := range args {
   414  		// Check to see if the volume is already detached.
   415  		attachments, err := storageAdapter.ListVolumeAttachments(string(arg.InstanceId))
   416  		if err != nil {
   417  			results[i] = errors.Annotate(err, "listing volume attachments")
   418  			continue
   419  		}
   420  		if err := detachVolume(
   421  			string(arg.InstanceId),
   422  			arg.VolumeId,
   423  			attachments,
   424  			storageAdapter,
   425  		); err != nil {
   426  			results[i] = errors.Annotatef(
   427  				err, "detaching volume %s from server %s",
   428  				arg.VolumeId, arg.InstanceId,
   429  			)
   430  			continue
   431  		}
   432  	}
   433  	return results, nil
   434  }
   435  
   436  func cinderToJujuVolumeInfo(volume *cinder.Volume) storage.VolumeInfo {
   437  	return storage.VolumeInfo{
   438  		VolumeId:   volume.ID,
   439  		Size:       uint64(volume.Size * 1024),
   440  		Persistent: true,
   441  	}
   442  }
   443  
   444  func detachVolume(instanceId, volumeId string, attachments []nova.VolumeAttachment, storageAdapter OpenstackStorage) error {
   445  	// TODO(axw) verify whether we need to do this find step. From looking at the example
   446  	// responses in the OpenStack docs, the "attachment ID" is always the same as the
   447  	// volume ID. So we should just be able to issue a blind detach request, and then
   448  	// ignore errors that indicate the volume is already detached.
   449  	if findAttachment(volumeId, attachments) == nil {
   450  		return nil
   451  	}
   452  	return storageAdapter.DetachVolume(instanceId, volumeId)
   453  }
   454  
   455  func findAttachment(volId string, attachments []nova.VolumeAttachment) *nova.VolumeAttachment {
   456  	for _, attachment := range attachments {
   457  		if attachment.VolumeId == volId {
   458  			return &attachment
   459  		}
   460  	}
   461  	return nil
   462  }
   463  
   464  type OpenstackStorage interface {
   465  	GetVolume(volumeId string) (*cinder.Volume, error)
   466  	GetVolumesDetail() ([]cinder.Volume, error)
   467  	DeleteVolume(volumeId string) error
   468  	CreateVolume(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error)
   469  	AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error)
   470  	DetachVolume(serverId, attachmentId string) error
   471  	ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error)
   472  }
   473  
   474  type endpointResolver interface {
   475  	Authenticate() error
   476  	IsAuthenticated() bool
   477  	EndpointsForRegion(region string) identity.ServiceURLs
   478  }
   479  
   480  func getVolumeEndpointURL(client endpointResolver, region string) (*url.URL, error) {
   481  	if !client.IsAuthenticated() {
   482  		if err := authenticateClient(client); err != nil {
   483  			return nil, errors.Trace(err)
   484  		}
   485  	}
   486  	endpointMap := client.EndpointsForRegion(region)
   487  	// The cinder openstack charm appends 'v2' to the type for the v2 api.
   488  	endpoint, ok := endpointMap["volumev2"]
   489  	if !ok {
   490  		logger.Debugf(`endpoint "volumev2" not found for %q region, trying "volume"`, region)
   491  		endpoint, ok = endpointMap["volume"]
   492  		if !ok {
   493  			return nil, errors.NotFoundf(`endpoint "volume" in region %q`, region)
   494  		}
   495  	}
   496  	return url.Parse(endpoint)
   497  }
   498  
   499  type openstackStorageAdapter struct {
   500  	cinderClient
   501  	novaClient
   502  }
   503  
   504  type cinderClient struct {
   505  	*cinder.Client
   506  }
   507  
   508  type novaClient struct {
   509  	*nova.Client
   510  }
   511  
   512  // CreateVolume is part of the OpenstackStorage interface.
   513  func (ga *openstackStorageAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) {
   514  	resp, err := ga.cinderClient.CreateVolume(args)
   515  	if err != nil {
   516  		return nil, err
   517  	}
   518  	return &resp.Volume, nil
   519  }
   520  
   521  // GetVolumesDetail is part of the OpenstackStorage interface.
   522  func (ga *openstackStorageAdapter) GetVolumesDetail() ([]cinder.Volume, error) {
   523  	resp, err := ga.cinderClient.GetVolumesDetail()
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  	return resp.Volumes, nil
   528  }
   529  
   530  // GetVolume is part of the OpenstackStorage interface.
   531  func (ga *openstackStorageAdapter) GetVolume(volumeId string) (*cinder.Volume, error) {
   532  	resp, err := ga.cinderClient.GetVolume(volumeId)
   533  	if err != nil {
   534  		return nil, err
   535  	}
   536  	return &resp.Volume, nil
   537  }