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 }