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 }