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