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 }