github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/azure/storage_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure_test
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  
    10  	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute"
    11  	armstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-07-01/storage"
    12  	azurestorage "github.com/Azure/azure-sdk-for-go/storage"
    13  	autorestazure "github.com/Azure/go-autorest/autorest/azure"
    14  	"github.com/Azure/go-autorest/autorest/mocks"
    15  	"github.com/Azure/go-autorest/autorest/to"
    16  	"github.com/juju/errors"
    17  	jc "github.com/juju/testing/checkers"
    18  	gc "gopkg.in/check.v1"
    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"
    24  	internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage"
    25  	"github.com/juju/juju/provider/azure/internal/azuretesting"
    26  	"github.com/juju/juju/storage"
    27  	"github.com/juju/juju/testing"
    28  )
    29  
    30  type storageSuite struct {
    31  	testing.BaseSuite
    32  
    33  	datavhdsContainer azuretesting.MockStorageContainer
    34  	storageClient     azuretesting.MockStorageClient
    35  	provider          storage.Provider
    36  	requests          []*http.Request
    37  	sender            azuretesting.Senders
    38  
    39  	cloudCallCtx      *context.CloudCallContext
    40  	invalidCredential bool
    41  }
    42  
    43  var _ = gc.Suite(&storageSuite{})
    44  
    45  func (s *storageSuite) SetUpTest(c *gc.C) {
    46  	s.BaseSuite.SetUpTest(c)
    47  	s.datavhdsContainer = azuretesting.MockStorageContainer{}
    48  	s.storageClient = azuretesting.MockStorageClient{
    49  		Containers: map[string]internalazurestorage.Container{
    50  			"datavhds": &s.datavhdsContainer,
    51  		},
    52  	}
    53  	s.requests = nil
    54  	envProvider := newProvider(c, azure.ProviderConfig{
    55  		Sender:                     &s.sender,
    56  		NewStorageClient:           s.storageClient.NewClient,
    57  		RequestInspector:           azuretesting.RequestRecorder(&s.requests),
    58  		RandomWindowsAdminPassword: func() string { return "sorandom" },
    59  	})
    60  	s.sender = nil
    61  
    62  	var err error
    63  	env := openEnviron(c, envProvider, &s.sender)
    64  	azure.SetRetries(env)
    65  	s.provider, err = env.StorageProvider("azure")
    66  	c.Assert(err, jc.ErrorIsNil)
    67  	s.cloudCallCtx = &context.CloudCallContext{
    68  		InvalidateCredentialFunc: func(string) error {
    69  			s.invalidCredential = true
    70  			return nil
    71  		},
    72  	}
    73  }
    74  
    75  func (s *storageSuite) TearDownTest(c *gc.C) {
    76  	s.invalidCredential = false
    77  	s.BaseSuite.TearDownTest(c)
    78  }
    79  
    80  func (s *storageSuite) volumeSource(c *gc.C, legacy bool, attrs ...testing.Attrs) storage.VolumeSource {
    81  	storageConfig, err := storage.NewConfig("azure", "azure", nil)
    82  	c.Assert(err, jc.ErrorIsNil)
    83  
    84  	s.sender = azuretesting.Senders{}
    85  	if legacy {
    86  		s.sender = append(s.sender, s.accountSender(), s.accountKeysSender())
    87  	} else {
    88  		s.sender = append(s.sender, s.accountNotFoundSender())
    89  	}
    90  	volumeSource, err := s.provider.VolumeSource(storageConfig)
    91  	c.Assert(err, jc.ErrorIsNil)
    92  
    93  	// Force an explicit refresh of the access token, so it isn't done
    94  	// implicitly during the tests.
    95  	s.sender = azuretesting.Senders{tokenRefreshSender()}
    96  	err = azure.ForceVolumeSourceTokenRefresh(volumeSource)
    97  	c.Assert(err, jc.ErrorIsNil)
    98  	return volumeSource
    99  }
   100  
   101  func (s *storageSuite) accountNotFoundSender() *mocks.Sender {
   102  	sender := mocks.NewSender()
   103  	sender.AppendResponse(mocks.NewResponseWithStatus(
   104  		"storage account not found", http.StatusNotFound,
   105  	))
   106  	return sender
   107  }
   108  
   109  func (s *storageSuite) accountSender() *azuretesting.MockSender {
   110  	envTags := map[string]*string{
   111  		"juju-model-uuid": to.StringPtr(testing.ModelTag.Id()),
   112  	}
   113  	account := armstorage.Account{
   114  		Name: to.StringPtr(storageAccountName),
   115  		Type: to.StringPtr("Standard_LRS"),
   116  		Tags: envTags,
   117  		AccountProperties: &armstorage.AccountProperties{
   118  			PrimaryEndpoints: &armstorage.Endpoints{
   119  				Blob: to.StringPtr(fmt.Sprintf("https://%s.blob.storage.azurestack.local/", storageAccountName)),
   120  			},
   121  		},
   122  	}
   123  	accountSender := azuretesting.NewSenderWithValue(account)
   124  	accountSender.PathPattern = ".*/storageAccounts/" + storageAccountName + ".*"
   125  	return accountSender
   126  }
   127  
   128  func (s *storageSuite) accountKeysSender() *azuretesting.MockSender {
   129  	keys := []armstorage.AccountKey{{
   130  		KeyName:     to.StringPtr(fakeStorageAccountKey + "-name"),
   131  		Value:       to.StringPtr(fakeStorageAccountKey),
   132  		Permissions: armstorage.Full,
   133  	}, {
   134  		KeyName:     to.StringPtr("key2-name"),
   135  		Value:       to.StringPtr("key2"),
   136  		Permissions: armstorage.Full,
   137  	}}
   138  	result := armstorage.AccountListKeysResult{Keys: &keys}
   139  	keysSender := azuretesting.NewSenderWithValue(&result)
   140  	keysSender.PathPattern = ".*/storageAccounts/.*/listKeys"
   141  	return keysSender
   142  }
   143  
   144  func (s *storageSuite) TestVolumeSource(c *gc.C) {
   145  	vs := s.volumeSource(c, false)
   146  	c.Assert(vs, gc.NotNil)
   147  }
   148  
   149  func (s *storageSuite) TestFilesystemSource(c *gc.C) {
   150  	storageConfig, err := storage.NewConfig("azure", "azure", nil)
   151  	c.Assert(err, jc.ErrorIsNil)
   152  
   153  	_, err = s.provider.FilesystemSource(storageConfig)
   154  	c.Assert(err, gc.ErrorMatches, "filesystems not supported")
   155  	c.Assert(err, jc.Satisfies, errors.IsNotSupported)
   156  }
   157  
   158  func (s *storageSuite) TestSupports(c *gc.C) {
   159  	c.Assert(s.provider.Supports(storage.StorageKindBlock), jc.IsTrue)
   160  	c.Assert(s.provider.Supports(storage.StorageKindFilesystem), jc.IsFalse)
   161  }
   162  
   163  func (s *storageSuite) TestDynamic(c *gc.C) {
   164  	c.Assert(s.provider.Dynamic(), jc.IsTrue)
   165  }
   166  
   167  func (s *storageSuite) TestScope(c *gc.C) {
   168  	c.Assert(s.provider.Scope(), gc.Equals, storage.ScopeEnviron)
   169  }
   170  
   171  func (s *storageSuite) TestCreateVolumes(c *gc.C) {
   172  	makeVolumeParams := func(volume, machine string, size uint64) storage.VolumeParams {
   173  		return storage.VolumeParams{
   174  			Tag:          names.NewVolumeTag(volume),
   175  			Size:         size,
   176  			Provider:     "azure",
   177  			ResourceTags: map[string]string{"foo": "bar"},
   178  			Attachment: &storage.VolumeAttachmentParams{
   179  				AttachmentParams: storage.AttachmentParams{
   180  					Provider:   "azure",
   181  					Machine:    names.NewMachineTag(machine),
   182  					InstanceId: instance.Id("machine-" + machine),
   183  				},
   184  				Volume: names.NewVolumeTag(volume),
   185  			},
   186  		}
   187  	}
   188  	params := []storage.VolumeParams{
   189  		makeVolumeParams("0", "0", 1),
   190  		makeVolumeParams("1", "1", 1025),
   191  		makeVolumeParams("2", "0", 1024),
   192  	}
   193  
   194  	makeSender := func(name string, sizeGB int32) *azuretesting.MockSender {
   195  		sender := azuretesting.NewSenderWithValue(&compute.Disk{
   196  			Name: to.StringPtr(name),
   197  			DiskProperties: &compute.DiskProperties{
   198  				DiskSizeGB: to.Int32Ptr(sizeGB),
   199  			},
   200  		})
   201  		sender.PathPattern = `.*/Microsoft\.Compute/disks/` + name
   202  		return sender
   203  	}
   204  
   205  	volumeSource := s.volumeSource(c, false)
   206  	s.requests = nil
   207  	s.sender = azuretesting.Senders{
   208  		makeSender("volume-0", 32),
   209  		makeSender("volume-0", 32), // future.Results call
   210  		makeSender("volume-1", 2),
   211  		makeSender("volume-1", 2), // future.Results call
   212  		makeSender("volume-2", 1),
   213  		makeSender("volume-2", 1), // future.Results call
   214  	}
   215  
   216  	results, err := volumeSource.CreateVolumes(s.cloudCallCtx, params)
   217  	c.Assert(err, jc.ErrorIsNil)
   218  	c.Assert(results, gc.HasLen, len(params))
   219  	c.Check(results[0].Error, jc.ErrorIsNil)
   220  	c.Check(results[1].Error, jc.ErrorIsNil)
   221  	c.Check(results[2].Error, jc.ErrorIsNil)
   222  
   223  	// Attachments are deferred.
   224  	c.Check(results[0].VolumeAttachment, gc.IsNil)
   225  	c.Check(results[1].VolumeAttachment, gc.IsNil)
   226  	c.Check(results[2].VolumeAttachment, gc.IsNil)
   227  
   228  	makeVolume := func(id string, size uint64) *storage.Volume {
   229  		return &storage.Volume{
   230  			Tag: names.NewVolumeTag(id),
   231  			VolumeInfo: storage.VolumeInfo{
   232  				Size:       size,
   233  				VolumeId:   "volume-" + id,
   234  				Persistent: true,
   235  			},
   236  		}
   237  	}
   238  	c.Check(results[0].Volume, jc.DeepEquals, makeVolume("0", 32*1024))
   239  	c.Check(results[1].Volume, jc.DeepEquals, makeVolume("1", 2*1024))
   240  	c.Check(results[2].Volume, jc.DeepEquals, makeVolume("2", 1*1024))
   241  
   242  	// Validate HTTP request bodies.
   243  	c.Assert(s.requests, gc.HasLen, 6)
   244  	c.Assert(s.requests[0].Method, gc.Equals, "PUT") // create volume-0
   245  	c.Assert(s.requests[1].Method, gc.Equals, "GET") // create volume-0 - future.Results call
   246  	c.Assert(s.requests[2].Method, gc.Equals, "PUT") // create volume-1
   247  	c.Assert(s.requests[3].Method, gc.Equals, "GET") // create volume-1 - future.Results call
   248  	c.Assert(s.requests[4].Method, gc.Equals, "PUT") // create volume-2
   249  	c.Assert(s.requests[5].Method, gc.Equals, "GET") // create volume-2 - future.Results call
   250  
   251  	makeDisk := func(name string, size int32) *compute.Disk {
   252  		tags := map[string]*string{
   253  			"foo": to.StringPtr("bar"),
   254  		}
   255  		return &compute.Disk{
   256  			Name:     to.StringPtr(name),
   257  			Location: to.StringPtr("westus"),
   258  			Tags:     tags,
   259  			Sku: &compute.DiskSku{
   260  				Name: compute.DiskStorageAccountTypes("Standard_LRS"),
   261  			},
   262  			DiskProperties: &compute.DiskProperties{
   263  				DiskSizeGB: to.Int32Ptr(size),
   264  				CreationData: &compute.CreationData{
   265  					CreateOption: compute.Empty,
   266  				},
   267  			},
   268  		}
   269  	}
   270  	// Only check the PUT requests.
   271  	assertRequestBody(c, s.requests[0], makeDisk("volume-0", 1))
   272  	assertRequestBody(c, s.requests[2], makeDisk("volume-1", 2))
   273  	assertRequestBody(c, s.requests[4], makeDisk("volume-2", 1))
   274  }
   275  
   276  func (s *storageSuite) createSenderWithUnauthorisedStatusCode(c *gc.C) {
   277  	mockSender := mocks.NewSender()
   278  	mockSender.AppendResponse(mocks.NewResponseWithStatus("401 Unauthorized", http.StatusUnauthorized))
   279  	s.sender = azuretesting.Senders{
   280  		mockSender,
   281  	}
   282  }
   283  
   284  func (s *storageSuite) TestCreateVolumesWithInvalidCredential(c *gc.C) {
   285  	makeVolumeParams := func(volume, machine string, size uint64) storage.VolumeParams {
   286  		return storage.VolumeParams{
   287  			Tag:          names.NewVolumeTag(volume),
   288  			Size:         size,
   289  			Provider:     "azure",
   290  			ResourceTags: map[string]string{"foo": "bar"},
   291  			Attachment: &storage.VolumeAttachmentParams{
   292  				AttachmentParams: storage.AttachmentParams{
   293  					Provider:   "azure",
   294  					Machine:    names.NewMachineTag(machine),
   295  					InstanceId: instance.Id("machine-" + machine),
   296  				},
   297  				Volume: names.NewVolumeTag(volume),
   298  			},
   299  		}
   300  	}
   301  	params := []storage.VolumeParams{
   302  		makeVolumeParams("0", "0", 1),
   303  		makeVolumeParams("1", "1", 1025),
   304  		makeVolumeParams("2", "0", 1024),
   305  	}
   306  
   307  	volumeSource := s.volumeSource(c, false)
   308  	s.requests = nil
   309  	s.createSenderWithUnauthorisedStatusCode(c)
   310  
   311  	c.Assert(s.invalidCredential, jc.IsFalse)
   312  	results, err := volumeSource.CreateVolumes(s.cloudCallCtx, params)
   313  	c.Assert(err, jc.ErrorIsNil)
   314  	c.Assert(results, gc.HasLen, len(params))
   315  	c.Check(results[0].Error, gc.NotNil)
   316  	c.Check(results[1].Error, gc.NotNil)
   317  	c.Check(results[2].Error, gc.NotNil)
   318  
   319  	// Attachments are deferred.
   320  	c.Check(results[0].VolumeAttachment, gc.IsNil)
   321  	c.Check(results[1].VolumeAttachment, gc.IsNil)
   322  	c.Check(results[2].VolumeAttachment, gc.IsNil)
   323  	c.Assert(s.invalidCredential, jc.IsTrue)
   324  
   325  	// Validate HTTP request bodies.
   326  	// account for the retry attemptd for volumes 1,2
   327  	c.Assert(s.requests, gc.HasLen, 5)
   328  	c.Assert(s.requests[0].Method, gc.Equals, "PUT") // create volume-0
   329  	c.Assert(s.requests[1].Method, gc.Equals, "PUT") // create volume-1
   330  	c.Assert(s.requests[3].Method, gc.Equals, "PUT") // create volume-2
   331  
   332  	makeDisk := func(name string, size int32) *compute.Disk {
   333  		tags := map[string]*string{
   334  			"foo": to.StringPtr("bar"),
   335  		}
   336  		return &compute.Disk{
   337  			Name:     to.StringPtr(name),
   338  			Location: to.StringPtr("westus"),
   339  			Tags:     tags,
   340  			DiskProperties: &compute.DiskProperties{
   341  				DiskSizeGB: to.Int32Ptr(size),
   342  				CreationData: &compute.CreationData{
   343  					CreateOption: compute.Empty,
   344  				},
   345  			},
   346  			Sku: &compute.DiskSku{
   347  				Name: compute.DiskStorageAccountTypes("Standard_LRS"),
   348  			},
   349  		}
   350  	}
   351  	// account for the retry attemptd for volumes 1,2
   352  	assertRequestBody(c, s.requests[0], makeDisk("volume-0", 1))
   353  	assertRequestBody(c, s.requests[1], makeDisk("volume-1", 2))
   354  	assertRequestBody(c, s.requests[3], makeDisk("volume-2", 1))
   355  }
   356  
   357  func (s *storageSuite) TestCreateVolumesLegacy(c *gc.C) {
   358  	// machine-1 has a single data disk with LUN 0.
   359  	machine1DataDisks := []compute.DataDisk{{Lun: to.Int32Ptr(0)}}
   360  	// machine-2 has 32 data disks; no LUNs free.
   361  	machine2DataDisks := make([]compute.DataDisk, 32)
   362  	for i := range machine2DataDisks {
   363  		machine2DataDisks[i].Lun = to.Int32Ptr(int32(i))
   364  	}
   365  
   366  	// volume-0 and volume-2 are attached to machine-0
   367  	// volume-1 is attached to machine-1
   368  	// volume-3 is attached to machine-42, but machine-42 is missing
   369  	// volume-42 is attached to machine-2, but machine-2 has no free LUNs
   370  	makeVolumeParams := func(volume, machine string, size uint64) storage.VolumeParams {
   371  		return storage.VolumeParams{
   372  			Tag:      names.NewVolumeTag(volume),
   373  			Size:     size,
   374  			Provider: "azure",
   375  			Attachment: &storage.VolumeAttachmentParams{
   376  				AttachmentParams: storage.AttachmentParams{
   377  					Provider:   "azure",
   378  					Machine:    names.NewMachineTag(machine),
   379  					InstanceId: instance.Id("machine-" + machine),
   380  				},
   381  				Volume: names.NewVolumeTag(volume),
   382  			},
   383  		}
   384  	}
   385  	params := []storage.VolumeParams{
   386  		makeVolumeParams("0", "0", 1),
   387  		makeVolumeParams("1", "1", 1025),
   388  		makeVolumeParams("2", "0", 1024),
   389  		makeVolumeParams("3", "42", 40),
   390  		makeVolumeParams("42", "2", 50),
   391  	}
   392  
   393  	virtualMachines := []compute.VirtualMachine{{
   394  		Name: to.StringPtr("machine-0"),
   395  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   396  			StorageProfile: &compute.StorageProfile{},
   397  		},
   398  	}, {
   399  		Name: to.StringPtr("machine-1"),
   400  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   401  			StorageProfile: &compute.StorageProfile{DataDisks: &machine1DataDisks},
   402  		},
   403  	}, {
   404  		Name: to.StringPtr("machine-2"),
   405  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   406  			StorageProfile: &compute.StorageProfile{DataDisks: &machine2DataDisks},
   407  		},
   408  	}}
   409  
   410  	// There should be a one API calls to list VMs, and one update per modified instance.
   411  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
   412  		Value: &virtualMachines,
   413  	})
   414  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
   415  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   416  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
   417  	updateVirtualMachine1Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   418  	updateVirtualMachine1Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-1`
   419  
   420  	volumeSource := s.volumeSource(c, true)
   421  	s.requests = nil
   422  	s.sender = azuretesting.Senders{
   423  		virtualMachinesSender,
   424  		updateVirtualMachine0Sender,
   425  		updateVirtualMachine0Sender,
   426  		updateVirtualMachine1Sender,
   427  		updateVirtualMachine1Sender,
   428  	}
   429  
   430  	results, err := volumeSource.CreateVolumes(s.cloudCallCtx, params)
   431  	c.Assert(err, jc.ErrorIsNil)
   432  	c.Assert(results, gc.HasLen, len(params))
   433  
   434  	c.Check(results[0].Error, jc.ErrorIsNil)
   435  	c.Check(results[1].Error, jc.ErrorIsNil)
   436  	c.Check(results[2].Error, jc.ErrorIsNil)
   437  	c.Check(results[3].Error, gc.ErrorMatches, "instance machine-42 not found")
   438  	c.Check(results[4].Error, gc.ErrorMatches, "choosing LUN: all LUNs are in use")
   439  
   440  	makeVolume := func(id string, size uint64) *storage.Volume {
   441  		return &storage.Volume{
   442  			Tag: names.NewVolumeTag(id),
   443  			VolumeInfo: storage.VolumeInfo{
   444  				Size:       size,
   445  				VolumeId:   "volume-" + id,
   446  				Persistent: true,
   447  			},
   448  		}
   449  	}
   450  	c.Check(results[0].Volume, jc.DeepEquals, makeVolume("0", 1024))
   451  	c.Check(results[1].Volume, jc.DeepEquals, makeVolume("1", 2048))
   452  	c.Check(results[2].Volume, jc.DeepEquals, makeVolume("2", 1024))
   453  
   454  	// Attachments created at the same time.
   455  	makeVolumeAttachment := func(volumeId, machineId string, lun int) *storage.VolumeAttachment {
   456  		return &storage.VolumeAttachment{
   457  			Volume:  names.NewVolumeTag(volumeId),
   458  			Machine: names.NewMachineTag(machineId),
   459  			VolumeAttachmentInfo: storage.VolumeAttachmentInfo{
   460  				BusAddress: fmt.Sprintf("scsi@5:0.0.%d", lun),
   461  			},
   462  		}
   463  	}
   464  	c.Check(results[0].VolumeAttachment, jc.DeepEquals, makeVolumeAttachment("0", "0", 0))
   465  	c.Check(results[1].VolumeAttachment, jc.DeepEquals, makeVolumeAttachment("1", "1", 1))
   466  	c.Check(results[2].VolumeAttachment, jc.DeepEquals, makeVolumeAttachment("2", "0", 1))
   467  
   468  	// Validate HTTP request bodies.
   469  	c.Assert(s.requests, gc.HasLen, 5)
   470  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list virtual machines
   471  	c.Assert(s.requests[1].Method, gc.Equals, "PUT") // update machine-0
   472  	c.Assert(s.requests[2].Method, gc.Equals, "GET") // update machine-0 - future.Result call
   473  	c.Assert(s.requests[3].Method, gc.Equals, "PUT") // update machine-1
   474  	c.Assert(s.requests[4].Method, gc.Equals, "GET") // update machine-1 - future.Result call
   475  
   476  	machine0DataDisks := []compute.DataDisk{{
   477  		Lun:        to.Int32Ptr(0),
   478  		DiskSizeGB: to.Int32Ptr(1),
   479  		Name:       to.StringPtr("volume-0"),
   480  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   481  			"https://%s.blob.storage.azurestack.local/datavhds/volume-0.vhd",
   482  			storageAccountName,
   483  		))},
   484  		Caching:      compute.CachingTypesReadWrite,
   485  		CreateOption: compute.DiskCreateOptionTypesEmpty,
   486  	}, {
   487  		Lun:        to.Int32Ptr(1),
   488  		DiskSizeGB: to.Int32Ptr(1),
   489  		Name:       to.StringPtr("volume-2"),
   490  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   491  			"https://%s.blob.storage.azurestack.local/datavhds/volume-2.vhd",
   492  			storageAccountName,
   493  		))},
   494  		Caching:      compute.CachingTypesReadWrite,
   495  		CreateOption: compute.DiskCreateOptionTypesEmpty,
   496  	}}
   497  	virtualMachines[0].StorageProfile.DataDisks = &machine0DataDisks
   498  	assertRequestBody(c, s.requests[1], &virtualMachines[0])
   499  
   500  	machine1DataDisks = append(machine1DataDisks, compute.DataDisk{
   501  		Lun:        to.Int32Ptr(1),
   502  		DiskSizeGB: to.Int32Ptr(2),
   503  		Name:       to.StringPtr("volume-1"),
   504  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   505  			"https://%s.blob.storage.azurestack.local/datavhds/volume-1.vhd",
   506  			storageAccountName,
   507  		))},
   508  		Caching:      compute.CachingTypesReadWrite,
   509  		CreateOption: compute.DiskCreateOptionTypesEmpty,
   510  	})
   511  	assertRequestBody(c, s.requests[3], &virtualMachines[1])
   512  }
   513  
   514  func (s *storageSuite) TestListVolumes(c *gc.C) {
   515  	volumeSource := s.volumeSource(c, false)
   516  	disks := []compute.Disk{{
   517  		Name: to.StringPtr("volume-0"),
   518  	}, {
   519  		Name: to.StringPtr("machine-0"),
   520  	}, {
   521  		Name: to.StringPtr("volume-1"),
   522  	}}
   523  	volumeSender := azuretesting.NewSenderWithValue(&compute.DiskList{
   524  		Value: &disks,
   525  	})
   526  	volumeSender.PathPattern = `.*/Microsoft\.Compute/disks`
   527  	s.sender = azuretesting.Senders{volumeSender}
   528  
   529  	volumeIds, err := volumeSource.ListVolumes(s.cloudCallCtx)
   530  	c.Assert(err, jc.ErrorIsNil)
   531  	c.Assert(volumeIds, jc.SameContents, []string{"volume-0", "volume-1"})
   532  }
   533  
   534  func (s *storageSuite) TestListVolumesWithInvalidCredential(c *gc.C) {
   535  	volumeSource := s.volumeSource(c, false)
   536  	s.createSenderWithUnauthorisedStatusCode(c)
   537  
   538  	c.Assert(s.invalidCredential, jc.IsFalse)
   539  	_, err := volumeSource.ListVolumes(s.cloudCallCtx)
   540  	c.Assert(err, gc.NotNil)
   541  	c.Assert(s.invalidCredential, jc.IsTrue)
   542  }
   543  
   544  func (s *storageSuite) TestListVolumesLegacy(c *gc.C) {
   545  	blob0 := &azuretesting.MockStorageBlob{
   546  		Name_: "volume-0.vhd",
   547  		Properties_: azurestorage.BlobProperties{
   548  			ContentLength: 1024 * 1024 * 1024 * 1024, // 1TiB
   549  		},
   550  	}
   551  	blob1 := &azuretesting.MockStorageBlob{
   552  		Name_: "volume-1.vhd",
   553  		Properties_: azurestorage.BlobProperties{
   554  			ContentLength: 1024 * 1024, // 1MiB
   555  		},
   556  	}
   557  	junkBlob := &azuretesting.MockStorageBlob{
   558  		Name_: "junk.vhd",
   559  	}
   560  	volumeBlob := &azuretesting.MockStorageBlob{
   561  		Name_: "volume",
   562  	}
   563  	s.datavhdsContainer.Blobs_ = []internalazurestorage.Blob{blob1, blob0, junkBlob, volumeBlob}
   564  
   565  	volumeSource := s.volumeSource(c, true)
   566  	volumeIds, err := volumeSource.ListVolumes(s.cloudCallCtx)
   567  	c.Assert(err, jc.ErrorIsNil)
   568  	s.storageClient.CheckCallNames(c, "NewClient", "GetContainerReference")
   569  	s.storageClient.CheckCall(
   570  		c, 0, "NewClient", storageAccountName, fakeStorageAccountKey,
   571  		"storage.azurestack.local", azurestorage.DefaultAPIVersion, true,
   572  	)
   573  	s.storageClient.CheckCall(c, 1, "GetContainerReference", "datavhds")
   574  	s.datavhdsContainer.CheckCallNames(c, "Blobs")
   575  	c.Assert(volumeIds, jc.DeepEquals, []string{"volume-1", "volume-0"})
   576  }
   577  
   578  func (s *storageSuite) TestListVolumesErrors(c *gc.C) {
   579  	volumeSource := s.volumeSource(c, false)
   580  	sender := mocks.NewSender()
   581  	sender.SetAndRepeatError(errors.New("no disks for you"), -1)
   582  	s.sender = azuretesting.Senders{
   583  		sender,
   584  		sender, // for the retry attempt
   585  	}
   586  	_, err := volumeSource.ListVolumes(s.cloudCallCtx)
   587  	c.Assert(err, gc.ErrorMatches, "listing disks: .*: no disks for you")
   588  }
   589  
   590  func (s *storageSuite) TestListVolumesErrorsLegacy(c *gc.C) {
   591  	volumeSource := s.volumeSource(c, true)
   592  	s.datavhdsContainer.SetErrors(errors.New("no blobs for you"))
   593  	_, err := volumeSource.ListVolumes(s.cloudCallCtx)
   594  	c.Assert(err, gc.ErrorMatches, "listing volumes: listing blobs: no blobs for you")
   595  }
   596  
   597  func (s *storageSuite) TestDescribeVolumes(c *gc.C) {
   598  	volumeSource := s.volumeSource(c, false)
   599  	volumeSender := azuretesting.NewSenderWithValue(&compute.Disk{
   600  		DiskProperties: &compute.DiskProperties{
   601  			DiskSizeGB: to.Int32Ptr(1024),
   602  		},
   603  	})
   604  	volumeSender.PathPattern = `.*/Microsoft\.Compute/disks/volume-0`
   605  	s.sender = azuretesting.Senders{volumeSender}
   606  
   607  	results, err := volumeSource.DescribeVolumes(s.cloudCallCtx, []string{"volume-0"})
   608  	c.Assert(err, jc.ErrorIsNil)
   609  	c.Assert(results, jc.DeepEquals, []storage.DescribeVolumesResult{{
   610  		VolumeInfo: &storage.VolumeInfo{
   611  			VolumeId:   "volume-0",
   612  			Size:       1024 * 1024,
   613  			Persistent: true,
   614  		},
   615  	}})
   616  }
   617  
   618  func (s *storageSuite) TestDescribeVolumesWithInvalidCredential(c *gc.C) {
   619  	volumeSource := s.volumeSource(c, false)
   620  	s.createSenderWithUnauthorisedStatusCode(c)
   621  
   622  	c.Assert(s.invalidCredential, jc.IsFalse)
   623  	_, err := volumeSource.DescribeVolumes(s.cloudCallCtx, []string{"volume-0"})
   624  	results, err := volumeSource.DescribeVolumes(s.cloudCallCtx, []string{"volume-0"})
   625  	c.Assert(err, jc.ErrorIsNil)
   626  	c.Assert(results[0].Error, gc.NotNil)
   627  	c.Assert(s.invalidCredential, jc.IsTrue)
   628  }
   629  
   630  func (s *storageSuite) TestDescribeVolumesNotFound(c *gc.C) {
   631  	volumeSource := s.volumeSource(c, false)
   632  	volumeSender := mocks.NewSender()
   633  	response := mocks.NewResponseWithBodyAndStatus(
   634  		mocks.NewBody("{}"),
   635  		http.StatusNotFound,
   636  		"disk not found",
   637  	)
   638  	volumeSender.AppendResponse(response)
   639  	s.sender = azuretesting.Senders{volumeSender}
   640  	results, err := volumeSource.DescribeVolumes(s.cloudCallCtx, []string{"volume-42"})
   641  	c.Assert(err, jc.ErrorIsNil)
   642  	c.Assert(results, gc.HasLen, 1)
   643  	c.Assert(results[0].Error, jc.Satisfies, errors.IsNotFound)
   644  	c.Assert(results[0].Error, gc.ErrorMatches, `disk volume-42 not found`)
   645  }
   646  
   647  func (s *storageSuite) TestDescribeVolumesLegacy(c *gc.C) {
   648  	blob0 := &azuretesting.MockStorageBlob{
   649  		Name_: "volume-0.vhd",
   650  		Properties_: azurestorage.BlobProperties{
   651  			ContentLength: 1024 * 1024 * 1024 * 1024, // 1TiB
   652  		},
   653  	}
   654  	blob1 := &azuretesting.MockStorageBlob{
   655  		Name_: "volume-1.vhd",
   656  		Properties_: azurestorage.BlobProperties{
   657  			ContentLength: 1024 * 1024, // 1MiB
   658  		},
   659  	}
   660  	s.datavhdsContainer.Blobs_ = []internalazurestorage.Blob{blob1, blob0}
   661  
   662  	volumeSource := s.volumeSource(c, true)
   663  	results, err := volumeSource.DescribeVolumes(s.cloudCallCtx, []string{"volume-0", "volume-1", "volume-0", "volume-42"})
   664  	c.Assert(err, jc.ErrorIsNil)
   665  	s.storageClient.CheckCallNames(c, "NewClient", "GetContainerReference")
   666  	s.storageClient.CheckCall(
   667  		c, 0, "NewClient", storageAccountName, fakeStorageAccountKey,
   668  		"storage.azurestack.local", azurestorage.DefaultAPIVersion, true,
   669  	)
   670  	c.Assert(results, gc.HasLen, 4)
   671  	c.Assert(results[:3], jc.DeepEquals, []storage.DescribeVolumesResult{{
   672  		VolumeInfo: &storage.VolumeInfo{
   673  			VolumeId:   "volume-0",
   674  			Size:       1024 * 1024,
   675  			Persistent: true,
   676  		},
   677  	}, {
   678  		VolumeInfo: &storage.VolumeInfo{
   679  			VolumeId:   "volume-1",
   680  			Size:       1,
   681  			Persistent: true,
   682  		},
   683  	}, {
   684  		VolumeInfo: &storage.VolumeInfo{
   685  			VolumeId:   "volume-0",
   686  			Size:       1024 * 1024,
   687  			Persistent: true,
   688  		},
   689  	}})
   690  	c.Assert(results[3].Error, gc.ErrorMatches, "volume-42 not found")
   691  }
   692  
   693  func (s *storageSuite) TestDestroyVolumes(c *gc.C) {
   694  	volumeSource := s.volumeSource(c, false)
   695  
   696  	volume0Sender := azuretesting.NewSenderWithValue(&autorestazure.ServiceError{})
   697  	volume0Sender.PathPattern = `.*/Microsoft\.Compute/disks/volume-0`
   698  	s.sender = azuretesting.Senders{volume0Sender}
   699  
   700  	results, err := volumeSource.DestroyVolumes(s.cloudCallCtx, []string{"volume-0"})
   701  	c.Assert(err, jc.ErrorIsNil)
   702  	c.Assert(results, gc.HasLen, 1)
   703  	c.Assert(results[0], jc.ErrorIsNil)
   704  }
   705  
   706  func (s *storageSuite) TestDestroyVolumesWithInvalidCredential(c *gc.C) {
   707  	volumeSource := s.volumeSource(c, false)
   708  
   709  	s.createSenderWithUnauthorisedStatusCode(c)
   710  	c.Assert(s.invalidCredential, jc.IsFalse)
   711  	results, err := volumeSource.DestroyVolumes(s.cloudCallCtx, []string{"volume-0"})
   712  	c.Assert(err, jc.ErrorIsNil)
   713  	c.Assert(results, gc.HasLen, 1)
   714  	c.Assert(results[0], gc.NotNil)
   715  	c.Assert(s.invalidCredential, jc.IsTrue)
   716  }
   717  
   718  func (s *storageSuite) TestDestroyVolumesNotFound(c *gc.C) {
   719  	volumeSource := s.volumeSource(c, false)
   720  
   721  	volume42Sender := mocks.NewSender()
   722  	volume42Sender.AppendResponse(mocks.NewResponseWithStatus(
   723  		"disk not found", http.StatusNotFound,
   724  	))
   725  	s.sender = azuretesting.Senders{volume42Sender}
   726  
   727  	results, err := volumeSource.DestroyVolumes(s.cloudCallCtx, []string{"volume-42"})
   728  	c.Assert(err, jc.ErrorIsNil)
   729  	c.Assert(results, gc.HasLen, 1)
   730  	c.Assert(results[0], jc.ErrorIsNil)
   731  }
   732  
   733  func (s *storageSuite) TestDestroyVolumesLegacy(c *gc.C) {
   734  	blob0 := &azuretesting.MockStorageBlob{
   735  		Name_: "volume-0.vhd",
   736  	}
   737  	blob1 := &azuretesting.MockStorageBlob{
   738  		Name_: "volume-42.vhd",
   739  	}
   740  	s.datavhdsContainer.Blobs_ = []internalazurestorage.Blob{blob0, blob1}
   741  
   742  	volumeSource := s.volumeSource(c, true)
   743  	results, err := volumeSource.DestroyVolumes(s.cloudCallCtx, []string{"volume-0", "volume-42"})
   744  	c.Assert(err, jc.ErrorIsNil)
   745  	c.Assert(results, gc.HasLen, 2)
   746  	c.Assert(results[0], jc.ErrorIsNil)
   747  	c.Assert(results[1], jc.ErrorIsNil)
   748  	s.storageClient.CheckCallNames(c, "NewClient", "GetContainerReference")
   749  	s.storageClient.CheckCall(c, 1, "GetContainerReference", "datavhds")
   750  	s.datavhdsContainer.CheckCallNames(c, "Blob", "Blob")
   751  	blob0.CheckCallNames(c, "DeleteIfExists")
   752  	blob1.CheckCallNames(c, "DeleteIfExists")
   753  }
   754  
   755  func (s *storageSuite) TestAttachVolumes(c *gc.C) {
   756  	s.testAttachVolumes(c, false)
   757  }
   758  
   759  func (s *storageSuite) TestAttachVolumesLegacy(c *gc.C) {
   760  	s.testAttachVolumes(c, true)
   761  }
   762  
   763  func (s *storageSuite) testAttachVolumes(c *gc.C, legacy bool) {
   764  	// machine-1 has a single data disk with LUN 0.
   765  	machine1DataDisks := []compute.DataDisk{{
   766  		Lun:  to.Int32Ptr(0),
   767  		Name: to.StringPtr("volume-1"),
   768  		Vhd: &compute.VirtualHardDisk{
   769  			URI: to.StringPtr(fmt.Sprintf(
   770  				"https://%s.blob.storage.azurestack.local/datavhds/volume-1.vhd",
   771  				storageAccountName,
   772  			)),
   773  		},
   774  	}}
   775  	// machine-2 has 32 data disks; no LUNs free.
   776  	machine2DataDisks := make([]compute.DataDisk, 32)
   777  	for i := range machine2DataDisks {
   778  		machine2DataDisks[i].Lun = to.Int32Ptr(int32(i))
   779  		machine2DataDisks[i].Name = to.StringPtr(fmt.Sprintf("volume-%d", i))
   780  		machine2DataDisks[i].Vhd = &compute.VirtualHardDisk{
   781  			URI: to.StringPtr(fmt.Sprintf(
   782  				"https://%s.blob.storage.azurestack.local/datavhds/volume-%d.vhd",
   783  				storageAccountName, i,
   784  			)),
   785  		}
   786  	}
   787  
   788  	// volume-0 and volume-2 are attached to machine-0
   789  	// volume-1 is attached to machine-1
   790  	// volume-3 is attached to machine-42, but machine-42 is missing
   791  	// volume-42 is attached to machine-2, but machine-2 has no free LUNs
   792  	makeParams := func(volume, machine string, size uint64) storage.VolumeAttachmentParams {
   793  		return storage.VolumeAttachmentParams{
   794  			AttachmentParams: storage.AttachmentParams{
   795  				Provider:   "azure",
   796  				Machine:    names.NewMachineTag(machine),
   797  				InstanceId: instance.Id("machine-" + machine),
   798  			},
   799  			Volume:   names.NewVolumeTag(volume),
   800  			VolumeId: "volume-" + volume,
   801  		}
   802  	}
   803  	params := []storage.VolumeAttachmentParams{
   804  		makeParams("0", "0", 1),
   805  		makeParams("1", "1", 1025),
   806  		makeParams("2", "0", 1024),
   807  		makeParams("3", "42", 40),
   808  		makeParams("42", "2", 50),
   809  	}
   810  
   811  	virtualMachines := []compute.VirtualMachine{{
   812  		Name: to.StringPtr("machine-0"),
   813  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   814  			StorageProfile: &compute.StorageProfile{},
   815  		},
   816  	}, {
   817  		Name: to.StringPtr("machine-1"),
   818  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   819  			StorageProfile: &compute.StorageProfile{DataDisks: &machine1DataDisks},
   820  		},
   821  	}, {
   822  		Name: to.StringPtr("machine-2"),
   823  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   824  			StorageProfile: &compute.StorageProfile{DataDisks: &machine2DataDisks},
   825  		},
   826  	}}
   827  
   828  	// There should be a one API calls to list VMs, and one update per modified instance.
   829  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
   830  		Value: &virtualMachines,
   831  	})
   832  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
   833  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   834  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
   835  
   836  	volumeSource := s.volumeSource(c, legacy)
   837  	s.requests = nil
   838  	s.sender = azuretesting.Senders{
   839  		virtualMachinesSender,
   840  		updateVirtualMachine0Sender,
   841  		updateVirtualMachine0Sender,
   842  	}
   843  
   844  	results, err := volumeSource.AttachVolumes(s.cloudCallCtx, params)
   845  	c.Assert(err, jc.ErrorIsNil)
   846  	c.Assert(results, gc.HasLen, len(params))
   847  
   848  	c.Check(results[0].Error, jc.ErrorIsNil)
   849  	c.Check(results[1].Error, jc.ErrorIsNil)
   850  	c.Check(results[2].Error, jc.ErrorIsNil)
   851  	c.Check(results[3].Error, gc.ErrorMatches, "instance machine-42 not found")
   852  	c.Check(results[4].Error, gc.ErrorMatches, "choosing LUN: all LUNs are in use")
   853  
   854  	// Validate HTTP request bodies.
   855  	c.Assert(s.requests, gc.HasLen, 3)
   856  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list virtual machines
   857  	c.Assert(s.requests[1].Method, gc.Equals, "PUT") // update machine-0
   858  	c.Assert(s.requests[2].Method, gc.Equals, "GET") // result call
   859  
   860  	makeVhd := func(volumeName string) *compute.VirtualHardDisk {
   861  		if !legacy {
   862  			return nil
   863  		}
   864  		return &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   865  			"https://%s.blob.storage.azurestack.local/datavhds/%s.vhd",
   866  			storageAccountName, volumeName,
   867  		))}
   868  	}
   869  	makeManagedDisk := func(volumeName string) *compute.ManagedDiskParameters {
   870  		if legacy {
   871  			return nil
   872  		}
   873  		return &compute.ManagedDiskParameters{
   874  			ID: to.StringPtr("/subscriptions/22222222-2222-2222-2222-222222222222/resourceGroups/juju-testmodel-model-deadbeef-0bad-400d-8000-4b1d0d06f00d/providers/Microsoft.Compute/disks/" + volumeName),
   875  		}
   876  	}
   877  
   878  	machine0DataDisks := []compute.DataDisk{{
   879  		Lun:          to.Int32Ptr(0),
   880  		Name:         to.StringPtr("volume-0"),
   881  		Vhd:          makeVhd("volume-0"),
   882  		ManagedDisk:  makeManagedDisk("volume-0"),
   883  		Caching:      compute.CachingTypesReadWrite,
   884  		CreateOption: compute.DiskCreateOptionTypesAttach,
   885  	}, {
   886  		Lun:          to.Int32Ptr(1),
   887  		Name:         to.StringPtr("volume-2"),
   888  		Vhd:          makeVhd("volume-2"),
   889  		ManagedDisk:  makeManagedDisk("volume-2"),
   890  		Caching:      compute.CachingTypesReadWrite,
   891  		CreateOption: compute.DiskCreateOptionTypesAttach,
   892  	}}
   893  
   894  	virtualMachines[0].StorageProfile.DataDisks = &machine0DataDisks
   895  	assertRequestBody(c, s.requests[1], &virtualMachines[0])
   896  }
   897  
   898  func (s *storageSuite) TestDetachVolumes(c *gc.C) {
   899  	s.testDetachVolumes(c, false)
   900  }
   901  
   902  func (s *storageSuite) TestDetachVolumesLegacy(c *gc.C) {
   903  	s.testDetachVolumes(c, true)
   904  }
   905  
   906  func (s *storageSuite) testDetachVolumes(c *gc.C, legacy bool) {
   907  	// machine-0 has a three data disks: volume-0, volume-1 and volume-2
   908  	machine0DataDisks := []compute.DataDisk{{
   909  		Lun:  to.Int32Ptr(0),
   910  		Name: to.StringPtr("volume-0"),
   911  	}, {
   912  		Lun:  to.Int32Ptr(1),
   913  		Name: to.StringPtr("volume-1"),
   914  	}, {
   915  		Lun:  to.Int32Ptr(2),
   916  		Name: to.StringPtr("volume-2"),
   917  	}}
   918  
   919  	makeParams := func(volume, machine string) storage.VolumeAttachmentParams {
   920  		return storage.VolumeAttachmentParams{
   921  			AttachmentParams: storage.AttachmentParams{
   922  				Provider:   "azure",
   923  				Machine:    names.NewMachineTag(machine),
   924  				InstanceId: instance.Id("machine-" + machine),
   925  			},
   926  			Volume:   names.NewVolumeTag(volume),
   927  			VolumeId: "volume-" + volume,
   928  		}
   929  	}
   930  	params := []storage.VolumeAttachmentParams{
   931  		makeParams("1", "0"),
   932  		makeParams("1", "0"),
   933  		makeParams("42", "1"),
   934  		makeParams("2", "42"),
   935  	}
   936  
   937  	virtualMachines := []compute.VirtualMachine{{
   938  		Name: to.StringPtr("machine-0"),
   939  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   940  			StorageProfile: &compute.StorageProfile{DataDisks: &machine0DataDisks},
   941  		},
   942  	}, {
   943  		Name: to.StringPtr("machine-1"),
   944  		VirtualMachineProperties: &compute.VirtualMachineProperties{
   945  			StorageProfile: &compute.StorageProfile{},
   946  		},
   947  	}}
   948  
   949  	// There should be a one API calls to list VMs, and one update per modified instance.
   950  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
   951  		Value: &virtualMachines,
   952  	})
   953  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
   954  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   955  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
   956  
   957  	volumeSource := s.volumeSource(c, legacy)
   958  	s.requests = nil
   959  	s.sender = azuretesting.Senders{
   960  		virtualMachinesSender,
   961  		updateVirtualMachine0Sender,
   962  		updateVirtualMachine0Sender,
   963  	}
   964  
   965  	results, err := volumeSource.DetachVolumes(s.cloudCallCtx, params)
   966  	c.Assert(err, jc.ErrorIsNil)
   967  	c.Assert(results, gc.HasLen, len(params))
   968  
   969  	c.Check(results[0], jc.ErrorIsNil)
   970  	c.Check(results[1], jc.ErrorIsNil)
   971  	c.Check(results[2], jc.ErrorIsNil)
   972  	c.Check(results[3], gc.ErrorMatches, "instance machine-42 not found")
   973  
   974  	// Validate HTTP request bodies.
   975  	c.Assert(s.requests, gc.HasLen, 3)
   976  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list virtual machines
   977  	c.Assert(s.requests[1].Method, gc.Equals, "PUT") // update machine-0
   978  	c.Assert(s.requests[2].Method, gc.Equals, "GET") // update machine-0 - future.Results call
   979  
   980  	machine0DataDisks = []compute.DataDisk{
   981  		machine0DataDisks[0],
   982  		machine0DataDisks[2],
   983  	}
   984  	virtualMachines[0].StorageProfile.DataDisks = &machine0DataDisks
   985  	assertRequestBody(c, s.requests[1], &virtualMachines[0])
   986  }
   987  
   988  func (s *storageSuite) TestDetachVolumesFinal(c *gc.C) {
   989  	// machine-0 has a one data disk: volume-0.
   990  	machine0DataDisks := []compute.DataDisk{{
   991  		Lun:  to.Int32Ptr(0),
   992  		Name: to.StringPtr("volume-0"),
   993  		Vhd: &compute.VirtualHardDisk{
   994  			URI: to.StringPtr(fmt.Sprintf(
   995  				"https://%s.blob.storage.azurestack.local/datavhds/volume-0.vhd",
   996  				storageAccountName,
   997  			)),
   998  		},
   999  	}}
  1000  
  1001  	params := []storage.VolumeAttachmentParams{{
  1002  		AttachmentParams: storage.AttachmentParams{
  1003  			Provider:   "azure",
  1004  			Machine:    names.NewMachineTag("0"),
  1005  			InstanceId: instance.Id("machine-0"),
  1006  		},
  1007  		Volume:   names.NewVolumeTag("0"),
  1008  		VolumeId: "volume-0",
  1009  	}}
  1010  
  1011  	virtualMachines := []compute.VirtualMachine{{
  1012  		Name: to.StringPtr("machine-0"),
  1013  		VirtualMachineProperties: &compute.VirtualMachineProperties{
  1014  			StorageProfile: &compute.StorageProfile{DataDisks: &machine0DataDisks},
  1015  		},
  1016  	}}
  1017  
  1018  	// There should be a one API call to list VMs, and one update to the VM.
  1019  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
  1020  		Value: &virtualMachines,
  1021  	})
  1022  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
  1023  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
  1024  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
  1025  
  1026  	volumeSource := s.volumeSource(c, false)
  1027  	s.requests = nil
  1028  	s.sender = azuretesting.Senders{
  1029  		virtualMachinesSender,
  1030  		updateVirtualMachine0Sender,
  1031  		updateVirtualMachine0Sender, // future.Results call
  1032  	}
  1033  
  1034  	results, err := volumeSource.DetachVolumes(s.cloudCallCtx, params)
  1035  	c.Assert(err, jc.ErrorIsNil)
  1036  	c.Assert(results, gc.HasLen, len(params))
  1037  	c.Assert(results[0], jc.ErrorIsNil)
  1038  
  1039  	// Validate HTTP request bodies.
  1040  	c.Assert(s.requests, gc.HasLen, 3)
  1041  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list virtual machines
  1042  	c.Assert(s.requests[1].Method, gc.Equals, "PUT") // update machine-0
  1043  	c.Assert(s.requests[2].Method, gc.Equals, "GET") // update machine-0 future.Results call
  1044  
  1045  	machine0DataDisks = []compute.DataDisk{}
  1046  	virtualMachines[0].StorageProfile.DataDisks = &machine0DataDisks
  1047  	assertRequestBody(c, s.requests[1], &virtualMachines[0])
  1048  }