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