github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "strings" 9 10 "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to" 11 "github.com/Azure/azure-sdk-for-go/arm/compute" 12 azurestorage "github.com/Azure/azure-sdk-for-go/storage" 13 "github.com/juju/errors" 14 "github.com/juju/names" 15 "github.com/juju/schema" 16 "github.com/juju/utils" 17 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/environs/config" 20 "github.com/juju/juju/instance" 21 internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage" 22 "github.com/juju/juju/storage" 23 ) 24 25 const ( 26 // volumeSizeMaxGiB is the maximum disk size (in gibibytes) for Azure disks. 27 // 28 // See: https://azure.microsoft.com/en-gb/documentation/articles/virtual-machines-disks-vhds/ 29 volumeSizeMaxGiB = 1023 30 31 // osDiskVHDContainer is the name of the blob container for VHDs 32 // backing OS disks. 33 osDiskVHDContainer = "osvhds" 34 35 // dataDiskVHDContainer is the name of the blob container for VHDs 36 // backing data disks. 37 dataDiskVHDContainer = "datavhds" 38 39 // vhdExtension is the filename extension we give to VHDs we create. 40 vhdExtension = ".vhd" 41 ) 42 43 // azureStorageProvider is a storage provider for Azure disks. 44 type azureStorageProvider struct { 45 environProvider *azureEnvironProvider 46 } 47 48 var _ storage.Provider = (*azureStorageProvider)(nil) 49 50 var azureStorageConfigFields = schema.Fields{} 51 52 var azureStorageConfigChecker = schema.FieldMap( 53 azureStorageConfigFields, 54 schema.Defaults{}, 55 ) 56 57 type azureStorageConfig struct { 58 } 59 60 func newAzureStorageConfig(attrs map[string]interface{}) (*azureStorageConfig, error) { 61 _, err := azureStorageConfigChecker.Coerce(attrs, nil) 62 if err != nil { 63 return nil, errors.Annotate(err, "validating Azure storage config") 64 } 65 azureStorageConfig := &azureStorageConfig{} 66 return azureStorageConfig, nil 67 } 68 69 // ValidateConfig is defined on the Provider interface. 70 func (e *azureStorageProvider) ValidateConfig(cfg *storage.Config) error { 71 _, err := newAzureStorageConfig(cfg.Attrs()) 72 return errors.Trace(err) 73 } 74 75 // Supports is defined on the Provider interface. 76 func (e *azureStorageProvider) Supports(k storage.StorageKind) bool { 77 return k == storage.StorageKindBlock 78 } 79 80 // Scope is defined on the Provider interface. 81 func (e *azureStorageProvider) Scope() storage.Scope { 82 return storage.ScopeEnviron 83 } 84 85 // Dynamic is defined on the Provider interface. 86 func (e *azureStorageProvider) Dynamic() bool { 87 return true 88 } 89 90 // VolumeSource is defined on the Provider interface. 91 func (e *azureStorageProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) { 92 if err := e.ValidateConfig(cfg); err != nil { 93 return nil, errors.Trace(err) 94 } 95 env, err := newEnviron(e.environProvider, environConfig) 96 if err != nil { 97 return nil, errors.Trace(err) 98 } 99 return &azureVolumeSource{env}, nil 100 } 101 102 // FilesystemSource is defined on the Provider interface. 103 func (e *azureStorageProvider) FilesystemSource( 104 environConfig *config.Config, providerConfig *storage.Config, 105 ) (storage.FilesystemSource, error) { 106 return nil, errors.NotSupportedf("filesystems") 107 } 108 109 type azureVolumeSource struct { 110 env *azureEnviron 111 } 112 113 // CreateVolumes is specified on the storage.VolumeSource interface. 114 func (v *azureVolumeSource) CreateVolumes(params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) { 115 116 // First, validate the params before we use them. 117 results := make([]storage.CreateVolumesResult, len(params)) 118 var instanceIds []instance.Id 119 for i, p := range params { 120 if err := v.ValidateVolumeParams(p); err != nil { 121 results[i].Error = err 122 continue 123 } 124 instanceIds = append(instanceIds, p.Attachment.InstanceId) 125 } 126 if len(instanceIds) == 0 { 127 return results, nil 128 } 129 virtualMachines, err := v.virtualMachines(instanceIds) 130 if err != nil { 131 return nil, errors.Annotate(err, "getting virtual machines") 132 } 133 134 // Update VirtualMachine objects in-memory, 135 // and then perform the updates all at once. 136 for i, p := range params { 137 if results[i].Error != nil { 138 continue 139 } 140 vm, ok := virtualMachines[p.Attachment.InstanceId] 141 if !ok { 142 continue 143 } 144 if vm.err != nil { 145 results[i].Error = vm.err 146 continue 147 } 148 volume, volumeAttachment, err := v.createVolume(vm.vm, p) 149 if err != nil { 150 results[i].Error = err 151 vm.err = err 152 continue 153 } 154 results[i].Volume = volume 155 results[i].VolumeAttachment = volumeAttachment 156 } 157 158 updateResults, err := v.updateVirtualMachines(virtualMachines, instanceIds) 159 if err != nil { 160 return nil, errors.Annotate(err, "updating virtual machines") 161 } 162 for i, err := range updateResults { 163 if results[i].Error != nil || err == nil { 164 continue 165 } 166 results[i].Error = err 167 results[i].Volume = nil 168 results[i].VolumeAttachment = nil 169 } 170 return results, nil 171 } 172 173 // createVolume updates the provided VirtualMachine's StorageProfile with the 174 // parameters for creating a new data disk. We don't actually interact with 175 // the Azure API until after all changes to the VirtualMachine are made. 176 func (v *azureVolumeSource) createVolume( 177 vm *compute.VirtualMachine, 178 p storage.VolumeParams, 179 ) (*storage.Volume, *storage.VolumeAttachment, error) { 180 181 lun, err := nextAvailableLUN(vm) 182 if err != nil { 183 return nil, nil, errors.Annotate(err, "choosing LUN") 184 } 185 186 dataDisksRoot := dataDiskVhdRoot(v.env.config.storageEndpoint, v.env.config.storageAccount) 187 dataDiskName := p.Tag.String() 188 vhdURI := dataDisksRoot + dataDiskName + vhdExtension 189 190 sizeInGib := mibToGib(p.Size) 191 dataDisk := compute.DataDisk{ 192 Lun: to.IntPtr(lun), 193 DiskSizeGB: to.IntPtr(int(sizeInGib)), 194 Name: to.StringPtr(dataDiskName), 195 Vhd: &compute.VirtualHardDisk{to.StringPtr(vhdURI)}, 196 Caching: compute.ReadWrite, 197 CreateOption: compute.Empty, 198 } 199 200 var dataDisks []compute.DataDisk 201 if vm.Properties.StorageProfile.DataDisks != nil { 202 dataDisks = *vm.Properties.StorageProfile.DataDisks 203 } 204 dataDisks = append(dataDisks, dataDisk) 205 vm.Properties.StorageProfile.DataDisks = &dataDisks 206 207 // Data disks associate VHDs to machines. In Juju's storage model, 208 // the VHD is the volume and the disk is the volume attachment. 209 volume := storage.Volume{ 210 p.Tag, 211 storage.VolumeInfo{ 212 VolumeId: dataDiskName, 213 Size: gibToMib(sizeInGib), 214 // We don't currently support persistent volumes in 215 // Azure, as it requires removal of "comp=media" when 216 // deleting VMs, complicating cleanup. 217 Persistent: true, 218 }, 219 } 220 volumeAttachment := storage.VolumeAttachment{ 221 p.Tag, 222 p.Attachment.Machine, 223 storage.VolumeAttachmentInfo{ 224 BusAddress: diskBusAddress(lun), 225 }, 226 } 227 return &volume, &volumeAttachment, nil 228 } 229 230 // ListVolumes is specified on the storage.VolumeSource interface. 231 func (v *azureVolumeSource) ListVolumes() ([]string, error) { 232 blobs, err := v.listBlobs() 233 if err != nil { 234 return nil, errors.Annotate(err, "listing volumes") 235 } 236 volumeIds := make([]string, 0, len(blobs)) 237 for _, blob := range blobs { 238 volumeId, ok := blobVolumeId(blob) 239 if !ok { 240 continue 241 } 242 volumeIds = append(volumeIds, volumeId) 243 } 244 return volumeIds, nil 245 } 246 247 // listBlobs returns a list of blobs in the data-disk container. 248 func (v *azureVolumeSource) listBlobs() ([]azurestorage.Blob, error) { 249 client, err := v.env.getStorageClient() 250 if err != nil { 251 return nil, errors.Trace(err) 252 } 253 blobsClient := client.GetBlobService() 254 // TODO(axw) handle pagination 255 // TODO(axw) consider taking a set of IDs and computing the 256 // longest common prefix to pass in the parameters 257 response, err := blobsClient.ListBlobs( 258 dataDiskVHDContainer, azurestorage.ListBlobsParameters{}, 259 ) 260 if err != nil { 261 if err, ok := err.(azurestorage.AzureStorageServiceError); ok { 262 switch err.Code { 263 case "ContainerNotFound": 264 return nil, nil 265 } 266 } 267 return nil, errors.Annotate(err, "listing blobs") 268 } 269 return response.Blobs, nil 270 } 271 272 // DescribeVolumes is specified on the storage.VolumeSource interface. 273 func (v *azureVolumeSource) DescribeVolumes(volumeIds []string) ([]storage.DescribeVolumesResult, error) { 274 blobs, err := v.listBlobs() 275 if err != nil { 276 return nil, errors.Annotate(err, "listing volumes") 277 } 278 279 byVolumeId := make(map[string]azurestorage.Blob) 280 for _, blob := range blobs { 281 volumeId, ok := blobVolumeId(blob) 282 if !ok { 283 continue 284 } 285 byVolumeId[volumeId] = blob 286 } 287 288 results := make([]storage.DescribeVolumesResult, len(volumeIds)) 289 for i, volumeId := range volumeIds { 290 blob, ok := byVolumeId[volumeId] 291 if !ok { 292 results[i].Error = errors.NotFoundf("%s", volumeId) 293 continue 294 } 295 sizeInMib := blob.Properties.ContentLength / (1024 * 1024) 296 results[i].VolumeInfo = &storage.VolumeInfo{ 297 VolumeId: volumeId, 298 Size: uint64(sizeInMib), 299 Persistent: true, 300 } 301 } 302 303 return results, nil 304 } 305 306 // DestroyVolumes is specified on the storage.VolumeSource interface. 307 func (v *azureVolumeSource) DestroyVolumes(volumeIds []string) ([]error, error) { 308 client, err := v.env.getStorageClient() 309 if err != nil { 310 return nil, errors.Trace(err) 311 } 312 blobsClient := client.GetBlobService() 313 results := make([]error, len(volumeIds)) 314 for i, volumeId := range volumeIds { 315 _, err := blobsClient.DeleteBlobIfExists( 316 dataDiskVHDContainer, volumeId+vhdExtension, 317 ) 318 results[i] = err 319 } 320 return results, nil 321 } 322 323 // ValidateVolumeParams is specified on the storage.VolumeSource interface. 324 func (v *azureVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 325 if mibToGib(params.Size) > volumeSizeMaxGiB { 326 return errors.Errorf( 327 "%d GiB exceeds the maximum of %d GiB", 328 mibToGib(params.Size), 329 volumeSizeMaxGiB, 330 ) 331 } 332 return nil 333 } 334 335 // AttachVolumes is specified on the storage.VolumeSource interface. 336 func (v *azureVolumeSource) AttachVolumes(attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 337 results := make([]storage.AttachVolumesResult, len(attachParams)) 338 instanceIds := make([]instance.Id, len(attachParams)) 339 for i, p := range attachParams { 340 instanceIds[i] = p.InstanceId 341 } 342 if len(instanceIds) == 0 { 343 return results, nil 344 } 345 virtualMachines, err := v.virtualMachines(instanceIds) 346 if err != nil { 347 return nil, errors.Annotate(err, "getting virtual machines") 348 } 349 350 // Update VirtualMachine objects in-memory, 351 // and then perform the updates all at once. 352 // 353 // An attachment does not require an update 354 // if it is pre-existing, so we keep a record 355 // of which VMs need updating. 356 changed := make(map[instance.Id]bool, len(virtualMachines)) 357 for i, p := range attachParams { 358 vm, ok := virtualMachines[p.InstanceId] 359 if !ok { 360 continue 361 } 362 if vm.err != nil { 363 results[i].Error = vm.err 364 continue 365 } 366 volumeAttachment, updated, err := v.attachVolume(vm.vm, p) 367 if err != nil { 368 results[i].Error = err 369 vm.err = err 370 continue 371 } 372 results[i].VolumeAttachment = volumeAttachment 373 if updated { 374 changed[p.InstanceId] = true 375 } 376 } 377 for _, instanceId := range instanceIds { 378 if !changed[instanceId] { 379 delete(virtualMachines, instanceId) 380 } 381 } 382 383 updateResults, err := v.updateVirtualMachines(virtualMachines, instanceIds) 384 if err != nil { 385 return nil, errors.Annotate(err, "updating virtual machines") 386 } 387 for i, err := range updateResults { 388 if results[i].Error != nil || err == nil { 389 continue 390 } 391 results[i].Error = err 392 results[i].VolumeAttachment = nil 393 } 394 return results, nil 395 } 396 397 func (v *azureVolumeSource) attachVolume( 398 vm *compute.VirtualMachine, 399 p storage.VolumeAttachmentParams, 400 ) (_ *storage.VolumeAttachment, updated bool, _ error) { 401 402 dataDisksRoot := dataDiskVhdRoot(v.env.config.storageEndpoint, v.env.config.storageAccount) 403 dataDiskName := p.VolumeId 404 vhdURI := dataDisksRoot + dataDiskName + vhdExtension 405 406 var dataDisks []compute.DataDisk 407 if vm.Properties.StorageProfile.DataDisks != nil { 408 dataDisks = *vm.Properties.StorageProfile.DataDisks 409 } 410 for _, disk := range dataDisks { 411 if to.String(disk.Name) != p.VolumeId { 412 continue 413 } 414 if to.String(disk.Vhd.URI) != vhdURI { 415 continue 416 } 417 // Disk is already attached. 418 volumeAttachment := &storage.VolumeAttachment{ 419 p.Volume, 420 p.Machine, 421 storage.VolumeAttachmentInfo{ 422 BusAddress: diskBusAddress(to.Int(disk.Lun)), 423 }, 424 } 425 return volumeAttachment, false, nil 426 } 427 428 lun, err := nextAvailableLUN(vm) 429 if err != nil { 430 return nil, false, errors.Annotate(err, "choosing LUN") 431 } 432 433 dataDisk := compute.DataDisk{ 434 Lun: to.IntPtr(lun), 435 Name: to.StringPtr(dataDiskName), 436 Vhd: &compute.VirtualHardDisk{to.StringPtr(vhdURI)}, 437 Caching: compute.ReadWrite, 438 CreateOption: compute.Attach, 439 } 440 dataDisks = append(dataDisks, dataDisk) 441 vm.Properties.StorageProfile.DataDisks = &dataDisks 442 443 volumeAttachment := storage.VolumeAttachment{ 444 p.Volume, 445 p.Machine, 446 storage.VolumeAttachmentInfo{ 447 BusAddress: diskBusAddress(lun), 448 }, 449 } 450 return &volumeAttachment, true, nil 451 } 452 453 // DetachVolumes is specified on the storage.VolumeSource interface. 454 func (v *azureVolumeSource) DetachVolumes(attachParams []storage.VolumeAttachmentParams) ([]error, error) { 455 results := make([]error, len(attachParams)) 456 instanceIds := make([]instance.Id, len(attachParams)) 457 for i, p := range attachParams { 458 instanceIds[i] = p.InstanceId 459 } 460 if len(instanceIds) == 0 { 461 return results, nil 462 } 463 virtualMachines, err := v.virtualMachines(instanceIds) 464 if err != nil { 465 return nil, errors.Annotate(err, "getting virtual machines") 466 } 467 468 // Update VirtualMachine objects in-memory, 469 // and then perform the updates all at once. 470 // 471 // An detachment does not require an update 472 // if the disk isn't attached, so we keep a 473 // record of which VMs need updating. 474 changed := make(map[instance.Id]bool, len(virtualMachines)) 475 for i, p := range attachParams { 476 vm, ok := virtualMachines[p.InstanceId] 477 if !ok { 478 continue 479 } 480 if vm.err != nil { 481 results[i] = vm.err 482 continue 483 } 484 if v.detachVolume(vm.vm, p) { 485 changed[p.InstanceId] = true 486 } 487 } 488 for _, instanceId := range instanceIds { 489 if !changed[instanceId] { 490 delete(virtualMachines, instanceId) 491 } 492 } 493 494 updateResults, err := v.updateVirtualMachines(virtualMachines, instanceIds) 495 if err != nil { 496 return nil, errors.Annotate(err, "updating virtual machines") 497 } 498 for i, err := range updateResults { 499 if results[i] != nil || err == nil { 500 continue 501 } 502 results[i] = err 503 } 504 return results, nil 505 } 506 507 func (v *azureVolumeSource) detachVolume( 508 vm *compute.VirtualMachine, 509 p storage.VolumeAttachmentParams, 510 ) (updated bool) { 511 512 dataDisksRoot := dataDiskVhdRoot(v.env.config.storageEndpoint, v.env.config.storageAccount) 513 dataDiskName := p.VolumeId 514 vhdURI := dataDisksRoot + dataDiskName + vhdExtension 515 516 var dataDisks []compute.DataDisk 517 if vm.Properties.StorageProfile.DataDisks != nil { 518 dataDisks = *vm.Properties.StorageProfile.DataDisks 519 } 520 for i, disk := range dataDisks { 521 if to.String(disk.Name) != p.VolumeId { 522 continue 523 } 524 if to.String(disk.Vhd.URI) != vhdURI { 525 continue 526 } 527 dataDisks = append(dataDisks[:i], dataDisks[i+1:]...) 528 if len(dataDisks) == 0 { 529 vm.Properties.StorageProfile.DataDisks = nil 530 } else { 531 *vm.Properties.StorageProfile.DataDisks = dataDisks 532 } 533 return true 534 } 535 return false 536 } 537 538 type maybeVirtualMachine struct { 539 vm *compute.VirtualMachine 540 err error 541 } 542 543 // virtualMachines returns a mapping of instance IDs to VirtualMachines and 544 // errors, for each of the specified instance IDs. 545 func (v *azureVolumeSource) virtualMachines(instanceIds []instance.Id) (map[instance.Id]*maybeVirtualMachine, error) { 546 // Fetch all instances at once. Failure to find an instance should 547 // not cause the entire method to fail. 548 results := make(map[instance.Id]*maybeVirtualMachine) 549 instances, err := v.env.instances( 550 v.env.resourceGroup, 551 instanceIds, 552 false, /* don't refresh addresses */ 553 ) 554 switch err { 555 case nil, environs.ErrPartialInstances: 556 for i, inst := range instances { 557 vm := &maybeVirtualMachine{} 558 if inst != nil { 559 vm.vm = &inst.(*azureInstance).VirtualMachine 560 } else { 561 vm.err = errors.NotFoundf("instance %v", instanceIds[i]) 562 } 563 results[instanceIds[i]] = vm 564 } 565 case environs.ErrNoInstances: 566 for _, instanceId := range instanceIds { 567 results[instanceId] = &maybeVirtualMachine{ 568 err: errors.NotFoundf("instance %v", instanceId), 569 } 570 } 571 default: 572 return nil, errors.Annotate(err, "getting instances") 573 } 574 return results, nil 575 } 576 577 // updateVirtualMachines updates virtual machines in the given map by iterating 578 // through the list of instance IDs in order, and updating each corresponding 579 // virtual machine at most once. 580 func (v *azureVolumeSource) updateVirtualMachines( 581 virtualMachines map[instance.Id]*maybeVirtualMachine, instanceIds []instance.Id, 582 ) ([]error, error) { 583 results := make([]error, len(instanceIds)) 584 vmsClient := compute.VirtualMachinesClient{v.env.compute} 585 for i, instanceId := range instanceIds { 586 vm, ok := virtualMachines[instanceId] 587 if !ok { 588 continue 589 } 590 if vm.err != nil { 591 results[i] = vm.err 592 continue 593 } 594 if _, err := vmsClient.CreateOrUpdate(v.env.resourceGroup, to.String(vm.vm.Name), *vm.vm); err != nil { 595 results[i] = err 596 vm.err = err 597 continue 598 } 599 // successfully updated, don't update again 600 delete(virtualMachines, instanceId) 601 } 602 return results, nil 603 } 604 605 func nextAvailableLUN(vm *compute.VirtualMachine) (int, error) { 606 // Pick the smallest LUN not in use. We have to choose them in order, 607 // or the disks don't show up. 608 var inUse [32]bool 609 if vm.Properties.StorageProfile.DataDisks != nil { 610 for _, disk := range *vm.Properties.StorageProfile.DataDisks { 611 lun := to.Int(disk.Lun) 612 if lun < 0 || lun > 31 { 613 logger.Warningf("ignore disk with invalid LUN: %+v", disk) 614 continue 615 } 616 inUse[lun] = true 617 } 618 } 619 for i, inUse := range inUse { 620 if !inUse { 621 return i, nil 622 } 623 } 624 return -1, errors.New("all LUNs are in use") 625 } 626 627 // diskBusAddress returns the value to use in the BusAddress field of 628 // VolumeAttachmentInfo for a disk with the specified LUN. 629 func diskBusAddress(lun int) string { 630 return fmt.Sprintf("scsi@5:0.0.%d", lun) 631 } 632 633 // mibToGib converts mebibytes to gibibytes. 634 // AWS expects GiB, we work in MiB; round up 635 // to nearest GiB. 636 func mibToGib(m uint64) uint64 { 637 return (m + 1023) / 1024 638 } 639 640 // gibToMib converts gibibytes to mebibytes. 641 func gibToMib(g uint64) uint64 { 642 return g * 1024 643 } 644 645 // osDiskVhdRoot returns the URL to the blob container in which we store the 646 // VHDs for OS disks for the environment. 647 func osDiskVhdRoot(storageEndpoint, storageAccountName string) string { 648 return blobContainerURL(storageEndpoint, storageAccountName, osDiskVHDContainer) 649 } 650 651 // dataDiskVhdRoot returns the URL to the blob container in which we store the 652 // VHDs for data disks for the environment. 653 func dataDiskVhdRoot(storageEndpoint, storageAccountName string) string { 654 return blobContainerURL(storageEndpoint, storageAccountName, dataDiskVHDContainer) 655 } 656 657 // blobContainer returns the URL to the named blob container. 658 func blobContainerURL(storageEndpoint, storageAccountName, container string) string { 659 return fmt.Sprintf( 660 "https://%s.blob.%s/%s/", 661 storageAccountName, 662 storageEndpoint, 663 container, 664 ) 665 } 666 667 // blobVolumeId returns the volume ID for a blob, and a boolean reporting 668 // whether or not the blob's name matches the scheme we use. 669 func blobVolumeId(blob azurestorage.Blob) (string, bool) { 670 if !strings.HasSuffix(blob.Name, vhdExtension) { 671 return "", false 672 } 673 volumeId := blob.Name[:len(blob.Name)-len(vhdExtension)] 674 if _, err := names.ParseVolumeTag(volumeId); err != nil { 675 return "", false 676 } 677 return volumeId, true 678 } 679 680 // getStorageClient returns a new storage client, given an environ config 681 // and a constructor. 682 func getStorageClient( 683 newClient internalazurestorage.NewClientFunc, 684 cfg *azureModelConfig, 685 ) (internalazurestorage.Client, error) { 686 storageAccountName := cfg.storageAccount 687 storageAccountKey := cfg.storageAccountKey 688 storageEndpoint := cfg.storageEndpoint 689 const useHTTPS = true 690 return newClient( 691 storageAccountName, storageAccountKey, 692 storageEndpoint, azurestorage.DefaultAPIVersion, useHTTPS, 693 ) 694 } 695 696 // RandomStorageAccountName returns a random storage account name. 697 func RandomStorageAccountName() string { 698 const maxStorageAccountNameLen = 24 699 validRunes := append(utils.LowerAlpha, utils.Digits...) 700 return utils.RandomString(maxStorageAccountNameLen, validRunes) 701 }