github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/azure/storage.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 "fmt" 8 "path" 9 "sync" 10 11 "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" 12 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2" 13 "github.com/juju/errors" 14 "github.com/juju/names/v5" 15 "github.com/juju/schema" 16 17 "github.com/juju/juju/core/instance" 18 "github.com/juju/juju/environs/context" 19 "github.com/juju/juju/provider/azure/internal/errorutils" 20 "github.com/juju/juju/storage" 21 ) 22 23 const ( 24 azureStorageProviderType = "azure" 25 26 accountTypeAttr = "account-type" 27 accountTypeStandardLRS = "Standard_LRS" 28 accountTypePremiumLRS = "Premium_LRS" 29 30 // volumeSizeMaxGiB is the maximum disk size (in gibibytes) for Azure disks. 31 // 32 // See: https://azure.microsoft.com/en-gb/documentation/articles/virtual-machines-disks-vhds/ 33 volumeSizeMaxGiB = 1023 34 ) 35 36 // StorageProviderTypes implements storage.ProviderRegistry. 37 func (env *azureEnviron) StorageProviderTypes() ([]storage.ProviderType, error) { 38 return []storage.ProviderType{azureStorageProviderType}, nil 39 } 40 41 // StorageProvider implements storage.ProviderRegistry. 42 func (env *azureEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 43 if t == azureStorageProviderType { 44 return &azureStorageProvider{env}, nil 45 } 46 return nil, errors.NotFoundf("storage provider %q", t) 47 } 48 49 // azureStorageProvider is a storage provider for Azure disks. 50 type azureStorageProvider struct { 51 env *azureEnviron 52 } 53 54 var _ storage.Provider = (*azureStorageProvider)(nil) 55 56 var azureStorageConfigFields = schema.Fields{ 57 accountTypeAttr: schema.OneOf( 58 schema.Const(accountTypeStandardLRS), 59 schema.Const(accountTypePremiumLRS), 60 ), 61 } 62 63 var azureStorageConfigChecker = schema.FieldMap( 64 azureStorageConfigFields, 65 schema.Defaults{ 66 accountTypeAttr: accountTypeStandardLRS, 67 }, 68 ) 69 70 type azureStorageConfig struct { 71 storageType armcompute.DiskStorageAccountTypes 72 } 73 74 func newAzureStorageConfig(attrs map[string]interface{}) (*azureStorageConfig, error) { 75 coerced, err := azureStorageConfigChecker.Coerce(attrs, nil) 76 if err != nil { 77 return nil, errors.Annotate(err, "validating Azure storage config") 78 } 79 attrs = coerced.(map[string]interface{}) 80 azureStorageConfig := &azureStorageConfig{ 81 storageType: armcompute.DiskStorageAccountTypes(attrs[accountTypeAttr].(string)), 82 } 83 return azureStorageConfig, nil 84 } 85 86 func (e *azureStorageProvider) ValidateForK8s(map[string]any) error { 87 // no validation required 88 return nil 89 } 90 91 // ValidateConfig is part of the Provider interface. 92 func (e *azureStorageProvider) ValidateConfig(cfg *storage.Config) error { 93 _, err := newAzureStorageConfig(cfg.Attrs()) 94 return errors.Trace(err) 95 } 96 97 // Supports is part of the Provider interface. 98 func (e *azureStorageProvider) Supports(k storage.StorageKind) bool { 99 return k == storage.StorageKindBlock 100 } 101 102 // Scope is part of the Provider interface. 103 func (e *azureStorageProvider) Scope() storage.Scope { 104 return storage.ScopeEnviron 105 } 106 107 // Dynamic is part of the Provider interface. 108 func (e *azureStorageProvider) Dynamic() bool { 109 return true 110 } 111 112 // Releasable is part of the Provider interface. 113 func (e *azureStorageProvider) Releasable() bool { 114 // NOTE(axw) Azure storage is currently tied to a model, and cannot 115 // be released or imported. To support releasing and importing, we'll 116 // need Azure to support moving managed disks between resource groups. 117 return false 118 } 119 120 // DefaultPools is part of the Provider interface. 121 func (e *azureStorageProvider) DefaultPools() []*storage.Config { 122 premiumPool, _ := storage.NewConfig("azure-premium", azureStorageProviderType, map[string]interface{}{ 123 accountTypeAttr: accountTypePremiumLRS, 124 }) 125 return []*storage.Config{premiumPool} 126 } 127 128 // VolumeSource is part of the Provider interface. 129 func (e *azureStorageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { 130 return &azureVolumeSource{e.env}, nil 131 } 132 133 // FilesystemSource is part of the Provider interface. 134 func (e *azureStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 135 return nil, errors.NotSupportedf("filesystems") 136 } 137 138 type azureVolumeSource struct { 139 env *azureEnviron 140 } 141 142 // CreateVolumes is specified on the storage.VolumeSource interface. 143 func (v *azureVolumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) { 144 results := make([]storage.CreateVolumesResult, len(params)) 145 for i, p := range params { 146 if err := v.ValidateVolumeParams(p); err != nil { 147 results[i].Error = err 148 continue 149 } 150 } 151 v.createManagedDiskVolumes(ctx, params, results) 152 return results, nil 153 } 154 155 // createManagedDiskVolumes creates volumes with associated managed disks. 156 func (v *azureVolumeSource) createManagedDiskVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams, results []storage.CreateVolumesResult) { 157 for i, p := range params { 158 if results[i].Error != nil { 159 continue 160 } 161 volume, err := v.createManagedDiskVolume(ctx, p) 162 if err != nil { 163 results[i].Error = err 164 continue 165 } 166 results[i].Volume = volume 167 } 168 } 169 170 // createManagedDiskVolume creates a managed disk. 171 func (v *azureVolumeSource) createManagedDiskVolume(ctx context.ProviderCallContext, p storage.VolumeParams) (*storage.Volume, error) { 172 cfg, err := newAzureStorageConfig(p.Attributes) 173 if err != nil { 174 return nil, errors.Trace(err) 175 } 176 177 diskTags := make(map[string]*string) 178 for k, v := range p.ResourceTags { 179 diskTags[k] = to.Ptr(v) 180 } 181 diskName := p.Tag.String() 182 sizeInGib := mibToGib(p.Size) 183 diskModel := armcompute.Disk{ 184 Name: to.Ptr(diskName), 185 Location: to.Ptr(v.env.location), 186 Tags: diskTags, 187 SKU: &armcompute.DiskSKU{ 188 Name: to.Ptr(cfg.storageType), 189 }, 190 Properties: &armcompute.DiskProperties{ 191 CreationData: &armcompute.CreationData{CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty)}, 192 DiskSizeGB: to.Ptr(int32(sizeInGib)), 193 }, 194 } 195 196 disks, err := v.env.disksClient() 197 if err != nil { 198 return nil, errors.Trace(err) 199 } 200 var result armcompute.DisksClientCreateOrUpdateResponse 201 poller, err := disks.BeginCreateOrUpdate(ctx, v.env.resourceGroup, diskName, diskModel, nil) 202 if err == nil { 203 result, err = poller.PollUntilDone(ctx, nil) 204 } 205 if err != nil || result.Properties == nil { 206 return nil, errorutils.HandleCredentialError(errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id()), ctx) 207 } 208 209 volume := storage.Volume{ 210 p.Tag, 211 storage.VolumeInfo{ 212 VolumeId: diskName, 213 Size: gibToMib(uint64(toValue(result.Properties.DiskSizeGB))), 214 Persistent: true, 215 }, 216 } 217 return &volume, nil 218 } 219 220 // ListVolumes is specified on the storage.VolumeSource interface. 221 func (v *azureVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) { 222 return v.listManagedDiskVolumes(ctx) 223 } 224 225 func (v *azureVolumeSource) listManagedDiskVolumes(ctx context.ProviderCallContext) ([]string, error) { 226 disks, err := v.env.disksClient() 227 if err != nil { 228 return nil, errors.Trace(err) 229 } 230 var volumeIds []string 231 pager := disks.NewListPager(nil) 232 for pager.More() { 233 next, err := pager.NextPage(ctx) 234 if err != nil { 235 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing disks"), ctx) 236 } 237 for _, d := range next.Value { 238 diskName := toValue(d.Name) 239 if _, err := names.ParseVolumeTag(diskName); err != nil { 240 continue 241 } 242 volumeIds = append(volumeIds, diskName) 243 } 244 } 245 return volumeIds, nil 246 } 247 248 // DescribeVolumes is specified on the storage.VolumeSource interface. 249 func (v *azureVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 250 return v.describeManagedDiskVolumes(ctx, volumeIds) 251 } 252 253 func (v *azureVolumeSource) describeManagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 254 results := make([]storage.DescribeVolumesResult, len(volumeIds)) 255 var wg sync.WaitGroup 256 for i, volumeId := range volumeIds { 257 wg.Add(1) 258 go func(i int, volumeId string) { 259 defer wg.Done() 260 disks, err := v.env.disksClient() 261 if err != nil { 262 results[i].Error = err 263 return 264 } 265 disk, err := disks.Get(ctx, v.env.resourceGroup, volumeId, nil) 266 if err != nil { 267 if errorutils.IsNotFoundError(err) { 268 err = errors.NotFoundf("disk %s", volumeId) 269 } 270 results[i].Error = errorutils.HandleCredentialError(err, ctx) 271 return 272 } 273 results[i].VolumeInfo = &storage.VolumeInfo{ 274 VolumeId: volumeId, 275 Size: gibToMib(uint64(toValue(disk.Properties.DiskSizeGB))), 276 Persistent: true, 277 } 278 }(i, volumeId) 279 } 280 wg.Wait() 281 return results, nil 282 } 283 284 // DestroyVolumes is specified on the storage.VolumeSource interface. 285 func (v *azureVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 286 return v.destroyManagedDiskVolumes(ctx, volumeIds) 287 } 288 289 func (v *azureVolumeSource) destroyManagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 290 return foreachVolume(volumeIds, func(volumeId string) error { 291 disks, err := v.env.disksClient() 292 if err != nil { 293 return errors.Trace(err) 294 } 295 poller, err := disks.BeginDelete(ctx, v.env.resourceGroup, volumeId, nil) 296 if err == nil { 297 _, err = poller.PollUntilDone(ctx, nil) 298 } 299 if err != nil { 300 if !errorutils.IsNotFoundError(err) { 301 return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting disk %q", volumeId), ctx) 302 } 303 } 304 return nil 305 }), nil 306 } 307 308 func foreachVolume(volumeIds []string, f func(string) error) []error { 309 results := make([]error, len(volumeIds)) 310 var wg sync.WaitGroup 311 for i, volumeId := range volumeIds { 312 wg.Add(1) 313 go func(i int, volumeId string) { 314 defer wg.Done() 315 results[i] = f(volumeId) 316 }(i, volumeId) 317 } 318 wg.Wait() 319 return results 320 } 321 322 // ReleaseVolumes is specified on the storage.VolumeSource interface. 323 func (v *azureVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 324 // Releasing volumes is not supported, see azureStorageProvider.Releasable. 325 // 326 // When managed disks can be moved between resource groups, we may want to 327 // support releasing unmanaged disks. We'll need to create a managed disk 328 // from the blob, and then release that. 329 return nil, errors.NotSupportedf("ReleaseVolumes") 330 } 331 332 // ValidateVolumeParams is specified on the storage.VolumeSource interface. 333 func (v *azureVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 334 if mibToGib(params.Size) > volumeSizeMaxGiB { 335 return errors.Errorf( 336 "%d GiB exceeds the maximum of %d GiB", 337 mibToGib(params.Size), 338 volumeSizeMaxGiB, 339 ) 340 } 341 return nil 342 } 343 344 // AttachVolumes is specified on the storage.VolumeSource interface. 345 func (v *azureVolumeSource) AttachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 346 results := make([]storage.AttachVolumesResult, len(attachParams)) 347 instanceIds := make([]instance.Id, len(attachParams)) 348 for i, p := range attachParams { 349 instanceIds[i] = p.InstanceId 350 } 351 if len(instanceIds) == 0 { 352 return results, nil 353 } 354 virtualMachines, err := v.virtualMachines(ctx, instanceIds) 355 if err != nil { 356 return nil, errors.Annotate(err, "getting virtual machines") 357 } 358 359 // Update VirtualMachine objects in-memory, 360 // and then perform the updates all at once. 361 // 362 // An attachment does not require an update 363 // if it is pre-existing, so we keep a record 364 // of which VMs need updating. 365 changed := make(map[instance.Id]bool, len(virtualMachines)) 366 for i, p := range attachParams { 367 vm, ok := virtualMachines[p.InstanceId] 368 if !ok { 369 continue 370 } 371 if vm.err != nil { 372 results[i].Error = vm.err 373 continue 374 } 375 volumeAttachment, updated, err := v.attachVolume(vm.vm, p) 376 if err != nil { 377 results[i].Error = err 378 vm.err = err 379 continue 380 } 381 results[i].VolumeAttachment = volumeAttachment 382 if updated { 383 changed[p.InstanceId] = true 384 } 385 } 386 for _, instanceId := range instanceIds { 387 if !changed[instanceId] { 388 delete(virtualMachines, instanceId) 389 } 390 } 391 392 updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds) 393 if err != nil { 394 return nil, errors.Annotate(err, "updating virtual machines") 395 } 396 for i, err := range updateResults { 397 if results[i].Error != nil || err == nil { 398 continue 399 } 400 results[i].Error = err 401 results[i].VolumeAttachment = nil 402 } 403 return results, nil 404 } 405 406 const azureDiskDeviceLink = "/dev/disk/azure/scsi1/lun%d" 407 408 func (v *azureVolumeSource) attachVolume( 409 vm *armcompute.VirtualMachine, 410 p storage.VolumeAttachmentParams, 411 ) (_ *storage.VolumeAttachment, updated bool, _ error) { 412 413 var dataDisks []*armcompute.DataDisk 414 if vm.Properties != nil && vm.Properties.StorageProfile.DataDisks != nil { 415 dataDisks = vm.Properties.StorageProfile.DataDisks 416 } 417 418 diskName := p.VolumeId 419 for _, disk := range dataDisks { 420 if toValue(disk.Name) != diskName { 421 continue 422 } 423 // Disk is already attached. 424 volumeAttachment := &storage.VolumeAttachment{ 425 Volume: p.Volume, 426 Machine: p.Machine, 427 VolumeAttachmentInfo: storage.VolumeAttachmentInfo{ 428 DeviceLink: fmt.Sprintf(azureDiskDeviceLink, toValue(disk.Lun)), 429 }, 430 } 431 return volumeAttachment, false, nil 432 } 433 434 volumeAttachment, err := v.addDataDisk(vm, diskName, p.Volume, p.Machine, armcompute.DiskCreateOptionTypesAttach, nil) 435 if err != nil { 436 return nil, false, errors.Trace(err) 437 } 438 return volumeAttachment, true, nil 439 } 440 441 func (v *azureVolumeSource) addDataDisk( 442 vm *armcompute.VirtualMachine, 443 diskName string, 444 volumeTag names.VolumeTag, 445 machineTag names.Tag, 446 createOption armcompute.DiskCreateOptionTypes, 447 diskSizeGB *int32, 448 ) (*storage.VolumeAttachment, error) { 449 450 lun, err := nextAvailableLUN(vm) 451 if err != nil { 452 return nil, errors.Annotate(err, "choosing LUN") 453 } 454 455 dataDisk := &armcompute.DataDisk{ 456 Lun: to.Ptr(lun), 457 Name: to.Ptr(diskName), 458 Caching: to.Ptr(armcompute.CachingTypesReadWrite), 459 CreateOption: to.Ptr(createOption), 460 DiskSizeGB: diskSizeGB, 461 } 462 diskResourceID := v.diskResourceID(diskName) 463 dataDisk.ManagedDisk = &armcompute.ManagedDiskParameters{ 464 ID: to.Ptr(diskResourceID), 465 } 466 467 if vm.Properties != nil { 468 var dataDisks []*armcompute.DataDisk 469 if vm.Properties.StorageProfile.DataDisks != nil { 470 dataDisks = vm.Properties.StorageProfile.DataDisks 471 } 472 dataDisks = append(dataDisks, dataDisk) 473 vm.Properties.StorageProfile.DataDisks = dataDisks 474 } 475 476 return &storage.VolumeAttachment{ 477 Volume: volumeTag, 478 Machine: machineTag, 479 VolumeAttachmentInfo: storage.VolumeAttachmentInfo{ 480 DeviceLink: fmt.Sprintf(azureDiskDeviceLink, lun), 481 }, 482 }, nil 483 } 484 485 // DetachVolumes is specified on the storage.VolumeSource interface. 486 func (v *azureVolumeSource) DetachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) { 487 results := make([]error, len(attachParams)) 488 instanceIds := make([]instance.Id, len(attachParams)) 489 for i, p := range attachParams { 490 instanceIds[i] = p.InstanceId 491 } 492 if len(instanceIds) == 0 { 493 return results, nil 494 } 495 virtualMachines, err := v.virtualMachines(ctx, instanceIds) 496 if err != nil { 497 return nil, errors.Annotate(err, "getting virtual machines") 498 } 499 500 // Update VirtualMachine objects in-memory, 501 // and then perform the updates all at once. 502 // 503 // An detachment does not require an update 504 // if the disk isn't attached, so we keep a 505 // record of which VMs need updating. 506 changed := make(map[instance.Id]bool, len(virtualMachines)) 507 for i, p := range attachParams { 508 vm, ok := virtualMachines[p.InstanceId] 509 if !ok { 510 continue 511 } 512 if vm.err != nil { 513 results[i] = vm.err 514 continue 515 } 516 if v.detachVolume(vm.vm, p) { 517 changed[p.InstanceId] = true 518 } 519 } 520 for _, instanceId := range instanceIds { 521 if !changed[instanceId] { 522 delete(virtualMachines, instanceId) 523 } 524 } 525 526 updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds) 527 if err != nil { 528 return nil, errors.Annotate(err, "updating virtual machines") 529 } 530 for i, err := range updateResults { 531 if results[i] != nil || err == nil { 532 continue 533 } 534 results[i] = err 535 } 536 return results, nil 537 } 538 539 func (v *azureVolumeSource) detachVolume( 540 vm *armcompute.VirtualMachine, 541 p storage.VolumeAttachmentParams, 542 ) (updated bool) { 543 544 if vm.Properties == nil { 545 return false 546 } 547 548 var dataDisks []*armcompute.DataDisk 549 if vm.Properties.StorageProfile.DataDisks != nil { 550 dataDisks = vm.Properties.StorageProfile.DataDisks 551 } 552 for i, disk := range dataDisks { 553 if toValue(disk.Name) != p.VolumeId { 554 continue 555 } 556 dataDisks = append(dataDisks[:i], dataDisks[i+1:]...) 557 vm.Properties.StorageProfile.DataDisks = dataDisks 558 return true 559 } 560 return false 561 } 562 563 // diskResourceID returns the full resource ID for a disk, given its name. 564 func (v *azureVolumeSource) diskResourceID(name string) string { 565 return path.Join( 566 "/subscriptions", 567 v.env.subscriptionId, 568 "resourceGroups", 569 v.env.resourceGroup, 570 "providers", 571 "Microsoft.Compute", 572 "disks", 573 name, 574 ) 575 } 576 577 type maybeVirtualMachine struct { 578 vm *armcompute.VirtualMachine 579 err error 580 } 581 582 // virtualMachines returns a mapping of instance IDs to VirtualMachines and 583 // errors, for each of the specified instance IDs. 584 func (v *azureVolumeSource) virtualMachines(ctx context.ProviderCallContext, instanceIds []instance.Id) (map[instance.Id]*maybeVirtualMachine, error) { 585 compute, err := v.env.computeClient() 586 if err != nil { 587 return nil, errors.Trace(err) 588 } 589 all := make(map[instance.Id]*armcompute.VirtualMachine) 590 pager := compute.NewListPager(v.env.resourceGroup, nil) 591 for pager.More() { 592 next, err := pager.NextPage(ctx) 593 if err != nil { 594 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing virtual machines"), ctx) 595 } 596 for _, vm := range next.Value { 597 vmCopy := *vm 598 all[instance.Id(toValue(vmCopy.Name))] = &vmCopy 599 } 600 } 601 results := make(map[instance.Id]*maybeVirtualMachine) 602 for _, id := range instanceIds { 603 result := &maybeVirtualMachine{vm: all[id]} 604 if result.vm == nil { 605 result.err = errors.NotFoundf("instance %v", id) 606 } 607 results[id] = result 608 } 609 return results, nil 610 } 611 612 // updateVirtualMachines updates virtual machines in the given map by iterating 613 // through the list of instance IDs in order, and updating each corresponding 614 // virtual machine at most once. 615 func (v *azureVolumeSource) updateVirtualMachines( 616 ctx context.ProviderCallContext, 617 virtualMachines map[instance.Id]*maybeVirtualMachine, instanceIds []instance.Id, 618 ) ([]error, error) { 619 compute, err := v.env.computeClient() 620 if err != nil { 621 return nil, errors.Trace(err) 622 } 623 624 results := make([]error, len(instanceIds)) 625 for i, instanceId := range instanceIds { 626 vm, ok := virtualMachines[instanceId] 627 if !ok { 628 continue 629 } 630 if vm.err != nil { 631 results[i] = vm.err 632 continue 633 } 634 poller, err := compute.BeginCreateOrUpdate( 635 ctx, 636 v.env.resourceGroup, toValue(vm.vm.Name), *vm.vm, nil, 637 ) 638 if err == nil { 639 _, err = poller.PollUntilDone(ctx, nil) 640 } 641 if err != nil { 642 if errorutils.MaybeInvalidateCredential(err, ctx) { 643 return nil, errors.Trace(err) 644 } 645 results[i] = err 646 vm.err = err 647 continue 648 } 649 // successfully updated, don't update again 650 delete(virtualMachines, instanceId) 651 } 652 return results, nil 653 } 654 655 func nextAvailableLUN(vm *armcompute.VirtualMachine) (int32, error) { 656 // Pick the smallest LUN not in use. We have to choose them in order, 657 // or the disks don't show up. 658 var inUse [32]bool 659 if vm.Properties != nil && vm.Properties.StorageProfile.DataDisks != nil { 660 for _, disk := range vm.Properties.StorageProfile.DataDisks { 661 lun := toValue(disk.Lun) 662 if lun < 0 || lun > 31 { 663 logger.Debugf("ignore disk with invalid LUN: %+v", disk) 664 continue 665 } 666 inUse[lun] = true 667 } 668 } 669 for i, inUse := range inUse { 670 if !inUse { 671 return int32(i), nil 672 } 673 } 674 return -1, errors.New("all LUNs are in use") 675 } 676 677 // mibToGib converts mebibytes to gibibytes. 678 // AWS expects GiB, we work in MiB; round up 679 // to nearest GiB. 680 func mibToGib(m uint64) uint64 { 681 return (m + 1023) / 1024 682 } 683 684 // gibToMib converts gibibytes to mebibytes. 685 func gibToMib(g uint64) uint64 { 686 return g * 1024 687 }