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