github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 "fmt" 8 "math" 9 "net/url" 10 "sync" 11 "time" 12 13 "github.com/go-goose/goose/v5/cinder" 14 gooseerrors "github.com/go-goose/goose/v5/errors" 15 "github.com/go-goose/goose/v5/identity" 16 "github.com/go-goose/goose/v5/nova" 17 "github.com/juju/collections/set" 18 "github.com/juju/errors" 19 "github.com/juju/schema" 20 "github.com/juju/utils/v3" 21 22 "github.com/juju/juju/core/instance" 23 "github.com/juju/juju/environs/context" 24 "github.com/juju/juju/environs/tags" 25 "github.com/juju/juju/provider/common" 26 "github.com/juju/juju/storage" 27 ) 28 29 const ( 30 CinderProviderType = storage.ProviderType("cinder") 31 32 cinderVolumeType = "volume-type" 33 34 // autoAssignedMountPoint specifies the value to pass in when 35 // you'd like Cinder to automatically assign a mount point. 36 autoAssignedMountPoint = "" 37 38 volumeStatusAvailable = "available" 39 volumeStatusDeleting = "deleting" 40 volumeStatusError = "error" 41 volumeStatusInUse = "in-use" 42 ) 43 44 var cinderConfigFields = schema.Fields{ 45 cinderVolumeType: schema.String(), 46 } 47 48 var cinderConfigChecker = schema.FieldMap( 49 cinderConfigFields, 50 schema.Defaults{ 51 cinderVolumeType: schema.Omit, 52 }, 53 ) 54 55 type cinderConfig struct { 56 volumeType string 57 } 58 59 func newCinderConfig(attrs map[string]interface{}) (*cinderConfig, error) { 60 out, err := cinderConfigChecker.Coerce(attrs, nil) 61 if err != nil { 62 return nil, errors.Annotate(err, "validating Cinder storage config") 63 } 64 coerced := out.(map[string]interface{}) 65 volumeType, _ := coerced[cinderVolumeType].(string) 66 cinderConfig := &cinderConfig{ 67 volumeType: volumeType, 68 } 69 return cinderConfig, nil 70 } 71 72 // StorageProviderTypes implements storage.ProviderRegistry. 73 func (e *Environ) StorageProviderTypes() ([]storage.ProviderType, error) { 74 var types []storage.ProviderType 75 if _, err := e.cinderProvider(); err == nil { 76 types = append(types, CinderProviderType) 77 } else if !errors.IsNotSupported(err) { 78 return nil, errors.Trace(err) 79 } 80 return types, nil 81 } 82 83 // StorageProvider implements storage.ProviderRegistry. 84 func (e *Environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 85 if t != CinderProviderType { 86 return nil, errors.NotFoundf("storage provider %q", t) 87 } 88 return e.cinderProvider() 89 } 90 91 func (e *Environ) cinderProvider() (*cinderProvider, error) { 92 storageAdapter, err := newOpenstackStorage(e) 93 if err != nil { 94 return nil, errors.Trace(err) 95 } 96 return &cinderProvider{ 97 storageAdapter: storageAdapter, 98 envName: e.name, 99 modelUUID: e.uuid, 100 namespace: e.namespace, 101 zonedEnv: e, 102 }, nil 103 } 104 105 var newOpenstackStorage = func(env *Environ) (OpenstackStorage, error) { 106 env.ecfgMutex.Lock() 107 defer env.ecfgMutex.Unlock() 108 109 client := env.clientUnlocked 110 if env.volumeURL == nil { 111 url, err := getVolumeEndpointURL(client, env.cloudUnlocked.Region) 112 if IsNotFoundError(err) { 113 // No volume endpoint found; Cinder is not supported. 114 return nil, errors.NotSupportedf("volumes") 115 } else if err != nil { 116 return nil, errors.Trace(err) 117 } 118 env.volumeURL = url 119 logger.Debugf("volume URL: %v", url) 120 } 121 122 // TODO (stickupkid): Move this to the ClientFactory. 123 // We shouldn't have another wrapper around an existing client. 124 cinderCl := cinderClient{cinder.Basic(env.volumeURL, client.TenantId(), client.Token)} 125 126 cloudSpec := env.cloudUnlocked 127 if len(cloudSpec.CACertificates) > 0 { 128 cinderCl = cinderClient{cinder.BasicTLSConfig( 129 env.volumeURL, 130 client.TenantId(), 131 client.Token, 132 tlsConfig(cloudSpec.CACertificates)), 133 } 134 } 135 136 return &openstackStorageAdapter{ 137 cinderCl, 138 novaClient{env.novaUnlocked}, 139 }, nil 140 } 141 142 type cinderProvider struct { 143 storageAdapter OpenstackStorage 144 envName string 145 modelUUID string 146 namespace instance.Namespace 147 zonedEnv common.ZonedEnviron 148 } 149 150 var _ storage.Provider = (*cinderProvider)(nil) 151 152 var cinderAttempt = utils.AttemptStrategy{ 153 Total: 1 * time.Minute, 154 Delay: 5 * time.Second, 155 } 156 157 // VolumeSource implements storage.Provider. 158 func (p *cinderProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { 159 if err := p.ValidateConfig(providerConfig); err != nil { 160 return nil, err 161 } 162 source := &cinderVolumeSource{ 163 storageAdapter: p.storageAdapter, 164 envName: p.envName, 165 modelUUID: p.modelUUID, 166 namespace: p.namespace, 167 zonedEnv: p.zonedEnv, 168 } 169 return source, nil 170 } 171 172 // FilesystemSource implements storage.Provider. 173 func (p *cinderProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 174 return nil, errors.NotSupportedf("filesystems") 175 } 176 177 // Supports implements storage.Provider. 178 func (p *cinderProvider) Supports(kind storage.StorageKind) bool { 179 switch kind { 180 case storage.StorageKindBlock: 181 return true 182 } 183 return false 184 } 185 186 // Scope implements storage.Provider. 187 func (s *cinderProvider) Scope() storage.Scope { 188 return storage.ScopeEnviron 189 } 190 191 func (p *cinderProvider) ValidateForK8s(map[string]any) error { 192 return errors.NotValidf("storage provider type %q", CinderProviderType) 193 } 194 195 // ValidateConfig implements storage.Provider. 196 func (p *cinderProvider) ValidateConfig(cfg *storage.Config) error { 197 // TODO(axw) 2015-05-01 #1450737 198 // Reject attempts to create non-persistent volumes. 199 _, err := newCinderConfig(cfg.Attrs()) 200 return errors.Trace(err) 201 } 202 203 // Dynamic implements storage.Provider. 204 func (p *cinderProvider) Dynamic() bool { 205 return true 206 } 207 208 // Releasable is defined on the Provider interface. 209 func (*cinderProvider) Releasable() bool { 210 return true 211 } 212 213 // DefaultPools implements storage.Provider. 214 func (p *cinderProvider) DefaultPools() []*storage.Config { 215 return nil 216 } 217 218 type cinderVolumeSource struct { 219 storageAdapter OpenstackStorage 220 envName string // non unique, informational only 221 modelUUID string 222 namespace instance.Namespace 223 zonedEnv common.ZonedEnviron 224 } 225 226 var _ storage.VolumeSource = (*cinderVolumeSource)(nil) 227 228 // CreateVolumes implements storage.VolumeSource. 229 func (s *cinderVolumeSource) CreateVolumes( 230 ctx context.ProviderCallContext, args []storage.VolumeParams, 231 ) ([]storage.CreateVolumesResult, error) { 232 results := make([]storage.CreateVolumesResult, len(args)) 233 for i, arg := range args { 234 volume, err := s.createVolume(ctx, arg) 235 if err != nil { 236 results[i].Error = errors.Trace(err) 237 if denied := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx); denied { 238 // If it is an unauthorised error, no need to continue since we will 100% fail... 239 break 240 } 241 continue 242 } 243 results[i].Volume = volume 244 } 245 return results, nil 246 } 247 248 func (s *cinderVolumeSource) createVolume( 249 ctx context.ProviderCallContext, arg storage.VolumeParams) (*storage.Volume, error) { 250 cinderConfig, err := newCinderConfig(arg.Attributes) 251 if err != nil { 252 return nil, errors.Trace(err) 253 } 254 255 var metadata interface{} 256 if len(arg.ResourceTags) > 0 { 257 metadata = arg.ResourceTags 258 } 259 260 az, err := s.availabilityZoneForVolume(ctx, arg.Tag.Id(), arg.Attachment) 261 if err != nil { 262 return nil, errors.Trace(err) 263 } 264 cinderVolume, err := s.storageAdapter.CreateVolume(cinder.CreateVolumeVolumeParams{ 265 // The Cinder documentation incorrectly states the 266 // size parameter is in GB. It is actually GiB. 267 Size: int(math.Ceil(float64(arg.Size / 1024))), 268 Name: resourceName(s.namespace, s.envName, arg.Tag.String()), 269 VolumeType: cinderConfig.volumeType, 270 AvailabilityZone: az, 271 Metadata: metadata, 272 }) 273 if err != nil { 274 return nil, errors.Trace(err) 275 } 276 277 // The response may (will?) come back before the volume transitions to 278 // "creating", in which case it will not have a size or status. 279 // Wait for the volume to transition, so we can record its actual size. 280 volumeId := cinderVolume.ID 281 cinderVolume, err = waitVolume(s.storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) { 282 return v.Status != "", nil 283 }) 284 if err != nil { 285 if err := s.storageAdapter.DeleteVolume(volumeId); err != nil { 286 logger.Warningf("destroying volume %s: %s", volumeId, err) 287 } 288 return nil, errors.Errorf("waiting for volume to be provisioned: %s", err) 289 } 290 logger.Debugf("created volume: %+v", cinderVolume) 291 return &storage.Volume{Tag: arg.Tag, VolumeInfo: cinderToJujuVolumeInfo(cinderVolume)}, nil 292 } 293 294 func (s *cinderVolumeSource) availabilityZoneForVolume( 295 ctx context.ProviderCallContext, volName string, attachment *storage.VolumeAttachmentParams, 296 ) (string, error) { 297 // If this volume is being attached to an instance, attempt to provision 298 // the storage in the same availability zone. 299 // This helps to avoid a situation with all storage residing in a single 300 // AZ that upon failure would effectively take down attached instances 301 // whatever zone they were in. 302 // However, we first attempt to query the possible volume availability zones. 303 // If the API is old and does not support explicit volume AZs, or volumes can 304 // only be provisioned in say the default "nova" zone and the instance is in 305 // a different zone, we won't attempt to use the instance zone because that 306 // won't work and we'll get a 400 error back. 307 if attachment == nil || attachment.InstanceId == "" { 308 return "", nil 309 } 310 311 volumeZones, err := s.storageAdapter.ListVolumeAvailabilityZones() 312 if err != nil && !gooseerrors.IsNotImplemented(err) { 313 logger.Infof("block volume zones not supported, not using availability zone for volume %q", volName) 314 return "", errors.Trace(err) 315 } 316 vZones := set.NewStrings() 317 for _, vz := range volumeZones { 318 if vz.State.Available { 319 vZones.Add(vz.Name) 320 } 321 } 322 if vZones.Size() == 0 { 323 logger.Infof("no block volume zones defined, not using availability zone for volume %q", volName) 324 return "", nil 325 } 326 logger.Debugf("possible block volume zones: %v", vZones.SortedValues()) 327 aZones, err := s.zonedEnv.InstanceAvailabilityZoneNames(ctx, []instance.Id{attachment.InstanceId}) 328 if err != nil { 329 return "", errors.Trace(err) 330 } 331 if len(aZones) == 0 { 332 // All instances should have an availability zone. 333 // The default is "nova" so something is wrong if nothing 334 // is returned from this call. 335 logger.Warningf("no availability zone detected for instance %q", attachment.InstanceId) 336 return "", nil 337 } 338 // Only choose an AZ from the instance if there's a matching volume AZ. 339 var az string 340 for _, az = range aZones { 341 break 342 } 343 if vZones.Contains(az) { 344 logger.Debugf("using availability zone %q to create cinder volume %q", az, volName) 345 return az, nil 346 } 347 logger.Warningf("no compatible availability zone detected for volume %q", volName) 348 return "", nil 349 } 350 351 // ListVolumes is specified on the storage.VolumeSource interface. 352 func (s *cinderVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) { 353 cinderVolumes, err := modelCinderVolumes(s.storageAdapter, s.modelUUID) 354 if err != nil { 355 handleCredentialError(err, ctx) 356 return nil, errors.Trace(err) 357 } 358 return volumeInfoToVolumeIds(cinderToJujuVolumeInfos(cinderVolumes)), nil 359 } 360 361 // modelCinderVolumes returns all of the cinder volumes for the model. 362 func modelCinderVolumes(storageAdapter OpenstackStorage, modelUUID string) ([]cinder.Volume, error) { 363 return cinderVolumes(storageAdapter, func(v *cinder.Volume) bool { 364 return v.Metadata[tags.JujuModel] == modelUUID 365 }) 366 } 367 368 // controllerCinderVolumes returns all of the cinder volumes for the model. 369 func controllerCinderVolumes(storageAdapter OpenstackStorage, controllerUUID string) ([]cinder.Volume, error) { 370 return cinderVolumes(storageAdapter, func(v *cinder.Volume) bool { 371 return v.Metadata[tags.JujuController] == controllerUUID 372 }) 373 } 374 375 // cinderVolumes returns all of the cinder volumes matching the given predicate. 376 func cinderVolumes(storageAdapter OpenstackStorage, pred func(*cinder.Volume) bool) ([]cinder.Volume, error) { 377 allCinderVolumes, err := storageAdapter.GetVolumesDetail() 378 if err != nil { 379 return nil, err 380 } 381 var matching []cinder.Volume 382 for _, v := range allCinderVolumes { 383 if pred(&v) { 384 matching = append(matching, v) 385 } 386 } 387 return matching, nil 388 } 389 390 func volumeInfoToVolumeIds(volumes []storage.VolumeInfo) []string { 391 volumeIds := make([]string, len(volumes)) 392 for i, volume := range volumes { 393 volumeIds[i] = volume.VolumeId 394 } 395 return volumeIds 396 } 397 398 // DescribeVolumes implements storage.VolumeSource. 399 func (s *cinderVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 400 // In most cases, it is quicker to get all volumes and loop 401 // locally than to make several round-trips to the provider. 402 cinderVolumes, err := s.storageAdapter.GetVolumesDetail() 403 if err != nil { 404 handleCredentialError(err, ctx) 405 return nil, errors.Trace(err) 406 } 407 volumesById := make(map[string]*cinder.Volume) 408 for i, volume := range cinderVolumes { 409 volumesById[volume.ID] = &cinderVolumes[i] 410 } 411 results := make([]storage.DescribeVolumesResult, len(volumeIds)) 412 for i, volumeId := range volumeIds { 413 cinderVolume, ok := volumesById[volumeId] 414 if !ok { 415 results[i].Error = errors.NotFoundf("volume %q", volumeId) 416 continue 417 } 418 info := cinderToJujuVolumeInfo(cinderVolume) 419 results[i].VolumeInfo = &info 420 } 421 return results, nil 422 } 423 424 // DestroyVolumes implements storage.VolumeSource. 425 func (s *cinderVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 426 return foreachVolume(ctx, s.storageAdapter, volumeIds, destroyVolume), nil 427 } 428 429 // ReleaseVolumes implements storage.VolumeSource. 430 func (s *cinderVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 431 return foreachVolume(ctx, s.storageAdapter, volumeIds, releaseVolume), nil 432 } 433 434 func foreachVolume(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, volumeIds []string, f func(context.ProviderCallContext, OpenstackStorage, string) error) []error { 435 var wg sync.WaitGroup 436 wg.Add(len(volumeIds)) 437 results := make([]error, len(volumeIds)) 438 for i, volumeId := range volumeIds { 439 go func(i int, volumeId string) { 440 defer wg.Done() 441 results[i] = f(ctx, storageAdapter, volumeId) 442 }(i, volumeId) 443 } 444 wg.Wait() 445 return results 446 } 447 448 func destroyVolume(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, volumeId string) error { 449 logger.Debugf("destroying volume %q", volumeId) 450 // Volumes must not be in-use when destroying. A volume may 451 // still be in-use when the instance it is attached to is 452 // in the process of being terminated. 453 var issuedDetach bool 454 volume, err := waitVolume(storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) { 455 switch v.Status { 456 default: 457 // Not ready for deletion; keep waiting. 458 return false, nil 459 case volumeStatusAvailable, volumeStatusDeleting, volumeStatusError: 460 return true, nil 461 case volumeStatusInUse: 462 // Detach below. 463 break 464 } 465 // Volume is still attached, so detach it. 466 if !issuedDetach { 467 args := make([]storage.VolumeAttachmentParams, len(v.Attachments)) 468 for i, a := range v.Attachments { 469 args[i].VolumeId = volumeId 470 args[i].InstanceId = instance.Id(a.ServerId) 471 } 472 if len(args) > 0 { 473 results := detachVolumes(ctx, storageAdapter, args) 474 for _, err := range results { 475 if err != nil { 476 return false, errors.Trace(err) 477 } 478 } 479 } 480 issuedDetach = true 481 } 482 return false, nil 483 }) 484 if err != nil { 485 if IsNotFoundError(err) { 486 // The volume wasn't found; nothing 487 // to destroy, so we're done. 488 return nil 489 } 490 handleCredentialError(err, ctx) 491 return errors.Trace(err) 492 } 493 if volume.Status == volumeStatusDeleting { 494 // Already being deleted, nothing to do. 495 return nil 496 } 497 if err := storageAdapter.DeleteVolume(volumeId); err != nil { 498 handleCredentialError(err, ctx) 499 return errors.Trace(err) 500 } 501 return nil 502 } 503 504 func releaseVolume(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, volumeId string) error { 505 logger.Debugf("releasing volume %q", volumeId) 506 _, err := waitVolume(storageAdapter, volumeId, func(v *cinder.Volume) (bool, error) { 507 switch v.Status { 508 case volumeStatusAvailable, volumeStatusError: 509 return true, nil 510 case volumeStatusDeleting: 511 return false, errors.New("volume is being deleted") 512 case volumeStatusInUse: 513 return false, errors.New("volume still in-use") 514 } 515 // Not ready for releasing; keep waiting. 516 return false, nil 517 }) 518 if err != nil { 519 handleCredentialError(err, ctx) 520 return errors.Annotatef(err, "cannot release volume %q", volumeId) 521 } 522 // Drop the model and controller tags from the volume. 523 tags := map[string]string{ 524 tags.JujuModel: "", 525 tags.JujuController: "", 526 } 527 _, err = storageAdapter.SetVolumeMetadata(volumeId, tags) 528 handleCredentialError(err, ctx) 529 return errors.Annotate(err, "tagging volume") 530 } 531 532 // ValidateVolumeParams implements storage.VolumeSource. 533 func (s *cinderVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 534 _, err := newCinderConfig(params.Attributes) 535 return errors.Trace(err) 536 } 537 538 // AttachVolumes implements storage.VolumeSource. 539 func (s *cinderVolumeSource) AttachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 540 results := make([]storage.AttachVolumesResult, len(args)) 541 for i, arg := range args { 542 attachment, err := s.attachVolume(arg) 543 if err != nil { 544 results[i].Error = errors.Trace(err) 545 if denial := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx); denial { 546 // We do not want to continue here as we'll 100% fail if we got unauthorised error. 547 break 548 } 549 continue 550 } 551 results[i].VolumeAttachment = attachment 552 } 553 return results, nil 554 } 555 556 func (s *cinderVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) { 557 // Check to see if the volume is already attached. 558 existingAttachments, err := s.storageAdapter.ListVolumeAttachments(string(arg.InstanceId)) 559 if err != nil { 560 return nil, err 561 } 562 novaAttachment := findAttachment(arg.VolumeId, existingAttachments) 563 if novaAttachment == nil { 564 // A volume must be "available" before it can be attached. 565 if _, err := waitVolume(s.storageAdapter, arg.VolumeId, func(v *cinder.Volume) (bool, error) { 566 return v.Status == "available", nil 567 }); err != nil { 568 return nil, errors.Annotate(err, "waiting for volume to become available") 569 } 570 novaAttachment, err = s.storageAdapter.AttachVolume( 571 string(arg.InstanceId), 572 arg.VolumeId, 573 autoAssignedMountPoint, 574 ) 575 if err != nil { 576 return nil, err 577 } 578 } 579 if novaAttachment.Device == nil { 580 return nil, errors.Errorf("device not assigned to volume attachment") 581 } 582 return &storage.VolumeAttachment{ 583 Volume: arg.Volume, 584 Machine: arg.Machine, 585 VolumeAttachmentInfo: storage.VolumeAttachmentInfo{ 586 DeviceName: (*novaAttachment.Device)[len("/dev/"):], 587 }, 588 }, nil 589 } 590 591 // ImportVolume is part of the storage.VolumeImporter interface. 592 func (s *cinderVolumeSource) ImportVolume(ctx context.ProviderCallContext, volumeId string, resourceTags map[string]string) (storage.VolumeInfo, error) { 593 volume, err := s.storageAdapter.GetVolume(volumeId) 594 if err != nil { 595 handleCredentialError(err, ctx) 596 return storage.VolumeInfo{}, errors.Annotate(err, "getting volume") 597 } 598 if volume.Status != volumeStatusAvailable { 599 return storage.VolumeInfo{}, errors.Errorf( 600 "cannot import volume %q with status %q", volumeId, volume.Status, 601 ) 602 } 603 if _, err := s.storageAdapter.SetVolumeMetadata(volumeId, resourceTags); err != nil { 604 handleCredentialError(err, ctx) 605 return storage.VolumeInfo{}, errors.Annotatef(err, "tagging volume %q", volumeId) 606 } 607 return cinderToJujuVolumeInfo(volume), nil 608 } 609 610 func waitVolume( 611 storageAdapter OpenstackStorage, 612 volumeId string, 613 pred func(*cinder.Volume) (bool, error), 614 ) (*cinder.Volume, error) { 615 for a := cinderAttempt.Start(); a.Next(); { 616 volume, err := storageAdapter.GetVolume(volumeId) 617 if err != nil { 618 return nil, errors.Annotate(err, "getting volume") 619 } 620 ok, err := pred(volume) 621 if err != nil { 622 return nil, errors.Trace(err) 623 } 624 if ok { 625 return volume, nil 626 } 627 } 628 return nil, errors.New("timed out") 629 } 630 631 // DetachVolumes implements storage.VolumeSource. 632 func (s *cinderVolumeSource) DetachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]error, error) { 633 return detachVolumes(ctx, s.storageAdapter, args), nil 634 } 635 636 func detachVolumes(ctx context.ProviderCallContext, storageAdapter OpenstackStorage, args []storage.VolumeAttachmentParams) []error { 637 results := make([]error, len(args)) 638 for i, arg := range args { 639 if err := detachVolume( 640 string(arg.InstanceId), 641 arg.VolumeId, 642 storageAdapter, 643 ); err != nil { 644 handleCredentialError(err, ctx) 645 results[i] = errors.Annotatef( 646 err, "detaching volume %s from server %s", 647 arg.VolumeId, arg.InstanceId, 648 ) 649 continue 650 } 651 } 652 return results 653 } 654 655 func cinderToJujuVolumeInfos(volumes []cinder.Volume) []storage.VolumeInfo { 656 out := make([]storage.VolumeInfo, len(volumes)) 657 for i, v := range volumes { 658 out[i] = cinderToJujuVolumeInfo(&v) 659 } 660 return out 661 } 662 663 func cinderToJujuVolumeInfo(volume *cinder.Volume) storage.VolumeInfo { 664 return storage.VolumeInfo{ 665 VolumeId: volume.ID, 666 Size: uint64(volume.Size * 1024), 667 Persistent: true, 668 } 669 } 670 671 func detachVolume(instanceId, volumeId string, storageAdapter OpenstackStorage) error { 672 err := storageAdapter.DetachVolume(instanceId, volumeId) 673 if err != nil && !IsNotFoundError(err) { 674 return errors.Trace(err) 675 } 676 // The volume was successfully detached, or was 677 // already detached (i.e. NotFound error case). 678 return nil 679 } 680 681 func IsNotFoundError(err error) bool { 682 return errors.IsNotFound(err) || gooseerrors.IsNotFound(err) 683 } 684 685 func findAttachment(volId string, attachments []nova.VolumeAttachment) *nova.VolumeAttachment { 686 for _, attachment := range attachments { 687 if attachment.VolumeId == volId { 688 return &attachment 689 } 690 } 691 return nil 692 } 693 694 type OpenstackStorage interface { 695 GetVolume(volumeId string) (*cinder.Volume, error) 696 GetVolumesDetail() ([]cinder.Volume, error) 697 DeleteVolume(volumeId string) error 698 CreateVolume(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) 699 AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error) 700 DetachVolume(serverId, attachmentId string) error 701 ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error) 702 SetVolumeMetadata(volumeId string, metadata map[string]string) (map[string]string, error) 703 ListVolumeAvailabilityZones() ([]cinder.AvailabilityZone, error) 704 } 705 706 type endpointResolver interface { 707 Authenticate() error 708 IsAuthenticated() bool 709 EndpointsForRegion(region string) identity.ServiceURLs 710 } 711 712 func getVolumeEndpointURL(client endpointResolver, region string) (*url.URL, error) { 713 if !client.IsAuthenticated() { 714 if err := authenticateClient(client); err != nil { 715 return nil, errors.Trace(err) 716 } 717 } 718 endpointMap := client.EndpointsForRegion(region) 719 720 // Different versions of block storage in OpenStack have different entries 721 // in the service catalog. Find the most recent version. If it does exist, 722 // fall back to older ones. 723 endpoint, ok := endpointMap["volumev3"] 724 if ok { 725 return url.Parse(endpoint) 726 } 727 logger.Debugf(`endpoint "volumev3" not found for %q region, trying "volumev2"`, region) 728 endpoint, ok = endpointMap["volumev2"] 729 if ok { 730 return url.Parse(endpoint) 731 } 732 logger.Debugf(`endpoint "volumev2" not found for %q region, trying "volume"`, region) 733 endpoint, ok = endpointMap["volume"] 734 if ok { 735 return url.Parse(endpoint) 736 } 737 return nil, errors.NotFoundf(`endpoint "volume" in region %q`, region) 738 } 739 740 type openstackStorageAdapter struct { 741 cinderClient 742 novaClient 743 } 744 745 type cinderClient struct { 746 *cinder.Client 747 } 748 749 type novaClient struct { 750 *nova.Client 751 } 752 753 // CreateVolume is part of the OpenstackStorage interface. 754 func (ga *openstackStorageAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 755 resp, err := ga.cinderClient.CreateVolume(args) 756 if err != nil { 757 return nil, err 758 } 759 return &resp.Volume, nil 760 } 761 762 // GetVolumesDetail is part of the OpenstackStorage interface. 763 func (ga *openstackStorageAdapter) GetVolumesDetail() ([]cinder.Volume, error) { 764 resp, err := ga.cinderClient.GetVolumesDetail() 765 if err != nil { 766 return nil, err 767 } 768 return resp.Volumes, nil 769 } 770 771 // GetVolume is part of the OpenstackStorage interface. 772 func (ga *openstackStorageAdapter) GetVolume(volumeId string) (*cinder.Volume, error) { 773 resp, err := ga.cinderClient.GetVolume(volumeId) 774 if err != nil { 775 if IsNotFoundError(err) { 776 return nil, errors.NotFoundf("volume %q", volumeId) 777 } 778 return nil, err 779 } 780 return &resp.Volume, nil 781 } 782 783 // SetVolumeMetadata is part of the OpenstackStorage interface. 784 func (ga *openstackStorageAdapter) SetVolumeMetadata(volumeId string, metadata map[string]string) (map[string]string, error) { 785 return ga.cinderClient.SetVolumeMetadata(volumeId, metadata) 786 } 787 788 // DeleteVolume is part of the OpenstackStorage interface. 789 func (ga *openstackStorageAdapter) DeleteVolume(volumeId string) error { 790 if err := ga.cinderClient.DeleteVolume(volumeId); err != nil { 791 if IsNotFoundError(err) { 792 return errors.NotFoundf("volume %q", volumeId) 793 } 794 return err 795 } 796 return nil 797 } 798 799 // DetachVolume is part of the OpenstackStorage interface. 800 func (ga *openstackStorageAdapter) DetachVolume(serverId, attachmentId string) error { 801 if err := ga.novaClient.DetachVolume(serverId, attachmentId); err != nil { 802 if IsNotFoundError(err) { 803 return errors.NewNotFound(nil, 804 fmt.Sprintf("volume %q is not attached to server %q", 805 attachmentId, serverId, 806 ), 807 ) 808 } 809 return err 810 } 811 return nil 812 }