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  }