github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	stdcontext "context"
     8  	"fmt"
     9  	"path"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute"
    14  	armstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-07-01/storage"
    15  	azurestorage "github.com/Azure/azure-sdk-for-go/storage"
    16  	"github.com/Azure/go-autorest/autorest/to"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/schema"
    19  	"gopkg.in/juju/names.v2"
    20  
    21  	"github.com/juju/juju/core/instance"
    22  	"github.com/juju/juju/environs/context"
    23  	"github.com/juju/juju/provider/azure/internal/armtemplates"
    24  	internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage"
    25  	"github.com/juju/juju/provider/azure/internal/errorutils"
    26  	"github.com/juju/juju/storage"
    27  )
    28  
    29  const (
    30  	azureStorageProviderType = "azure"
    31  
    32  	accountTypeAttr        = "account-type"
    33  	accountTypeStandardLRS = "Standard_LRS"
    34  	accountTypePremiumLRS  = "Premium_LRS"
    35  
    36  	// volumeSizeMaxGiB is the maximum disk size (in gibibytes) for Azure disks.
    37  	//
    38  	// See: https://azure.microsoft.com/en-gb/documentation/articles/virtual-machines-disks-vhds/
    39  	volumeSizeMaxGiB = 1023
    40  
    41  	// osDiskVHDContainer is the name of the blob container for VHDs
    42  	// backing OS disks.
    43  	osDiskVHDContainer = "osvhds"
    44  
    45  	// dataDiskVHDContainer is the name of the blob container for VHDs
    46  	// backing data disks.
    47  	dataDiskVHDContainer = "datavhds"
    48  
    49  	// vhdExtension is the filename extension we give to VHDs we create.
    50  	vhdExtension = ".vhd"
    51  )
    52  
    53  // StorageProviderTypes implements storage.ProviderRegistry.
    54  func (env *azureEnviron) StorageProviderTypes() ([]storage.ProviderType, error) {
    55  	return []storage.ProviderType{azureStorageProviderType}, nil
    56  }
    57  
    58  // StorageProvider implements storage.ProviderRegistry.
    59  func (env *azureEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    60  	if t == azureStorageProviderType {
    61  		return &azureStorageProvider{env}, nil
    62  	}
    63  	return nil, errors.NotFoundf("storage provider %q", t)
    64  }
    65  
    66  // azureStorageProvider is a storage provider for Azure disks.
    67  type azureStorageProvider struct {
    68  	env *azureEnviron
    69  }
    70  
    71  var _ storage.Provider = (*azureStorageProvider)(nil)
    72  
    73  var azureStorageConfigFields = schema.Fields{
    74  	accountTypeAttr: schema.OneOf(
    75  		schema.Const(accountTypeStandardLRS),
    76  		schema.Const(accountTypePremiumLRS),
    77  	),
    78  }
    79  
    80  var azureStorageConfigChecker = schema.FieldMap(
    81  	azureStorageConfigFields,
    82  	schema.Defaults{
    83  		accountTypeAttr: accountTypeStandardLRS,
    84  	},
    85  )
    86  
    87  type azureStorageConfig struct {
    88  	storageType compute.DiskStorageAccountTypes
    89  }
    90  
    91  func newAzureStorageConfig(attrs map[string]interface{}) (*azureStorageConfig, error) {
    92  	coerced, err := azureStorageConfigChecker.Coerce(attrs, nil)
    93  	if err != nil {
    94  		return nil, errors.Annotate(err, "validating Azure storage config")
    95  	}
    96  	attrs = coerced.(map[string]interface{})
    97  	azureStorageConfig := &azureStorageConfig{
    98  		storageType: compute.DiskStorageAccountTypes(attrs[accountTypeAttr].(string)),
    99  	}
   100  	return azureStorageConfig, nil
   101  }
   102  
   103  // ValidateConfig is part of the Provider interface.
   104  func (e *azureStorageProvider) ValidateConfig(cfg *storage.Config) error {
   105  	_, err := newAzureStorageConfig(cfg.Attrs())
   106  	return errors.Trace(err)
   107  }
   108  
   109  // Supports is part of the Provider interface.
   110  func (e *azureStorageProvider) Supports(k storage.StorageKind) bool {
   111  	return k == storage.StorageKindBlock
   112  }
   113  
   114  // Scope is part of the Provider interface.
   115  func (e *azureStorageProvider) Scope() storage.Scope {
   116  	return storage.ScopeEnviron
   117  }
   118  
   119  // Dynamic is part of the Provider interface.
   120  func (e *azureStorageProvider) Dynamic() bool {
   121  	return true
   122  }
   123  
   124  // Releasable is part of the Provider interface.
   125  func (e *azureStorageProvider) Releasable() bool {
   126  	// NOTE(axw) Azure storage is currently tied to a model, and cannot
   127  	// be released or imported. To support releasing and importing, we'll
   128  	// need Azure to support moving managed disks between resource groups.
   129  	return false
   130  }
   131  
   132  // DefaultPools is part of the Provider interface.
   133  func (e *azureStorageProvider) DefaultPools() []*storage.Config {
   134  	premiumPool, _ := storage.NewConfig("azure-premium", azureStorageProviderType, map[string]interface{}{
   135  		accountTypeAttr: accountTypePremiumLRS,
   136  	})
   137  	return []*storage.Config{premiumPool}
   138  }
   139  
   140  // VolumeSource is part of the Provider interface.
   141  func (e *azureStorageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) {
   142  	// Check to see if the environment has a storage account,
   143  	// which means it uses unmanaged disks. All models created
   144  	// before Juju 2.3 will have a storage account already, so
   145  	// it's safe to do the check up front.
   146  	maybeStorageClient, maybeStorageAccount, err := e.env.maybeGetStorageClient()
   147  	if err != nil {
   148  		return nil, errors.Trace(err)
   149  	}
   150  	return &azureVolumeSource{e.env, maybeStorageAccount, maybeStorageClient}, nil
   151  }
   152  
   153  // FilesystemSource is part of the Provider interface.
   154  func (e *azureStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
   155  	return nil, errors.NotSupportedf("filesystems")
   156  }
   157  
   158  type azureVolumeSource struct {
   159  	env                 *azureEnviron
   160  	maybeStorageAccount *armstorage.Account
   161  	maybeStorageClient  internalazurestorage.Client
   162  }
   163  
   164  // CreateVolumes is specified on the storage.VolumeSource interface.
   165  func (v *azureVolumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) {
   166  	results := make([]storage.CreateVolumesResult, len(params))
   167  	for i, p := range params {
   168  		if err := v.ValidateVolumeParams(p); err != nil {
   169  			results[i].Error = err
   170  			continue
   171  		}
   172  	}
   173  	if v.maybeStorageClient == nil {
   174  		v.createManagedDiskVolumes(ctx, params, results)
   175  		return results, nil
   176  	}
   177  	return results, v.createUnmanagedDiskVolumes(ctx, params, results)
   178  }
   179  
   180  // createManagedDiskVolumes creates volumes with associated managed disks.
   181  func (v *azureVolumeSource) createManagedDiskVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams, results []storage.CreateVolumesResult) {
   182  	for i, p := range params {
   183  		if results[i].Error != nil {
   184  			continue
   185  		}
   186  		volume, err := v.createManagedDiskVolume(ctx, p)
   187  		if err != nil {
   188  			results[i].Error = err
   189  			continue
   190  		}
   191  		results[i].Volume = volume
   192  	}
   193  }
   194  
   195  // createManagedDiskVolume creates a managed disk.
   196  func (v *azureVolumeSource) createManagedDiskVolume(ctx context.ProviderCallContext, p storage.VolumeParams) (*storage.Volume, error) {
   197  	cfg, err := newAzureStorageConfig(p.Attributes)
   198  	if err != nil {
   199  		return nil, errors.Trace(err)
   200  	}
   201  
   202  	diskTags := make(map[string]*string)
   203  	for k, v := range p.ResourceTags {
   204  		diskTags[k] = to.StringPtr(v)
   205  	}
   206  	diskName := p.Tag.String()
   207  	sizeInGib := mibToGib(p.Size)
   208  	diskModel := compute.Disk{
   209  		Name:     to.StringPtr(diskName),
   210  		Location: to.StringPtr(v.env.location),
   211  		Tags:     diskTags,
   212  		Sku: &compute.DiskSku{
   213  			Name: cfg.storageType,
   214  		},
   215  		DiskProperties: &compute.DiskProperties{
   216  			CreationData: &compute.CreationData{CreateOption: compute.Empty},
   217  			DiskSizeGB:   to.Int32Ptr(int32(sizeInGib)),
   218  		},
   219  	}
   220  
   221  	diskClient := compute.DisksClient{v.env.disk}
   222  	sdkCtx := stdcontext.Background()
   223  	future, err := diskClient.CreateOrUpdate(sdkCtx, v.env.resourceGroup, diskName, diskModel)
   224  	if err != nil {
   225  		return nil, errorutils.HandleCredentialError(errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id()), ctx)
   226  	}
   227  	err = future.WaitForCompletionRef(sdkCtx, diskClient.Client)
   228  	if err != nil {
   229  		return nil, errorutils.HandleCredentialError(errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id()), ctx)
   230  	}
   231  	result, err := future.Result(diskClient)
   232  	if err != nil && !isNotFoundResult(result.Response) {
   233  		return nil, errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id())
   234  	}
   235  
   236  	volume := storage.Volume{
   237  		p.Tag,
   238  		storage.VolumeInfo{
   239  			VolumeId:   diskName,
   240  			Size:       gibToMib(uint64(to.Int32(result.DiskSizeGB))),
   241  			Persistent: true,
   242  		},
   243  	}
   244  	return &volume, nil
   245  }
   246  
   247  // createUnmanagedDiskVolumes creates volumes with associated unmanaged disks (blobs).
   248  func (v *azureVolumeSource) createUnmanagedDiskVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams, results []storage.CreateVolumesResult) error {
   249  	var instanceIds []instance.Id
   250  	for i, p := range params {
   251  		if results[i].Error != nil {
   252  			continue
   253  		}
   254  		instanceIds = append(instanceIds, p.Attachment.InstanceId)
   255  	}
   256  	if len(instanceIds) == 0 {
   257  		return nil
   258  	}
   259  	virtualMachines, err := v.virtualMachines(ctx, instanceIds)
   260  	if err != nil {
   261  		return errors.Annotate(err, "getting virtual machines")
   262  	}
   263  	// Update VirtualMachine objects in-memory,
   264  	// and then perform the updates all at once.
   265  	for i, p := range params {
   266  		if results[i].Error != nil {
   267  			continue
   268  		}
   269  		vm, ok := virtualMachines[p.Attachment.InstanceId]
   270  		if !ok {
   271  			continue
   272  		}
   273  		if vm.err != nil {
   274  			results[i].Error = vm.err
   275  			continue
   276  		}
   277  		volume, volumeAttachment, err := v.createUnmanagedDiskVolume(vm.vm, p)
   278  		if err != nil {
   279  			results[i].Error = err
   280  			vm.err = err
   281  			continue
   282  		}
   283  		results[i].Volume = volume
   284  		results[i].VolumeAttachment = volumeAttachment
   285  	}
   286  
   287  	updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds)
   288  	if err != nil {
   289  		return errors.Annotate(err, "updating virtual machines")
   290  	}
   291  	for i, err := range updateResults {
   292  		if results[i].Error != nil || err == nil {
   293  			continue
   294  		}
   295  		results[i].Error = err
   296  		results[i].Volume = nil
   297  		results[i].VolumeAttachment = nil
   298  	}
   299  	return nil
   300  }
   301  
   302  // createUnmanagedDiskVolume updates the provided VirtualMachine's
   303  // StorageProfile with the parameters for creating a new unmanaged
   304  // data disk. We don't actually interact with the Azure API until
   305  // after all changes to the VirtualMachine are made.
   306  func (v *azureVolumeSource) createUnmanagedDiskVolume(
   307  	vm *compute.VirtualMachine,
   308  	p storage.VolumeParams,
   309  ) (*storage.Volume, *storage.VolumeAttachment, error) {
   310  
   311  	diskName := p.Tag.String()
   312  	sizeInGib := mibToGib(p.Size)
   313  	volumeAttachment, err := v.addDataDisk(
   314  		vm,
   315  		diskName,
   316  		p.Tag,
   317  		p.Attachment.Machine,
   318  		compute.DiskCreateOptionTypesEmpty,
   319  		to.Int32Ptr(int32(sizeInGib)),
   320  	)
   321  	if err != nil {
   322  		return nil, nil, errors.Trace(err)
   323  	}
   324  	// Data disks associate VHDs to machines. In Juju's storage model,
   325  	// the VHD is the volume and the disk is the volume attachment.
   326  	volume := storage.Volume{
   327  		p.Tag,
   328  		storage.VolumeInfo{
   329  			VolumeId:   diskName,
   330  			Size:       gibToMib(sizeInGib),
   331  			Persistent: true,
   332  		},
   333  	}
   334  	return &volume, volumeAttachment, nil
   335  }
   336  
   337  // ListVolumes is specified on the storage.VolumeSource interface.
   338  func (v *azureVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) {
   339  	if v.maybeStorageClient == nil {
   340  		return v.listManagedDiskVolumes(ctx)
   341  	}
   342  	return v.listUnmanagedDiskVolumes(ctx)
   343  }
   344  
   345  func (v *azureVolumeSource) listManagedDiskVolumes(ctx context.ProviderCallContext) ([]string, error) {
   346  	var volumeIds []string
   347  	diskClient := compute.DisksClient{v.env.disk}
   348  	sdkCtx := stdcontext.Background()
   349  	list, err := diskClient.ListComplete(sdkCtx)
   350  	if err != nil {
   351  		return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing disks"), ctx)
   352  	}
   353  	for ; list.NotDone(); err = list.NextWithContext(sdkCtx) {
   354  		if err != nil {
   355  			return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing disks"), ctx)
   356  		}
   357  		diskName := to.String(list.Value().Name)
   358  		if _, err := names.ParseVolumeTag(diskName); err != nil {
   359  			continue
   360  		}
   361  		volumeIds = append(volumeIds, diskName)
   362  	}
   363  	return volumeIds, nil
   364  }
   365  
   366  func (v *azureVolumeSource) listUnmanagedDiskVolumes(ctx context.ProviderCallContext) ([]string, error) {
   367  	blobs, err := v.listBlobs(ctx)
   368  	if err != nil {
   369  		return nil, errors.Annotate(err, "listing volumes")
   370  	}
   371  	volumeIds := make([]string, 0, len(blobs))
   372  	for _, blob := range blobs {
   373  		volumeId, ok := blobVolumeId(blob)
   374  		if !ok {
   375  			continue
   376  		}
   377  		volumeIds = append(volumeIds, volumeId)
   378  	}
   379  	return volumeIds, nil
   380  }
   381  
   382  // listBlobs returns a list of blobs in the data-disk container.
   383  func (v *azureVolumeSource) listBlobs(ctx context.ProviderCallContext) ([]internalazurestorage.Blob, error) {
   384  	blobsClient := v.maybeStorageClient.GetBlobService()
   385  	vhdContainer := blobsClient.GetContainerReference(dataDiskVHDContainer)
   386  	// TODO(axw) consider taking a set of IDs and computing the
   387  	//           longest common prefix to pass in the parameters
   388  	blobs, err := vhdContainer.Blobs()
   389  	if err != nil {
   390  		errorutils.HandleCredentialError(err, ctx)
   391  		if err, ok := err.(azurestorage.AzureStorageServiceError); ok {
   392  			switch err.Code {
   393  			case "ContainerNotFound":
   394  				return nil, nil
   395  			}
   396  		}
   397  		return nil, errors.Annotate(err, "listing blobs")
   398  	}
   399  	return blobs, nil
   400  }
   401  
   402  // DescribeVolumes is specified on the storage.VolumeSource interface.
   403  func (v *azureVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) {
   404  	if v.maybeStorageClient == nil {
   405  		return v.describeManagedDiskVolumes(ctx, volumeIds)
   406  	}
   407  	return v.describeUnmanagedDiskVolumes(ctx, volumeIds)
   408  }
   409  
   410  func (v *azureVolumeSource) describeManagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) {
   411  	diskClient := compute.DisksClient{v.env.disk}
   412  	results := make([]storage.DescribeVolumesResult, len(volumeIds))
   413  	var wg sync.WaitGroup
   414  	sdkCtx := stdcontext.Background()
   415  	for i, volumeId := range volumeIds {
   416  		wg.Add(1)
   417  		go func(i int, volumeId string) {
   418  			defer wg.Done()
   419  			disk, err := diskClient.Get(sdkCtx, v.env.resourceGroup, volumeId)
   420  			if err != nil {
   421  				if isNotFoundResult(disk.Response) {
   422  					err = errors.NotFoundf("disk %s", volumeId)
   423  				}
   424  				results[i].Error = errorutils.HandleCredentialError(err, ctx)
   425  				return
   426  			}
   427  			results[i].VolumeInfo = &storage.VolumeInfo{
   428  				VolumeId:   volumeId,
   429  				Size:       gibToMib(uint64(to.Int32(disk.DiskSizeGB))),
   430  				Persistent: true,
   431  			}
   432  		}(i, volumeId)
   433  	}
   434  	wg.Wait()
   435  	return results, nil
   436  }
   437  
   438  func (v *azureVolumeSource) describeUnmanagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) {
   439  	blobs, err := v.listBlobs(ctx)
   440  	if err != nil {
   441  		return nil, errors.Annotate(err, "listing volumes")
   442  	}
   443  
   444  	byVolumeId := make(map[string]internalazurestorage.Blob)
   445  	for _, blob := range blobs {
   446  		volumeId, ok := blobVolumeId(blob)
   447  		if !ok {
   448  			continue
   449  		}
   450  		byVolumeId[volumeId] = blob
   451  	}
   452  
   453  	results := make([]storage.DescribeVolumesResult, len(volumeIds))
   454  	for i, volumeId := range volumeIds {
   455  		blob, ok := byVolumeId[volumeId]
   456  		if !ok {
   457  			results[i].Error = errors.NotFoundf("%s", volumeId)
   458  			continue
   459  		}
   460  		sizeInMib := blob.Properties().ContentLength / (1024 * 1024)
   461  		results[i].VolumeInfo = &storage.VolumeInfo{
   462  			VolumeId:   volumeId,
   463  			Size:       uint64(sizeInMib),
   464  			Persistent: true,
   465  		}
   466  	}
   467  
   468  	return results, nil
   469  }
   470  
   471  // DestroyVolumes is specified on the storage.VolumeSource interface.
   472  func (v *azureVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   473  	if v.maybeStorageClient == nil {
   474  		return v.destroyManagedDiskVolumes(ctx, volumeIds)
   475  	}
   476  	return v.destroyUnmanagedDiskVolumes(ctx, volumeIds)
   477  }
   478  
   479  func (v *azureVolumeSource) destroyManagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   480  	diskClient := compute.DisksClient{v.env.disk}
   481  	sdkCtx := stdcontext.Background()
   482  	return foreachVolume(volumeIds, func(volumeId string) error {
   483  		future, err := diskClient.Delete(sdkCtx, v.env.resourceGroup, volumeId)
   484  		if err != nil {
   485  			if !isNotFoundResponse(future.Response()) {
   486  				return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting disk %q", volumeId), ctx)
   487  			}
   488  			return nil
   489  		}
   490  		err = future.WaitForCompletionRef(sdkCtx, diskClient.Client)
   491  		if err != nil {
   492  			return errors.Annotatef(err, "deleting disk %q", volumeId)
   493  		}
   494  		result, err := future.Result(diskClient)
   495  		if err != nil && !isNotFoundResult(result) {
   496  			return errors.Annotatef(err, "deleting disk %q", volumeId)
   497  		}
   498  		return nil
   499  	}), nil
   500  }
   501  
   502  func (v *azureVolumeSource) destroyUnmanagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   503  	blobsClient := v.maybeStorageClient.GetBlobService()
   504  	vhdContainer := blobsClient.GetContainerReference(dataDiskVHDContainer)
   505  	return foreachVolume(volumeIds, func(volumeId string) error {
   506  		vhdBlob := vhdContainer.Blob(volumeId + vhdExtension)
   507  		_, err := vhdBlob.DeleteIfExists(nil)
   508  		return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting blob %q", vhdBlob.Name()), ctx)
   509  	}), nil
   510  }
   511  
   512  func foreachVolume(volumeIds []string, f func(string) error) []error {
   513  	results := make([]error, len(volumeIds))
   514  	var wg sync.WaitGroup
   515  	for i, volumeId := range volumeIds {
   516  		wg.Add(1)
   517  		go func(i int, volumeId string) {
   518  			defer wg.Done()
   519  			results[i] = f(volumeId)
   520  		}(i, volumeId)
   521  	}
   522  	wg.Wait()
   523  	return results
   524  }
   525  
   526  // ReleaseVolumes is specified on the storage.VolumeSource interface.
   527  func (v *azureVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   528  	// Releasing volumes is not supported, see azureStorageProvider.Releasable.
   529  	//
   530  	// When managed disks can be moved between resource groups, we may want to
   531  	// support releasing unmanaged disks. We'll need to create a managed disk
   532  	// from the blob, and then release that.
   533  	return nil, errors.NotSupportedf("ReleaseVolumes")
   534  }
   535  
   536  // ValidateVolumeParams is specified on the storage.VolumeSource interface.
   537  func (v *azureVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   538  	if mibToGib(params.Size) > volumeSizeMaxGiB {
   539  		return errors.Errorf(
   540  			"%d GiB exceeds the maximum of %d GiB",
   541  			mibToGib(params.Size),
   542  			volumeSizeMaxGiB,
   543  		)
   544  	}
   545  	return nil
   546  }
   547  
   548  // AttachVolumes is specified on the storage.VolumeSource interface.
   549  func (v *azureVolumeSource) AttachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   550  	results := make([]storage.AttachVolumesResult, len(attachParams))
   551  	instanceIds := make([]instance.Id, len(attachParams))
   552  	for i, p := range attachParams {
   553  		instanceIds[i] = p.InstanceId
   554  	}
   555  	if len(instanceIds) == 0 {
   556  		return results, nil
   557  	}
   558  	virtualMachines, err := v.virtualMachines(ctx, instanceIds)
   559  	if err != nil {
   560  		return nil, errors.Annotate(err, "getting virtual machines")
   561  	}
   562  
   563  	// Update VirtualMachine objects in-memory,
   564  	// and then perform the updates all at once.
   565  	//
   566  	// An attachment does not require an update
   567  	// if it is pre-existing, so we keep a record
   568  	// of which VMs need updating.
   569  	changed := make(map[instance.Id]bool, len(virtualMachines))
   570  	for i, p := range attachParams {
   571  		vm, ok := virtualMachines[p.InstanceId]
   572  		if !ok {
   573  			continue
   574  		}
   575  		if vm.err != nil {
   576  			results[i].Error = vm.err
   577  			continue
   578  		}
   579  		volumeAttachment, updated, err := v.attachVolume(vm.vm, p)
   580  		if err != nil {
   581  			results[i].Error = err
   582  			vm.err = err
   583  			continue
   584  		}
   585  		results[i].VolumeAttachment = volumeAttachment
   586  		if updated {
   587  			changed[p.InstanceId] = true
   588  		}
   589  	}
   590  	for _, instanceId := range instanceIds {
   591  		if !changed[instanceId] {
   592  			delete(virtualMachines, instanceId)
   593  		}
   594  	}
   595  
   596  	updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds)
   597  	if err != nil {
   598  		return nil, errors.Annotate(err, "updating virtual machines")
   599  	}
   600  	for i, err := range updateResults {
   601  		if results[i].Error != nil || err == nil {
   602  			continue
   603  		}
   604  		results[i].Error = err
   605  		results[i].VolumeAttachment = nil
   606  	}
   607  	return results, nil
   608  }
   609  
   610  func (v *azureVolumeSource) attachVolume(
   611  	vm *compute.VirtualMachine,
   612  	p storage.VolumeAttachmentParams,
   613  ) (_ *storage.VolumeAttachment, updated bool, _ error) {
   614  
   615  	var dataDisks []compute.DataDisk
   616  	if vm.StorageProfile.DataDisks != nil {
   617  		dataDisks = *vm.StorageProfile.DataDisks
   618  	}
   619  
   620  	diskName := p.VolumeId
   621  	for _, disk := range dataDisks {
   622  		if to.String(disk.Name) != diskName {
   623  			continue
   624  		}
   625  		// Disk is already attached.
   626  		volumeAttachment := &storage.VolumeAttachment{
   627  			p.Volume,
   628  			p.Machine,
   629  			storage.VolumeAttachmentInfo{
   630  				BusAddress: diskBusAddress(to.Int32(disk.Lun)),
   631  			},
   632  		}
   633  		return volumeAttachment, false, nil
   634  	}
   635  
   636  	volumeAttachment, err := v.addDataDisk(vm, diskName, p.Volume, p.Machine, compute.DiskCreateOptionTypesAttach, nil)
   637  	if err != nil {
   638  		return nil, false, errors.Trace(err)
   639  	}
   640  	return volumeAttachment, true, nil
   641  }
   642  
   643  func (v *azureVolumeSource) addDataDisk(
   644  	vm *compute.VirtualMachine,
   645  	diskName string,
   646  	volumeTag names.VolumeTag,
   647  	machineTag names.Tag,
   648  	createOption compute.DiskCreateOptionTypes,
   649  	diskSizeGB *int32,
   650  ) (*storage.VolumeAttachment, error) {
   651  
   652  	lun, err := nextAvailableLUN(vm)
   653  	if err != nil {
   654  		return nil, errors.Annotate(err, "choosing LUN")
   655  	}
   656  
   657  	dataDisk := compute.DataDisk{
   658  		Lun:          to.Int32Ptr(lun),
   659  		Name:         to.StringPtr(diskName),
   660  		Caching:      compute.CachingTypesReadWrite,
   661  		CreateOption: createOption,
   662  		DiskSizeGB:   diskSizeGB,
   663  	}
   664  	if v.maybeStorageAccount == nil {
   665  		// This model uses managed disks.
   666  		diskResourceID := v.diskResourceID(diskName)
   667  		dataDisk.ManagedDisk = &compute.ManagedDiskParameters{
   668  			ID: to.StringPtr(diskResourceID),
   669  		}
   670  	} else {
   671  		// This model uses unmanaged disks.
   672  		dataDisksRoot := dataDiskVhdRoot(v.maybeStorageAccount)
   673  		vhdURI := dataDisksRoot + diskName + vhdExtension
   674  		dataDisk.Vhd = &compute.VirtualHardDisk{to.StringPtr(vhdURI)}
   675  	}
   676  
   677  	var dataDisks []compute.DataDisk
   678  	if vm.StorageProfile.DataDisks != nil {
   679  		dataDisks = *vm.StorageProfile.DataDisks
   680  	}
   681  	dataDisks = append(dataDisks, dataDisk)
   682  	vm.StorageProfile.DataDisks = &dataDisks
   683  
   684  	return &storage.VolumeAttachment{
   685  		volumeTag,
   686  		machineTag,
   687  		storage.VolumeAttachmentInfo{
   688  			BusAddress: diskBusAddress(lun),
   689  		},
   690  	}, nil
   691  }
   692  
   693  // DetachVolumes is specified on the storage.VolumeSource interface.
   694  func (v *azureVolumeSource) DetachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) {
   695  	results := make([]error, len(attachParams))
   696  	instanceIds := make([]instance.Id, len(attachParams))
   697  	for i, p := range attachParams {
   698  		instanceIds[i] = p.InstanceId
   699  	}
   700  	if len(instanceIds) == 0 {
   701  		return results, nil
   702  	}
   703  	virtualMachines, err := v.virtualMachines(ctx, instanceIds)
   704  	if err != nil {
   705  		return nil, errors.Annotate(err, "getting virtual machines")
   706  	}
   707  
   708  	// Update VirtualMachine objects in-memory,
   709  	// and then perform the updates all at once.
   710  	//
   711  	// An detachment does not require an update
   712  	// if the disk isn't attached, so we keep a
   713  	// record of which VMs need updating.
   714  	changed := make(map[instance.Id]bool, len(virtualMachines))
   715  	for i, p := range attachParams {
   716  		vm, ok := virtualMachines[p.InstanceId]
   717  		if !ok {
   718  			continue
   719  		}
   720  		if vm.err != nil {
   721  			results[i] = vm.err
   722  			continue
   723  		}
   724  		if v.detachVolume(vm.vm, p) {
   725  			changed[p.InstanceId] = true
   726  		}
   727  	}
   728  	for _, instanceId := range instanceIds {
   729  		if !changed[instanceId] {
   730  			delete(virtualMachines, instanceId)
   731  		}
   732  	}
   733  
   734  	updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds)
   735  	if err != nil {
   736  		return nil, errors.Annotate(err, "updating virtual machines")
   737  	}
   738  	for i, err := range updateResults {
   739  		if results[i] != nil || err == nil {
   740  			continue
   741  		}
   742  		results[i] = err
   743  	}
   744  	return results, nil
   745  }
   746  
   747  func (v *azureVolumeSource) detachVolume(
   748  	vm *compute.VirtualMachine,
   749  	p storage.VolumeAttachmentParams,
   750  ) (updated bool) {
   751  
   752  	var dataDisks []compute.DataDisk
   753  	if vm.StorageProfile.DataDisks != nil {
   754  		dataDisks = *vm.StorageProfile.DataDisks
   755  	}
   756  	for i, disk := range dataDisks {
   757  		if to.String(disk.Name) != p.VolumeId {
   758  			continue
   759  		}
   760  		dataDisks = append(dataDisks[:i], dataDisks[i+1:]...)
   761  		vm.StorageProfile.DataDisks = &dataDisks
   762  		return true
   763  	}
   764  	return false
   765  }
   766  
   767  // diskResourceID returns the full resource ID for a disk, given its name.
   768  func (v *azureVolumeSource) diskResourceID(name string) string {
   769  	return path.Join(
   770  		"/subscriptions",
   771  		v.env.subscriptionId,
   772  		"resourceGroups",
   773  		v.env.resourceGroup,
   774  		"providers",
   775  		"Microsoft.Compute",
   776  		"disks",
   777  		name,
   778  	)
   779  }
   780  
   781  type maybeVirtualMachine struct {
   782  	vm  *compute.VirtualMachine
   783  	err error
   784  }
   785  
   786  // virtualMachines returns a mapping of instance IDs to VirtualMachines and
   787  // errors, for each of the specified instance IDs.
   788  func (v *azureVolumeSource) virtualMachines(ctx context.ProviderCallContext, instanceIds []instance.Id) (map[instance.Id]*maybeVirtualMachine, error) {
   789  	vmsClient := compute.VirtualMachinesClient{v.env.compute}
   790  	sdkCtx := stdcontext.Background()
   791  	result, err := vmsClient.ListComplete(sdkCtx, v.env.resourceGroup)
   792  	if err != nil {
   793  		return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing virtual machines"), ctx)
   794  	}
   795  
   796  	all := make(map[instance.Id]*compute.VirtualMachine)
   797  	for ; result.NotDone(); err = result.NextWithContext(sdkCtx) {
   798  		if err != nil {
   799  			return nil, errors.Annotate(err, "listing disks")
   800  		}
   801  		vmCopy := result.Value()
   802  		all[instance.Id(to.String(vmCopy.Name))] = &vmCopy
   803  	}
   804  	results := make(map[instance.Id]*maybeVirtualMachine)
   805  	for _, id := range instanceIds {
   806  		result := &maybeVirtualMachine{vm: all[id]}
   807  		if result.vm == nil {
   808  			result.err = errors.NotFoundf("instance %v", id)
   809  		}
   810  		results[id] = result
   811  	}
   812  	return results, nil
   813  }
   814  
   815  // updateVirtualMachines updates virtual machines in the given map by iterating
   816  // through the list of instance IDs in order, and updating each corresponding
   817  // virtual machine at most once.
   818  func (v *azureVolumeSource) updateVirtualMachines(
   819  	ctx context.ProviderCallContext,
   820  	virtualMachines map[instance.Id]*maybeVirtualMachine, instanceIds []instance.Id,
   821  ) ([]error, error) {
   822  	results := make([]error, len(instanceIds))
   823  	vmsClient := compute.VirtualMachinesClient{v.env.compute}
   824  	for i, instanceId := range instanceIds {
   825  		vm, ok := virtualMachines[instanceId]
   826  		if !ok {
   827  			continue
   828  		}
   829  		if vm.err != nil {
   830  			results[i] = vm.err
   831  			continue
   832  		}
   833  		sdkCtx := stdcontext.Background()
   834  		future, err := vmsClient.CreateOrUpdate(
   835  			sdkCtx,
   836  			v.env.resourceGroup, to.String(vm.vm.Name), *vm.vm,
   837  		)
   838  		if err != nil {
   839  			if errorutils.MaybeInvalidateCredential(err, ctx) {
   840  				return nil, errors.Trace(err)
   841  			}
   842  			results[i] = err
   843  			vm.err = err
   844  			continue
   845  		}
   846  		err = future.WaitForCompletionRef(sdkCtx, vmsClient.Client)
   847  		if err != nil {
   848  			results[i] = err
   849  			vm.err = err
   850  			continue
   851  		}
   852  		_, err = future.Result(vmsClient)
   853  		if err != nil {
   854  			results[i] = err
   855  			vm.err = err
   856  			continue
   857  		}
   858  		// successfully updated, don't update again
   859  		delete(virtualMachines, instanceId)
   860  	}
   861  	return results, nil
   862  }
   863  
   864  func nextAvailableLUN(vm *compute.VirtualMachine) (int32, error) {
   865  	// Pick the smallest LUN not in use. We have to choose them in order,
   866  	// or the disks don't show up.
   867  	var inUse [32]bool
   868  	if vm.StorageProfile.DataDisks != nil {
   869  		for _, disk := range *vm.StorageProfile.DataDisks {
   870  			lun := to.Int32(disk.Lun)
   871  			if lun < 0 || lun > 31 {
   872  				logger.Debugf("ignore disk with invalid LUN: %+v", disk)
   873  				continue
   874  			}
   875  			inUse[lun] = true
   876  		}
   877  	}
   878  	for i, inUse := range inUse {
   879  		if !inUse {
   880  			return int32(i), nil
   881  		}
   882  	}
   883  	return -1, errors.New("all LUNs are in use")
   884  }
   885  
   886  // diskBusAddress returns the value to use in the BusAddress field of
   887  // VolumeAttachmentInfo for a disk with the specified LUN.
   888  func diskBusAddress(lun int32) string {
   889  	return fmt.Sprintf("scsi@5:0.0.%d", lun)
   890  }
   891  
   892  // mibToGib converts mebibytes to gibibytes.
   893  // AWS expects GiB, we work in MiB; round up
   894  // to nearest GiB.
   895  func mibToGib(m uint64) uint64 {
   896  	return (m + 1023) / 1024
   897  }
   898  
   899  // gibToMib converts gibibytes to mebibytes.
   900  func gibToMib(g uint64) uint64 {
   901  	return g * 1024
   902  }
   903  
   904  // dataDiskVhdRoot returns the URL to the blob container in which we store the
   905  // VHDs for data disks for the environment.
   906  func dataDiskVhdRoot(storageAccount *armstorage.Account) string {
   907  	return blobContainerURL(storageAccount, dataDiskVHDContainer)
   908  }
   909  
   910  // blobContainer returns the URL to the named blob container.
   911  func blobContainerURL(storageAccount *armstorage.Account, container string) string {
   912  	return fmt.Sprintf(
   913  		"%s%s/",
   914  		to.String(storageAccount.PrimaryEndpoints.Blob),
   915  		container,
   916  	)
   917  }
   918  
   919  // blobVolumeId returns the volume ID for a blob, and a boolean reporting
   920  // whether or not the blob's name matches the scheme we use.
   921  func blobVolumeId(blob internalazurestorage.Blob) (string, bool) {
   922  	blobName := blob.Name()
   923  	if !strings.HasSuffix(blobName, vhdExtension) {
   924  		return "", false
   925  	}
   926  	volumeId := blobName[:len(blobName)-len(vhdExtension)]
   927  	if _, err := names.ParseVolumeTag(volumeId); err != nil {
   928  		return "", false
   929  	}
   930  	return volumeId, true
   931  }
   932  
   933  // getStorageClient returns a new storage client, given an environ config
   934  // and a constructor.
   935  func getStorageClient(
   936  	newClient internalazurestorage.NewClientFunc,
   937  	storageEndpoint string,
   938  	storageAccount *armstorage.Account,
   939  	storageAccountKey *armstorage.AccountKey,
   940  ) (internalazurestorage.Client, error) {
   941  	storageAccountName := to.String(storageAccount.Name)
   942  	const useHTTPS = true
   943  	return newClient(
   944  		storageAccountName,
   945  		to.String(storageAccountKey.Value),
   946  		storageEndpoint,
   947  		azurestorage.DefaultAPIVersion,
   948  		useHTTPS,
   949  	)
   950  }
   951  
   952  // getStorageAccountKey returns the key for the storage account.
   953  func getStorageAccountKey(
   954  	client armstorage.AccountsClient,
   955  	resourceGroup, accountName string,
   956  ) (*armstorage.AccountKey, error) {
   957  	logger.Debugf("getting keys for storage account %q", accountName)
   958  	sdkCtx := stdcontext.Background()
   959  	listKeysResult, err := client.ListKeys(sdkCtx, resourceGroup, accountName)
   960  	if err != nil {
   961  		if isNotFoundResult(listKeysResult.Response) {
   962  			return nil, errors.NewNotFound(err, "storage account keys not found")
   963  		}
   964  		return nil, errors.Annotate(err, "listing storage account keys")
   965  	}
   966  	if listKeysResult.Keys == nil {
   967  		return nil, errors.NotFoundf("storage account keys")
   968  	}
   969  
   970  	// We need a storage key with full permissions.
   971  	var fullKey *armstorage.AccountKey
   972  	for _, key := range *listKeysResult.Keys {
   973  		logger.Debugf("storage account key: %#v", key)
   974  		// At least some of the time, Azure returns the permissions
   975  		// in title-case, which does not match the constant.
   976  		if strings.ToUpper(string(key.Permissions)) != strings.ToUpper(string(armstorage.Full)) {
   977  			continue
   978  		}
   979  		fullKey = &key
   980  		break
   981  	}
   982  	if fullKey == nil {
   983  		return nil, errors.NotFoundf(
   984  			"storage account key with %q permission",
   985  			armstorage.Full,
   986  		)
   987  	}
   988  	return fullKey, nil
   989  }
   990  
   991  // storageAccountTemplateResource returns a template resource definition
   992  // for creating a storage account.
   993  func storageAccountTemplateResource(
   994  	location string,
   995  	envTags map[string]string,
   996  	accountName, accountType string,
   997  ) armtemplates.Resource {
   998  	return armtemplates.Resource{
   999  		APIVersion: storageAPIVersion,
  1000  		Type:       "Microsoft.Storage/storageAccounts",
  1001  		Name:       accountName,
  1002  		Location:   location,
  1003  		Tags:       envTags,
  1004  		StorageSku: &armstorage.Sku{
  1005  			Name: armstorage.SkuName(accountType),
  1006  		},
  1007  	}
  1008  }