1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
18
"github.com/juju/juju/environs/tags"
19
"github.com/juju/juju/instance"
20
"github.com/juju/juju/storage"
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 = ""
29
volumeStatusAvailable = "available"
30
volumeStatusDeleting = "deleting"
31
volumeStatusError = "error"
32
volumeStatusInUse = "in-use"
35
// StorageProviderTypes implements storage.ProviderRegistry.
36
func (*Environ) StorageProviderTypes() []storage.ProviderType {
37
return []storage.ProviderType{CinderProviderType}
40
// StorageProvider implements storage.ProviderRegistry.
41
func (env *Environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
42
if t != CinderProviderType {
43
return nil, errors.NotFoundf("storage provider %q", t)
45
return env.cinderProvider()
48
func (env *Environ) cinderProvider() (*cinderProvider, error) {
50
envName := env.ecfgUnlocked.Config.Name()
51
modelUUID := env.ecfgUnlocked.Config.UUID()
52
env.ecfgMutex.Unlock()
54
storageAdapter, err := newOpenstackStorage(env)
56
return nil, errors.Trace(err)
58
return &cinderProvider{storageAdapter, envName, modelUUID}, nil
61
var newOpenstackStorage = func(env *Environ) (OpenstackStorage, error) {
63
authClient := env.client
64
envNovaClient := env.novaUnlocked
65
env.ecfgMutex.Unlock()
67
endpointUrl, err := getVolumeEndpointURL(authClient, env.cloud.Region)
69
if errors.IsNotFound(err) {
70
return nil, errors.NewNotSupported(err, "volumes not supported")
72
return nil, errors.Annotate(err, "getting volume endpoint")
75
return &openstackStorageAdapter{
76
cinderClient{cinder.Basic(endpointUrl, authClient.TenantId(), authClient.Token)},
77
novaClient{envNovaClient},
81
type cinderProvider struct {
82
storageAdapter OpenstackStorage
87
var _ storage.Provider = (*cinderProvider)(nil)
89
var cinderAttempt = utils.AttemptStrategy{
90
Total: 1 * time.Minute,
91
Delay: 5 * time.Second,
94
// VolumeSource implements storage.Provider.
95
func (p *cinderProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) {
96
if err := p.ValidateConfig(providerConfig); err != nil {
99
source := &cinderVolumeSource{
100
storageAdapter: p.storageAdapter,
102
modelUUID: p.modelUUID,
107
// FilesystemSource implements storage.Provider.
108
func (p *cinderProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
109
return nil, errors.NotSupportedf("filesystems")
112
// Supports implements storage.Provider.
113
func (p *cinderProvider) Supports(kind storage.StorageKind) bool {
115
case storage.StorageKindBlock:
121
// Scope implements storage.Provider.
122
func (s *cinderProvider) Scope() storage.Scope {
123
return storage.ScopeEnviron
126
// ValidateConfig implements storage.Provider.
127
func (p *cinderProvider) ValidateConfig(cfg *storage.Config) error {
128
// TODO(axw) 2015-05-01 #1450737
129
// Reject attempts to create non-persistent volumes.
133
// Dynamic implements storage.Provider.
134
func (p *cinderProvider) Dynamic() bool {
138
// DefaultPools implements storage.Provider.
139
func (p *cinderProvider) DefaultPools() []*storage.Config {
143
type cinderVolumeSource struct {
144
storageAdapter OpenstackStorage
145
envName string // non unique, informational only
149
var _ storage.VolumeSource = (*cinderVolumeSource)(nil)
151
// CreateVolumes implements storage.VolumeSource.
152
func (s *cinderVolumeSource) CreateVolumes(args []storage.VolumeParams) ([]storage.CreateVolumesResult, error) {
153
results := make([]storage.CreateVolumesResult, len(args))
154
for i, arg := range args {
155
volume, err := s.createVolume(arg)
157
results[i].Error = errors.Trace(err)
160
results[i].Volume = volume
165
func (s *cinderVolumeSource) createVolume(arg storage.VolumeParams) (*storage.Volume, error) {
166
var metadata interface{}
167
if len(arg.ResourceTags) > 0 {
168
metadata = arg.ResourceTags
170
cinderVolume, err := s.storageAdapter.CreateVolume(cinder.CreateVolumeVolumeParams{
171
// The Cinder documentation incorrectly states the
172
// size parameter is in GB. It is actually GiB.
173
Size: int(math.Ceil(float64(arg.Size / 1024))),
174
Name: resourceName(arg.Tag, s.envName),
175
// TODO(axw) use the AZ of the initially attached machine.
176
AvailabilityZone: "",
180
return nil, errors.Trace(err)
183
// The response may (will?) come back before the volume transitions to
184
// "creating", in which case it will not have a size or status. Wait for
185
// the volume to transition, so we can record its actual size.
186
volumeId := cinderVolume.ID
187
cinderVolume, err = waitVolume(s.storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
188
return v.Status != "", nil
191
if err := s.storageAdapter.DeleteVolume(volumeId); err != nil {
192
logger.Warningf("destroying volume %s: %s", volumeId, err)
194
return nil, errors.Errorf("waiting for volume to be provisioned: %s", err)
196
logger.Debugf("created volume: %+v", cinderVolume)
197
return &storage.Volume{arg.Tag, cinderToJujuVolumeInfo(cinderVolume)}, nil
200
// ListVolumes is specified on the storage.VolumeSource interface.
201
func (s *cinderVolumeSource) ListVolumes() ([]string, error) {
202
volumes, err := listVolumes(s.storageAdapter, func(v *cinder.Volume) bool {
203
return v.Metadata[tags.JujuModel] == s.modelUUID
206
return nil, errors.Trace(err)
208
return volumeInfoToVolumeIds(volumes), nil
211
func volumeInfoToVolumeIds(volumes []storage.VolumeInfo) []string {
212
volumeIds := make([]string, len(volumes))
213
for i, volume := range volumes {
214
volumeIds[i] = volume.VolumeId
219
// listVolumes returns all of the volumes matching the given function.
220
func listVolumes(storageAdapter OpenstackStorage, match func(*cinder.Volume) bool) ([]storage.VolumeInfo, error) {
221
cinderVolumes, err := storageAdapter.GetVolumesDetail()
225
volumes := make([]storage.VolumeInfo, 0, len(cinderVolumes))
226
for _, volume := range cinderVolumes {
230
volumes = append(volumes, cinderToJujuVolumeInfo(&volume))
235
// DescribeVolumes implements storage.VolumeSource.
236
func (s *cinderVolumeSource) DescribeVolumes(volumeIds []string) ([]storage.DescribeVolumesResult, error) {
237
// In most cases, it is quicker to get all volumes and loop
238
// locally than to make several round-trips to the provider.
239
cinderVolumes, err := s.storageAdapter.GetVolumesDetail()
241
return nil, errors.Trace(err)
243
volumesById := make(map[string]*cinder.Volume)
244
for i, volume := range cinderVolumes {
245
volumesById[volume.ID] = &cinderVolumes[i]
247
results := make([]storage.DescribeVolumesResult, len(volumeIds))
248
for i, volumeId := range volumeIds {
249
cinderVolume, ok := volumesById[volumeId]
251
results[i].Error = errors.NotFoundf("volume %q", volumeId)
254
info := cinderToJujuVolumeInfo(cinderVolume)
255
results[i].VolumeInfo = &info
260
// DestroyVolumes implements storage.VolumeSource.
261
func (s *cinderVolumeSource) DestroyVolumes(volumeIds []string) ([]error, error) {
262
return destroyVolumes(s.storageAdapter, volumeIds), nil
265
func destroyVolumes(storageAdapter OpenstackStorage, volumeIds []string) []error {
266
var wg sync.WaitGroup
267
wg.Add(len(volumeIds))
268
results := make([]error, len(volumeIds))
269
for i, volumeId := range volumeIds {
270
go func(i int, volumeId string) {
272
results[i] = destroyVolume(storageAdapter, volumeId)
279
func destroyVolume(storageAdapter OpenstackStorage, volumeId string) error {
280
logger.Debugf("destroying volume %q", volumeId)
281
// Volumes must not be in-use when destroying. A volume may
282
// still be in-use when the instance it is attached to is
283
// in the process of being terminated.
284
var issuedDetach bool
285
volume, err := waitVolume(storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) {
288
// Not ready for deletion; keep waiting.
290
case volumeStatusAvailable, volumeStatusDeleting, volumeStatusError:
292
case volumeStatusInUse:
296
// Volume is still attached, so detach it.
298
args := make([]storage.VolumeAttachmentParams, len(v.Attachments))
299
for i, a := range v.Attachments {
300
args[i].VolumeId = volumeId
301
args[i].InstanceId = instance.Id(a.ServerId)
304
results, err := detachVolumes(storageAdapter, args)
306
return false, errors.Trace(err)
308
for _, err := range results {
310
return false, errors.Trace(err)
319
return errors.Trace(err)
321
if volume.Status == volumeStatusDeleting {
322
// Already being deleted, nothing to do.
325
if err := storageAdapter.DeleteVolume(volumeId); err != nil {
326
return errors.Trace(err)
331
// ValidateVolumeParams implements storage.VolumeSource.
332
func (s *cinderVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
336
// AttachVolumes implements storage.VolumeSource.
337
func (s *cinderVolumeSource) AttachVolumes(args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
338
results := make([]storage.AttachVolumesResult, len(args))
339
for i, arg := range args {
340
attachment, err := s.attachVolume(arg)
342
results[i].Error = errors.Trace(err)
345
results[i].VolumeAttachment = attachment
350
func (s *cinderVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) {
351
// Check to see if the volume is already attached.
352
existingAttachments, err := s.storageAdapter.ListVolumeAttachments(string(arg.InstanceId))
356
novaAttachment := findAttachment(arg.VolumeId, existingAttachments)
357
if novaAttachment == nil {
358
// A volume must be "available" before it can be attached.
359
if _, err := waitVolume(s.storageAdapter, arg.VolumeId, func(v *cinder.Volume) (bool, error) {
360
return v.Status == "available", nil
362
return nil, errors.Annotate(err, "waiting for volume to become available")
364
novaAttachment, err = s.storageAdapter.AttachVolume(
365
string(arg.InstanceId),
367
autoAssignedMountPoint,
373
return &storage.VolumeAttachment{
376
storage.VolumeAttachmentInfo{
377
DeviceName: novaAttachment.Device[len("/dev/"):],
383
storageAdapter OpenstackStorage,
385
pred func(*cinder.Volume) (bool, error),
386
) (*cinder.Volume, error) {
387
for a := cinderAttempt.Start(); a.Next(); {
388
volume, err := storageAdapter.GetVolume(volumeId)
390
return nil, errors.Annotate(err, "getting volume")
392
ok, err := pred(volume)
394
return nil, errors.Trace(err)
400
return nil, errors.New("timed out")
403
// DetachVolumes implements storage.VolumeSource.
404
func (s *cinderVolumeSource) DetachVolumes(args []storage.VolumeAttachmentParams) ([]error, error) {
405
return detachVolumes(s.storageAdapter, args)
408
func detachVolumes(storageAdapter OpenstackStorage, args []storage.VolumeAttachmentParams) ([]error, error) {
409
results := make([]error, len(args))
410
for i, arg := range args {
411
// Check to see if the volume is already detached.
412
attachments, err := storageAdapter.ListVolumeAttachments(string(arg.InstanceId))
414
results[i] = errors.Annotate(err, "listing volume attachments")
417
if err := detachVolume(
418
string(arg.InstanceId),
423
results[i] = errors.Annotatef(
424
err, "detaching volume %s from server %s",
425
arg.VolumeId, arg.InstanceId,
433
func cinderToJujuVolumeInfo(volume *cinder.Volume) storage.VolumeInfo {
434
return storage.VolumeInfo{
436
Size: uint64(volume.Size * 1024),
441
func detachVolume(instanceId, volumeId string, attachments []nova.VolumeAttachment, storageAdapter OpenstackStorage) error {
442
// TODO(axw) verify whether we need to do this find step. From looking at the example
443
// responses in the OpenStack docs, the "attachment ID" is always the same as the
444
// volume ID. So we should just be able to issue a blind detach request, and then
445
// ignore errors that indicate the volume is already detached.
446
if findAttachment(volumeId, attachments) == nil {
449
return storageAdapter.DetachVolume(instanceId, volumeId)
452
func findAttachment(volId string, attachments []nova.VolumeAttachment) *nova.VolumeAttachment {
453
for _, attachment := range attachments {
454
if attachment.VolumeId == volId {
461
type OpenstackStorage interface {
462
GetVolume(volumeId string) (*cinder.Volume, error)
463
GetVolumesDetail() ([]cinder.Volume, error)
464
DeleteVolume(volumeId string) error
465
CreateVolume(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error)
466
AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error)
467
DetachVolume(serverId, attachmentId string) error
468
ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error)
471
type endpointResolver interface {
472
EndpointsForRegion(region string) identity.ServiceURLs
475
func getVolumeEndpointURL(client endpointResolver, region string) (*url.URL, error) {
476
endpointMap := client.EndpointsForRegion(region)
477
// The cinder openstack charm appends 'v2' to the type for the v2 api.
478
endpoint, ok := endpointMap["volumev2"]
480
logger.Debugf(`endpoint "volumev2" not found for %q region, trying "volume"`, region)
481
endpoint, ok = endpointMap["volume"]
483
return nil, errors.NotFoundf(`endpoint "volume" in region %q`, region)
486
return url.Parse(endpoint)
489
type openstackStorageAdapter struct {
494
type cinderClient struct {
498
type novaClient struct {
502
// CreateVolume is part of the OpenstackStorage interface.
503
func (ga *openstackStorageAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) {
504
resp, err := ga.cinderClient.CreateVolume(args)
508
return &resp.Volume, nil
511
// GetVolumesDetail is part of the OpenstackStorage interface.
512
func (ga *openstackStorageAdapter) GetVolumesDetail() ([]cinder.Volume, error) {
513
resp, err := ga.cinderClient.GetVolumesDetail()
517
return resp.Volumes, nil
520
// GetVolume is part of the OpenstackStorage interface.
521
func (ga *openstackStorageAdapter) GetVolume(volumeId string) (*cinder.Volume, error) {
522
resp, err := ga.cinderClient.GetVolume(volumeId)
526
return &resp.Volume, nil