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  }