github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to"
    11  	"github.com/Azure/azure-sdk-for-go/arm/compute"
    12  	"github.com/Azure/azure-sdk-for-go/arm/network"
    13  	azurestorage "github.com/Azure/azure-sdk-for-go/storage"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names"
    16  	jc "github.com/juju/testing/checkers"
    17  	gc "gopkg.in/check.v1"
    18  
    19  	"github.com/juju/juju/instance"
    20  	"github.com/juju/juju/provider/azure"
    21  	"github.com/juju/juju/provider/azure/internal/azuretesting"
    22  	"github.com/juju/juju/storage"
    23  	"github.com/juju/juju/testing"
    24  )
    25  
    26  type storageSuite struct {
    27  	testing.BaseSuite
    28  
    29  	storageClient azuretesting.MockStorageClient
    30  	provider      storage.Provider
    31  	requests      []*http.Request
    32  	sender        azuretesting.Senders
    33  }
    34  
    35  var _ = gc.Suite(&storageSuite{})
    36  
    37  func (s *storageSuite) SetUpTest(c *gc.C) {
    38  	s.BaseSuite.SetUpTest(c)
    39  	s.storageClient = azuretesting.MockStorageClient{}
    40  	s.requests = nil
    41  	_, s.provider = newProviders(c, azure.ProviderConfig{
    42  		Sender:           &s.sender,
    43  		NewStorageClient: s.storageClient.NewClient,
    44  		RequestInspector: requestRecorder(&s.requests),
    45  	})
    46  	s.sender = nil
    47  }
    48  
    49  func (s *storageSuite) volumeSource(c *gc.C, attrs ...testing.Attrs) storage.VolumeSource {
    50  	storageConfig, err := storage.NewConfig("azure", "azure", nil)
    51  	c.Assert(err, jc.ErrorIsNil)
    52  
    53  	attrs = append([]testing.Attrs{{
    54  		"storage-account":     fakeStorageAccount,
    55  		"storage-account-key": fakeStorageAccountKey,
    56  	}}, attrs...)
    57  	cfg := makeTestModelConfig(c, attrs...)
    58  	volumeSource, err := s.provider.VolumeSource(cfg, storageConfig)
    59  	c.Assert(err, jc.ErrorIsNil)
    60  
    61  	// Force an explicit refresh of the access token, so it isn't done
    62  	// implicitly during the tests.
    63  	s.sender = azuretesting.Senders{tokenRefreshSender()}
    64  	err = azure.ForceVolumeSourceTokenRefresh(volumeSource)
    65  	c.Assert(err, jc.ErrorIsNil)
    66  	return volumeSource
    67  }
    68  
    69  func (s *storageSuite) TestVolumeSource(c *gc.C) {
    70  	vs := s.volumeSource(c)
    71  	c.Assert(vs, gc.NotNil)
    72  }
    73  
    74  func (s *storageSuite) TestFilesystemSource(c *gc.C) {
    75  	storageConfig, err := storage.NewConfig("azure", "azure", nil)
    76  	c.Assert(err, jc.ErrorIsNil)
    77  
    78  	cfg := makeTestModelConfig(c)
    79  	_, err = s.provider.FilesystemSource(cfg, storageConfig)
    80  	c.Assert(err, gc.ErrorMatches, "filesystems not supported")
    81  	c.Assert(err, jc.Satisfies, errors.IsNotSupported)
    82  }
    83  
    84  func (s *storageSuite) TestSupports(c *gc.C) {
    85  	c.Assert(s.provider.Supports(storage.StorageKindBlock), jc.IsTrue)
    86  	c.Assert(s.provider.Supports(storage.StorageKindFilesystem), jc.IsFalse)
    87  }
    88  
    89  func (s *storageSuite) TestDynamic(c *gc.C) {
    90  	c.Assert(s.provider.Dynamic(), jc.IsTrue)
    91  }
    92  
    93  func (s *storageSuite) TestScope(c *gc.C) {
    94  	c.Assert(s.provider.Scope(), gc.Equals, storage.ScopeEnviron)
    95  }
    96  
    97  func (s *storageSuite) TestCreateVolumes(c *gc.C) {
    98  	// machine-1 has a single data disk with LUN 0.
    99  	machine1DataDisks := []compute.DataDisk{{Lun: to.IntPtr(0)}}
   100  	// machine-2 has 32 data disks; no LUNs free.
   101  	machine2DataDisks := make([]compute.DataDisk, 32)
   102  	for i := range machine2DataDisks {
   103  		machine2DataDisks[i].Lun = to.IntPtr(i)
   104  	}
   105  
   106  	// volume-0 and volume-2 are attached to machine-0
   107  	// volume-1 is attached to machine-1
   108  	// volume-3 is attached to machine-42, but machine-42 is missing
   109  	// volume-42 is attached to machine-2, but machine-2 has no free LUNs
   110  	makeVolumeParams := func(volume, machine string, size uint64) storage.VolumeParams {
   111  		return storage.VolumeParams{
   112  			Tag:      names.NewVolumeTag(volume),
   113  			Size:     size,
   114  			Provider: "azure",
   115  			Attachment: &storage.VolumeAttachmentParams{
   116  				AttachmentParams: storage.AttachmentParams{
   117  					Provider:   "azure",
   118  					Machine:    names.NewMachineTag(machine),
   119  					InstanceId: instance.Id("machine-" + machine),
   120  				},
   121  				Volume: names.NewVolumeTag(volume),
   122  			},
   123  		}
   124  	}
   125  	params := []storage.VolumeParams{
   126  		makeVolumeParams("0", "0", 1),
   127  		makeVolumeParams("1", "1", 1025),
   128  		makeVolumeParams("2", "0", 1024),
   129  		makeVolumeParams("3", "42", 40),
   130  		makeVolumeParams("42", "2", 50),
   131  	}
   132  
   133  	virtualMachines := []compute.VirtualMachine{{
   134  		Name: to.StringPtr("machine-0"),
   135  		Properties: &compute.VirtualMachineProperties{
   136  			StorageProfile: &compute.StorageProfile{},
   137  		},
   138  	}, {
   139  		Name: to.StringPtr("machine-1"),
   140  		Properties: &compute.VirtualMachineProperties{
   141  			StorageProfile: &compute.StorageProfile{DataDisks: &machine1DataDisks},
   142  		},
   143  	}, {
   144  		Name: to.StringPtr("machine-2"),
   145  		Properties: &compute.VirtualMachineProperties{
   146  			StorageProfile: &compute.StorageProfile{DataDisks: &machine2DataDisks},
   147  		},
   148  	}}
   149  
   150  	// There should be a couple of API calls to list instances,
   151  	// and one update per modified instance.
   152  	nics := []network.Interface{
   153  		makeNetworkInterface("nic-0", "machine-0"),
   154  		makeNetworkInterface("nic-1", "machine-1"),
   155  		makeNetworkInterface("nic-2", "machine-2"),
   156  	}
   157  	nicsSender := azuretesting.NewSenderWithValue(network.InterfaceListResult{
   158  		Value: &nics,
   159  	})
   160  	nicsSender.PathPattern = `.*/Microsoft\.Network/networkInterfaces`
   161  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
   162  		Value: &virtualMachines,
   163  	})
   164  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
   165  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   166  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
   167  	updateVirtualMachine1Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   168  	updateVirtualMachine1Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-1`
   169  	volumeSource := s.volumeSource(c)
   170  	s.sender = azuretesting.Senders{
   171  		nicsSender,
   172  		virtualMachinesSender,
   173  		updateVirtualMachine0Sender,
   174  		updateVirtualMachine1Sender,
   175  	}
   176  
   177  	results, err := volumeSource.CreateVolumes(params)
   178  	c.Assert(err, jc.ErrorIsNil)
   179  	c.Assert(results, gc.HasLen, len(params))
   180  
   181  	c.Check(results[0].Error, jc.ErrorIsNil)
   182  	c.Check(results[1].Error, jc.ErrorIsNil)
   183  	c.Check(results[2].Error, jc.ErrorIsNil)
   184  	c.Check(results[3].Error, gc.ErrorMatches, "instance machine-42 not found")
   185  	c.Check(results[4].Error, gc.ErrorMatches, "choosing LUN: all LUNs are in use")
   186  
   187  	// Validate HTTP request bodies.
   188  	c.Assert(s.requests, gc.HasLen, 4)
   189  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list NICs
   190  	c.Assert(s.requests[1].Method, gc.Equals, "GET") // list virtual machines
   191  	c.Assert(s.requests[2].Method, gc.Equals, "PUT") // update machine-0
   192  	c.Assert(s.requests[3].Method, gc.Equals, "PUT") // update machine-1
   193  
   194  	machine0DataDisks := []compute.DataDisk{{
   195  		Lun:        to.IntPtr(0),
   196  		DiskSizeGB: to.IntPtr(1),
   197  		Name:       to.StringPtr("volume-0"),
   198  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   199  			"https://%s.blob.storage.azurestack.local/datavhds/volume-0.vhd",
   200  			fakeStorageAccount,
   201  		))},
   202  		Caching:      compute.ReadWrite,
   203  		CreateOption: compute.Empty,
   204  	}, {
   205  		Lun:        to.IntPtr(1),
   206  		DiskSizeGB: to.IntPtr(1),
   207  		Name:       to.StringPtr("volume-2"),
   208  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   209  			"https://%s.blob.storage.azurestack.local/datavhds/volume-2.vhd",
   210  			fakeStorageAccount,
   211  		))},
   212  		Caching:      compute.ReadWrite,
   213  		CreateOption: compute.Empty,
   214  	}}
   215  	virtualMachines[0].Properties.StorageProfile.DataDisks = &machine0DataDisks
   216  	assertRequestBody(c, s.requests[2], &virtualMachines[0])
   217  
   218  	machine1DataDisks = append(machine1DataDisks, compute.DataDisk{
   219  		Lun:        to.IntPtr(1),
   220  		DiskSizeGB: to.IntPtr(2),
   221  		Name:       to.StringPtr("volume-1"),
   222  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   223  			"https://%s.blob.storage.azurestack.local/datavhds/volume-1.vhd",
   224  			fakeStorageAccount,
   225  		))},
   226  		Caching:      compute.ReadWrite,
   227  		CreateOption: compute.Empty,
   228  	})
   229  	assertRequestBody(c, s.requests[3], &virtualMachines[1])
   230  }
   231  
   232  func (s *storageSuite) TestListVolumes(c *gc.C) {
   233  	s.storageClient.ListBlobsFunc = func(
   234  		container string,
   235  		params azurestorage.ListBlobsParameters,
   236  	) (azurestorage.BlobListResponse, error) {
   237  		return azurestorage.BlobListResponse{
   238  			Blobs: []azurestorage.Blob{{
   239  				Name: "volume-1.vhd",
   240  				Properties: azurestorage.BlobProperties{
   241  					ContentLength: 1024 * 1024, // 1MiB
   242  				},
   243  			}, {
   244  				Name: "volume-0.vhd",
   245  				Properties: azurestorage.BlobProperties{
   246  					ContentLength: 1024 * 1024 * 1024 * 1024, // 1TiB
   247  				},
   248  			}, {
   249  				Name: "junk.vhd",
   250  			}, {
   251  				Name: "volume",
   252  			}},
   253  		}, nil
   254  	}
   255  
   256  	volumeSource := s.volumeSource(c)
   257  	volumeIds, err := volumeSource.ListVolumes()
   258  	c.Assert(err, jc.ErrorIsNil)
   259  	s.storageClient.CheckCallNames(c, "NewClient", "ListBlobs")
   260  	s.storageClient.CheckCall(
   261  		c, 0, "NewClient", fakeStorageAccount, fakeStorageAccountKey,
   262  		"storage.azurestack.local", azurestorage.DefaultAPIVersion, true,
   263  	)
   264  	s.storageClient.CheckCall(c, 1, "ListBlobs", "datavhds", azurestorage.ListBlobsParameters{})
   265  	c.Assert(volumeIds, jc.DeepEquals, []string{"volume-1", "volume-0"})
   266  }
   267  
   268  func (s *storageSuite) TestListVolumesErrors(c *gc.C) {
   269  	volumeSource := s.volumeSource(c)
   270  	s.storageClient.SetErrors(errors.New("no client for you"))
   271  	_, err := volumeSource.ListVolumes()
   272  	c.Assert(err, gc.ErrorMatches, "listing volumes: getting storage client: no client for you")
   273  
   274  	s.storageClient.SetErrors(nil, errors.New("no blobs for you"))
   275  	_, err = volumeSource.ListVolumes()
   276  	c.Assert(err, gc.ErrorMatches, "listing volumes: listing blobs: no blobs for you")
   277  }
   278  
   279  func (s *storageSuite) TestDescribeVolumes(c *gc.C) {
   280  	s.storageClient.ListBlobsFunc = func(
   281  		container string,
   282  		params azurestorage.ListBlobsParameters,
   283  	) (azurestorage.BlobListResponse, error) {
   284  		return azurestorage.BlobListResponse{
   285  			Blobs: []azurestorage.Blob{{
   286  				Name: "volume-1.vhd",
   287  				Properties: azurestorage.BlobProperties{
   288  					ContentLength: 1024 * 1024, // 1MiB
   289  				},
   290  			}, {
   291  				Name: "volume-0.vhd",
   292  				Properties: azurestorage.BlobProperties{
   293  					ContentLength: 1024 * 1024 * 1024 * 1024, // 1TiB
   294  				},
   295  			}},
   296  		}, nil
   297  	}
   298  
   299  	volumeSource := s.volumeSource(c)
   300  	results, err := volumeSource.DescribeVolumes([]string{"volume-0", "volume-1", "volume-0", "volume-42"})
   301  	c.Assert(err, jc.ErrorIsNil)
   302  	s.storageClient.CheckCallNames(c, "NewClient", "ListBlobs")
   303  	s.storageClient.CheckCall(
   304  		c, 0, "NewClient", fakeStorageAccount, fakeStorageAccountKey,
   305  		"storage.azurestack.local", azurestorage.DefaultAPIVersion, true,
   306  	)
   307  	c.Assert(results, gc.HasLen, 4)
   308  	c.Assert(results[:3], jc.DeepEquals, []storage.DescribeVolumesResult{{
   309  		VolumeInfo: &storage.VolumeInfo{
   310  			VolumeId:   "volume-0",
   311  			Size:       1024 * 1024,
   312  			Persistent: true,
   313  		},
   314  	}, {
   315  		VolumeInfo: &storage.VolumeInfo{
   316  			VolumeId:   "volume-1",
   317  			Size:       1,
   318  			Persistent: true,
   319  		},
   320  	}, {
   321  		VolumeInfo: &storage.VolumeInfo{
   322  			VolumeId:   "volume-0",
   323  			Size:       1024 * 1024,
   324  			Persistent: true,
   325  		},
   326  	}})
   327  	c.Assert(results[3].Error, gc.ErrorMatches, "volume-42 not found")
   328  }
   329  
   330  func (s *storageSuite) TestDestroyVolumes(c *gc.C) {
   331  	volumeSource := s.volumeSource(c)
   332  	results, err := volumeSource.DestroyVolumes([]string{"volume-0", "volume-42"})
   333  	c.Assert(err, jc.ErrorIsNil)
   334  	c.Assert(results, gc.HasLen, 2)
   335  	c.Assert(results[0], jc.ErrorIsNil)
   336  	c.Assert(results[1], jc.ErrorIsNil)
   337  	s.storageClient.CheckCallNames(c, "NewClient", "DeleteBlobIfExists", "DeleteBlobIfExists")
   338  	s.storageClient.CheckCall(c, 1, "DeleteBlobIfExists", "datavhds", "volume-0.vhd")
   339  	s.storageClient.CheckCall(c, 2, "DeleteBlobIfExists", "datavhds", "volume-42.vhd")
   340  }
   341  
   342  func (s *storageSuite) TestAttachVolumes(c *gc.C) {
   343  	// machine-1 has a single data disk with LUN 0.
   344  	machine1DataDisks := []compute.DataDisk{{
   345  		Lun:  to.IntPtr(0),
   346  		Name: to.StringPtr("volume-1"),
   347  		Vhd: &compute.VirtualHardDisk{
   348  			URI: to.StringPtr(fmt.Sprintf(
   349  				"https://%s.blob.storage.azurestack.local/datavhds/volume-1.vhd",
   350  				fakeStorageAccount,
   351  			)),
   352  		},
   353  	}}
   354  	// machine-2 has 32 data disks; no LUNs free.
   355  	machine2DataDisks := make([]compute.DataDisk, 32)
   356  	for i := range machine2DataDisks {
   357  		machine2DataDisks[i].Lun = to.IntPtr(i)
   358  		machine2DataDisks[i].Name = to.StringPtr(fmt.Sprintf("volume-%d", i))
   359  		machine2DataDisks[i].Vhd = &compute.VirtualHardDisk{
   360  			URI: to.StringPtr(fmt.Sprintf(
   361  				"https://%s.blob.storage.azurestack.local/datavhds/volume-%d.vhd",
   362  				fakeStorageAccount, i,
   363  			)),
   364  		}
   365  	}
   366  
   367  	// volume-0 and volume-2 are attached to machine-0
   368  	// volume-1 is attached to machine-1
   369  	// volume-3 is attached to machine-42, but machine-42 is missing
   370  	// volume-42 is attached to machine-2, but machine-2 has no free LUNs
   371  	makeParams := func(volume, machine string, size uint64) storage.VolumeAttachmentParams {
   372  		return storage.VolumeAttachmentParams{
   373  			AttachmentParams: storage.AttachmentParams{
   374  				Provider:   "azure",
   375  				Machine:    names.NewMachineTag(machine),
   376  				InstanceId: instance.Id("machine-" + machine),
   377  			},
   378  			Volume:   names.NewVolumeTag(volume),
   379  			VolumeId: "volume-" + volume,
   380  		}
   381  	}
   382  	params := []storage.VolumeAttachmentParams{
   383  		makeParams("0", "0", 1),
   384  		makeParams("1", "1", 1025),
   385  		makeParams("2", "0", 1024),
   386  		makeParams("3", "42", 40),
   387  		makeParams("42", "2", 50),
   388  	}
   389  
   390  	virtualMachines := []compute.VirtualMachine{{
   391  		Name: to.StringPtr("machine-0"),
   392  		Properties: &compute.VirtualMachineProperties{
   393  			StorageProfile: &compute.StorageProfile{},
   394  		},
   395  	}, {
   396  		Name: to.StringPtr("machine-1"),
   397  		Properties: &compute.VirtualMachineProperties{
   398  			StorageProfile: &compute.StorageProfile{DataDisks: &machine1DataDisks},
   399  		},
   400  	}, {
   401  		Name: to.StringPtr("machine-2"),
   402  		Properties: &compute.VirtualMachineProperties{
   403  			StorageProfile: &compute.StorageProfile{DataDisks: &machine2DataDisks},
   404  		},
   405  	}}
   406  
   407  	// There should be a couple of API calls to list instances,
   408  	// and one update per modified instance.
   409  	nics := []network.Interface{
   410  		makeNetworkInterface("nic-0", "machine-0"),
   411  		makeNetworkInterface("nic-1", "machine-1"),
   412  		makeNetworkInterface("nic-2", "machine-2"),
   413  	}
   414  	nicsSender := azuretesting.NewSenderWithValue(network.InterfaceListResult{
   415  		Value: &nics,
   416  	})
   417  	nicsSender.PathPattern = `.*/Microsoft\.Network/networkInterfaces`
   418  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
   419  		Value: &virtualMachines,
   420  	})
   421  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
   422  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   423  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
   424  	volumeSource := s.volumeSource(c)
   425  	s.sender = azuretesting.Senders{
   426  		nicsSender,
   427  		virtualMachinesSender,
   428  		updateVirtualMachine0Sender,
   429  	}
   430  
   431  	results, err := volumeSource.AttachVolumes(params)
   432  	c.Assert(err, jc.ErrorIsNil)
   433  	c.Assert(results, gc.HasLen, len(params))
   434  
   435  	c.Check(results[0].Error, jc.ErrorIsNil)
   436  	c.Check(results[1].Error, jc.ErrorIsNil)
   437  	c.Check(results[2].Error, jc.ErrorIsNil)
   438  	c.Check(results[3].Error, gc.ErrorMatches, "instance machine-42 not found")
   439  	c.Check(results[4].Error, gc.ErrorMatches, "choosing LUN: all LUNs are in use")
   440  
   441  	// Validate HTTP request bodies.
   442  	c.Assert(s.requests, gc.HasLen, 3)
   443  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list NICs
   444  	c.Assert(s.requests[1].Method, gc.Equals, "GET") // list virtual machines
   445  	c.Assert(s.requests[2].Method, gc.Equals, "PUT") // update machine-0
   446  
   447  	machine0DataDisks := []compute.DataDisk{{
   448  		Lun:  to.IntPtr(0),
   449  		Name: to.StringPtr("volume-0"),
   450  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   451  			"https://%s.blob.storage.azurestack.local/datavhds/volume-0.vhd",
   452  			fakeStorageAccount,
   453  		))},
   454  		Caching:      compute.ReadWrite,
   455  		CreateOption: compute.Attach,
   456  	}, {
   457  		Lun:  to.IntPtr(1),
   458  		Name: to.StringPtr("volume-2"),
   459  		Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(fmt.Sprintf(
   460  			"https://%s.blob.storage.azurestack.local/datavhds/volume-2.vhd",
   461  			fakeStorageAccount,
   462  		))},
   463  		Caching:      compute.ReadWrite,
   464  		CreateOption: compute.Attach,
   465  	}}
   466  	virtualMachines[0].Properties.StorageProfile.DataDisks = &machine0DataDisks
   467  	assertRequestBody(c, s.requests[2], &virtualMachines[0])
   468  }
   469  
   470  func (s *storageSuite) TestDetachVolumes(c *gc.C) {
   471  	// machine-0 has a three data disks: volume-0, volume-1 and volume-2
   472  	machine0DataDisks := []compute.DataDisk{{
   473  		Lun:  to.IntPtr(0),
   474  		Name: to.StringPtr("volume-0"),
   475  		Vhd: &compute.VirtualHardDisk{
   476  			URI: to.StringPtr(fmt.Sprintf(
   477  				"https://%s.blob.storage.azurestack.local/datavhds/volume-0.vhd",
   478  				fakeStorageAccount,
   479  			)),
   480  		},
   481  	}, {
   482  		Lun:  to.IntPtr(1),
   483  		Name: to.StringPtr("volume-1"),
   484  		Vhd: &compute.VirtualHardDisk{
   485  			URI: to.StringPtr(fmt.Sprintf(
   486  				"https://%s.blob.storage.azurestack.local/datavhds/volume-1.vhd",
   487  				fakeStorageAccount,
   488  			)),
   489  		},
   490  	}, {
   491  		Lun:  to.IntPtr(2),
   492  		Name: to.StringPtr("volume-2"),
   493  		Vhd: &compute.VirtualHardDisk{
   494  			URI: to.StringPtr(fmt.Sprintf(
   495  				"https://%s.blob.storage.azurestack.local/datavhds/volume-2.vhd",
   496  				fakeStorageAccount,
   497  			)),
   498  		},
   499  	}}
   500  
   501  	makeParams := func(volume, machine string) storage.VolumeAttachmentParams {
   502  		return storage.VolumeAttachmentParams{
   503  			AttachmentParams: storage.AttachmentParams{
   504  				Provider:   "azure",
   505  				Machine:    names.NewMachineTag(machine),
   506  				InstanceId: instance.Id("machine-" + machine),
   507  			},
   508  			Volume:   names.NewVolumeTag(volume),
   509  			VolumeId: "volume-" + volume,
   510  		}
   511  	}
   512  	params := []storage.VolumeAttachmentParams{
   513  		makeParams("1", "0"),
   514  		makeParams("1", "0"),
   515  		makeParams("42", "1"),
   516  		makeParams("2", "42"),
   517  	}
   518  
   519  	virtualMachines := []compute.VirtualMachine{{
   520  		Name: to.StringPtr("machine-0"),
   521  		Properties: &compute.VirtualMachineProperties{
   522  			StorageProfile: &compute.StorageProfile{DataDisks: &machine0DataDisks},
   523  		},
   524  	}, {
   525  		Name: to.StringPtr("machine-1"),
   526  		Properties: &compute.VirtualMachineProperties{
   527  			StorageProfile: &compute.StorageProfile{},
   528  		},
   529  	}}
   530  
   531  	// There should be a couple of API calls to list instances,
   532  	// and one update per modified instance.
   533  	nics := []network.Interface{
   534  		makeNetworkInterface("nic-0", "machine-0"),
   535  		makeNetworkInterface("nic-1", "machine-1"),
   536  	}
   537  	nicsSender := azuretesting.NewSenderWithValue(network.InterfaceListResult{
   538  		Value: &nics,
   539  	})
   540  	nicsSender.PathPattern = `.*/Microsoft\.Network/networkInterfaces`
   541  	virtualMachinesSender := azuretesting.NewSenderWithValue(compute.VirtualMachineListResult{
   542  		Value: &virtualMachines,
   543  	})
   544  	virtualMachinesSender.PathPattern = `.*/Microsoft\.Compute/virtualMachines`
   545  	updateVirtualMachine0Sender := azuretesting.NewSenderWithValue(&compute.VirtualMachine{})
   546  	updateVirtualMachine0Sender.PathPattern = `.*/Microsoft\.Compute/virtualMachines/machine-0`
   547  	volumeSource := s.volumeSource(c)
   548  	s.sender = azuretesting.Senders{
   549  		nicsSender,
   550  		virtualMachinesSender,
   551  		updateVirtualMachine0Sender,
   552  	}
   553  
   554  	results, err := volumeSource.DetachVolumes(params)
   555  	c.Assert(err, jc.ErrorIsNil)
   556  	c.Assert(results, gc.HasLen, len(params))
   557  
   558  	c.Check(results[0], jc.ErrorIsNil)
   559  	c.Check(results[1], jc.ErrorIsNil)
   560  	c.Check(results[2], jc.ErrorIsNil)
   561  	c.Check(results[3], gc.ErrorMatches, "instance machine-42 not found")
   562  
   563  	// Validate HTTP request bodies.
   564  	c.Assert(s.requests, gc.HasLen, 3)
   565  	c.Assert(s.requests[0].Method, gc.Equals, "GET") // list NICs
   566  	c.Assert(s.requests[1].Method, gc.Equals, "GET") // list virtual machines
   567  	c.Assert(s.requests[2].Method, gc.Equals, "PUT") // update machine-0
   568  
   569  	machine0DataDisks = []compute.DataDisk{
   570  		machine0DataDisks[0],
   571  		machine0DataDisks[2],
   572  	}
   573  	virtualMachines[0].Properties.StorageProfile.DataDisks = &machine0DataDisks
   574  	assertRequestBody(c, s.requests[2], &virtualMachines[0])
   575  }