github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/azure/environ_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 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 "path" 12 "reflect" 13 "time" 14 15 autorestazure "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure" 16 "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/mocks" 17 "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to" 18 "github.com/Azure/azure-sdk-for-go/arm/compute" 19 "github.com/Azure/azure-sdk-for-go/arm/network" 20 "github.com/Azure/azure-sdk-for-go/arm/resources" 21 "github.com/Azure/azure-sdk-for-go/arm/storage" 22 "github.com/juju/names" 23 gitjujutesting "github.com/juju/testing" 24 jc "github.com/juju/testing/checkers" 25 "github.com/juju/utils/arch" 26 "github.com/juju/utils/series" 27 gc "gopkg.in/check.v1" 28 29 "github.com/juju/juju/api" 30 "github.com/juju/juju/cloudconfig/instancecfg" 31 "github.com/juju/juju/constraints" 32 "github.com/juju/juju/environs" 33 "github.com/juju/juju/environs/imagemetadata" 34 "github.com/juju/juju/environs/simplestreams" 35 "github.com/juju/juju/environs/tags" 36 envtesting "github.com/juju/juju/environs/testing" 37 envtools "github.com/juju/juju/environs/tools" 38 "github.com/juju/juju/instance" 39 "github.com/juju/juju/mongo" 40 "github.com/juju/juju/provider/azure" 41 "github.com/juju/juju/provider/azure/internal/azuretesting" 42 "github.com/juju/juju/testing" 43 "github.com/juju/juju/tools" 44 "github.com/juju/version" 45 ) 46 47 type environSuite struct { 48 testing.BaseSuite 49 50 provider environs.EnvironProvider 51 requests []*http.Request 52 storageClient azuretesting.MockStorageClient 53 sender azuretesting.Senders 54 retryClock mockClock 55 56 tags map[string]*string 57 group *resources.ResourceGroup 58 vmSizes *compute.VirtualMachineSizeListResult 59 storageNameAvailabilityResult *storage.CheckNameAvailabilityResult 60 storageAccount *storage.Account 61 storageAccountKeys *storage.AccountKeys 62 vnet *network.VirtualNetwork 63 nsg *network.SecurityGroup 64 subnet *network.Subnet 65 ubuntuServerSKUs []compute.VirtualMachineImageResource 66 publicIPAddress *network.PublicIPAddress 67 oldNetworkInterfaces *network.InterfaceListResult 68 newNetworkInterface *network.Interface 69 jujuAvailabilitySet *compute.AvailabilitySet 70 virtualMachine *compute.VirtualMachine 71 } 72 73 var _ = gc.Suite(&environSuite{}) 74 75 func (s *environSuite) SetUpTest(c *gc.C) { 76 s.BaseSuite.SetUpTest(c) 77 s.storageClient = azuretesting.MockStorageClient{} 78 s.sender = nil 79 s.retryClock = mockClock{Clock: testing.NewClock(time.Time{})} 80 81 s.provider, _ = newProviders(c, azure.ProviderConfig{ 82 Sender: &s.sender, 83 RequestInspector: requestRecorder(&s.requests), 84 NewStorageClient: s.storageClient.NewClient, 85 RetryClock: &testing.AutoAdvancingClock{ 86 &s.retryClock, s.retryClock.Advance, 87 }, 88 }) 89 90 envTags := map[string]*string{ 91 "juju-model-uuid": to.StringPtr(testing.ModelTag.Id()), 92 "juju-controller-uuid": to.StringPtr(testing.ModelTag.Id()), 93 } 94 s.tags = map[string]*string{ 95 "juju-machine-name": to.StringPtr("machine-0"), 96 } 97 98 s.group = &resources.ResourceGroup{ 99 Location: to.StringPtr("westus"), 100 Tags: &envTags, 101 } 102 103 vmSizes := []compute.VirtualMachineSize{{ 104 Name: to.StringPtr("Standard_D1"), 105 NumberOfCores: to.IntPtr(1), 106 OsDiskSizeInMB: to.IntPtr(1047552), 107 ResourceDiskSizeInMB: to.IntPtr(51200), 108 MemoryInMB: to.IntPtr(3584), 109 MaxDataDiskCount: to.IntPtr(2), 110 }} 111 s.vmSizes = &compute.VirtualMachineSizeListResult{Value: &vmSizes} 112 113 s.storageNameAvailabilityResult = &storage.CheckNameAvailabilityResult{ 114 NameAvailable: to.BoolPtr(true), 115 } 116 117 s.storageAccount = &storage.Account{ 118 Name: to.StringPtr("my-storage-account"), 119 Type: to.StringPtr("Standard_LRS"), 120 Tags: &envTags, 121 Properties: &storage.AccountProperties{ 122 PrimaryEndpoints: &storage.Endpoints{ 123 Blob: to.StringPtr(fmt.Sprintf("https://%s.blob.storage.azurestack.local/", fakeStorageAccount)), 124 }, 125 }, 126 } 127 128 s.storageAccountKeys = &storage.AccountKeys{ 129 Key1: to.StringPtr("key-1"), 130 } 131 132 addressPrefixes := []string{"10.0.0.0/16"} 133 s.vnet = &network.VirtualNetwork{ 134 ID: to.StringPtr("juju-internal-network"), 135 Name: to.StringPtr("juju-internal-network"), 136 Location: to.StringPtr("westus"), 137 Tags: &envTags, 138 Properties: &network.VirtualNetworkPropertiesFormat{ 139 AddressSpace: &network.AddressSpace{&addressPrefixes}, 140 }, 141 } 142 143 s.nsg = &network.SecurityGroup{ 144 ID: to.StringPtr(path.Join( 145 "/subscriptions", fakeSubscriptionId, 146 "resourceGroups", "juju-testenv-model-"+testing.ModelTag.Id(), 147 "providers/Microsoft.Network/networkSecurityGroups/juju-internal-nsg", 148 )), 149 Tags: &envTags, 150 } 151 152 s.subnet = &network.Subnet{ 153 ID: to.StringPtr("subnet-id"), 154 Name: to.StringPtr("juju-internal-subnet"), 155 Properties: &network.SubnetPropertiesFormat{ 156 AddressPrefix: to.StringPtr("10.0.0.0/16"), 157 NetworkSecurityGroup: &network.SubResource{s.nsg.ID}, 158 }, 159 } 160 161 s.ubuntuServerSKUs = []compute.VirtualMachineImageResource{ 162 {Name: to.StringPtr("12.04-LTS")}, 163 {Name: to.StringPtr("12.10")}, 164 {Name: to.StringPtr("14.04-LTS")}, 165 {Name: to.StringPtr("15.04")}, 166 {Name: to.StringPtr("15.10")}, 167 {Name: to.StringPtr("16.04-LTS")}, 168 } 169 170 s.publicIPAddress = &network.PublicIPAddress{ 171 ID: to.StringPtr("public-ip-id"), 172 Name: to.StringPtr("machine-0-public-ip"), 173 Location: to.StringPtr("westus"), 174 Tags: &s.tags, 175 Properties: &network.PublicIPAddressPropertiesFormat{ 176 PublicIPAllocationMethod: network.Dynamic, 177 IPAddress: to.StringPtr("1.2.3.4"), 178 }, 179 } 180 181 // Existing IPs/NICs. These are the results of querying NICs so we 182 // can tell which IP to allocate. 183 oldIPConfigurations := []network.InterfaceIPConfiguration{{ 184 ID: to.StringPtr("ip-configuration-0-id"), 185 Name: to.StringPtr("ip-configuration-0"), 186 Properties: &network.InterfaceIPConfigurationPropertiesFormat{ 187 PrivateIPAddress: to.StringPtr("10.0.0.4"), 188 PrivateIPAllocationMethod: network.Static, 189 Subnet: &network.SubResource{ID: s.subnet.ID}, 190 }, 191 }} 192 oldNetworkInterfaces := []network.Interface{{ 193 ID: to.StringPtr("network-interface-0-id"), 194 Name: to.StringPtr("network-interface-0"), 195 Properties: &network.InterfacePropertiesFormat{ 196 IPConfigurations: &oldIPConfigurations, 197 Primary: to.BoolPtr(true), 198 }, 199 }} 200 s.oldNetworkInterfaces = &network.InterfaceListResult{ 201 Value: &oldNetworkInterfaces, 202 } 203 204 // The newly created IP/NIC. 205 newIPConfigurations := []network.InterfaceIPConfiguration{{ 206 ID: to.StringPtr("ip-configuration-1-id"), 207 Name: to.StringPtr("primary"), 208 Properties: &network.InterfaceIPConfigurationPropertiesFormat{ 209 PrivateIPAddress: to.StringPtr("10.0.0.5"), 210 PrivateIPAllocationMethod: network.Static, 211 Subnet: &network.SubResource{ID: s.subnet.ID}, 212 PublicIPAddress: &network.SubResource{ID: s.publicIPAddress.ID}, 213 }, 214 }} 215 s.newNetworkInterface = &network.Interface{ 216 ID: to.StringPtr("network-interface-1-id"), 217 Name: to.StringPtr("network-interface-1"), 218 Location: to.StringPtr("westus"), 219 Tags: &s.tags, 220 Properties: &network.InterfacePropertiesFormat{ 221 IPConfigurations: &newIPConfigurations, 222 }, 223 } 224 225 s.jujuAvailabilitySet = &compute.AvailabilitySet{ 226 ID: to.StringPtr("juju-availability-set-id"), 227 Name: to.StringPtr("juju"), 228 Location: to.StringPtr("westus"), 229 Tags: &envTags, 230 } 231 232 sshPublicKeys := []compute.SSHPublicKey{{ 233 Path: to.StringPtr("/home/ubuntu/.ssh/authorized_keys"), 234 KeyData: to.StringPtr(testing.FakeAuthKeys), 235 }} 236 networkInterfaceReferences := []compute.NetworkInterfaceReference{{ 237 ID: s.newNetworkInterface.ID, 238 Properties: &compute.NetworkInterfaceReferenceProperties{ 239 Primary: to.BoolPtr(true), 240 }, 241 }} 242 s.virtualMachine = &compute.VirtualMachine{ 243 ID: to.StringPtr("machine-0-id"), 244 Name: to.StringPtr("machine-0"), 245 Location: to.StringPtr("westus"), 246 Tags: &s.tags, 247 Properties: &compute.VirtualMachineProperties{ 248 HardwareProfile: &compute.HardwareProfile{ 249 VMSize: "Standard_D1", 250 }, 251 StorageProfile: &compute.StorageProfile{ 252 ImageReference: &compute.ImageReference{ 253 Publisher: to.StringPtr("Canonical"), 254 Offer: to.StringPtr("UbuntuServer"), 255 Sku: to.StringPtr("12.10"), 256 Version: to.StringPtr("latest"), 257 }, 258 OsDisk: &compute.OSDisk{ 259 Name: to.StringPtr("machine-0"), 260 CreateOption: compute.FromImage, 261 Caching: compute.ReadWrite, 262 Vhd: &compute.VirtualHardDisk{ 263 URI: to.StringPtr(fmt.Sprintf( 264 "https://%s.blob.storage.azurestack.local/osvhds/machine-0.vhd", 265 fakeStorageAccount, 266 )), 267 }, 268 }, 269 }, 270 OsProfile: &compute.OSProfile{ 271 ComputerName: to.StringPtr("machine-0"), 272 CustomData: to.StringPtr("<juju-goes-here>"), 273 AdminUsername: to.StringPtr("ubuntu"), 274 LinuxConfiguration: &compute.LinuxConfiguration{ 275 DisablePasswordAuthentication: to.BoolPtr(true), 276 SSH: &compute.SSHConfiguration{ 277 PublicKeys: &sshPublicKeys, 278 }, 279 }, 280 }, 281 NetworkProfile: &compute.NetworkProfile{ 282 NetworkInterfaces: &networkInterfaceReferences, 283 }, 284 AvailabilitySet: &compute.SubResource{ID: s.jujuAvailabilitySet.ID}, 285 ProvisioningState: to.StringPtr("Successful"), 286 }, 287 } 288 } 289 290 func (s *environSuite) openEnviron(c *gc.C, attrs ...testing.Attrs) environs.Environ { 291 attrs = append([]testing.Attrs{{"storage-account": fakeStorageAccount}}, attrs...) 292 return openEnviron(c, s.provider, &s.sender, attrs...) 293 } 294 295 func openEnviron( 296 c *gc.C, 297 provider environs.EnvironProvider, 298 sender *azuretesting.Senders, 299 attrs ...testing.Attrs, 300 ) environs.Environ { 301 // Opening the environment should not incur network communication, 302 // so we don't set s.sender until after opening. 303 cfg := makeTestModelConfig(c, attrs...) 304 env, err := provider.Open(cfg) 305 c.Assert(err, jc.ErrorIsNil) 306 307 // Force an explicit refresh of the access token, so it isn't done 308 // implicitly during the tests. 309 *sender = azuretesting.Senders{tokenRefreshSender()} 310 err = azure.ForceTokenRefresh(env) 311 c.Assert(err, jc.ErrorIsNil) 312 return env 313 } 314 315 func prepareForBootstrap( 316 c *gc.C, 317 ctx environs.BootstrapContext, 318 provider environs.EnvironProvider, 319 sender *azuretesting.Senders, 320 attrs ...testing.Attrs, 321 ) environs.Environ { 322 // Opening the environment should not incur network communication, 323 // so we don't set s.sender until after opening. 324 cfg := makeTestModelConfig(c, attrs...) 325 *sender = azuretesting.Senders{tokenRefreshSender()} 326 cfg, err := provider.BootstrapConfig(environs.BootstrapConfigParams{ 327 Config: cfg, 328 CloudRegion: "westus", 329 CloudEndpoint: "https://management.azure.com", 330 CloudStorageEndpoint: "https://core.windows.net", 331 Credentials: fakeUserPassCredential(), 332 }) 333 c.Assert(err, jc.ErrorIsNil) 334 env, err := provider.PrepareForBootstrap(ctx, cfg) 335 c.Assert(err, jc.ErrorIsNil) 336 return env 337 } 338 339 func tokenRefreshSender() *azuretesting.MockSender { 340 tokenRefreshSender := azuretesting.NewSenderWithValue(&autorestazure.Token{ 341 AccessToken: "access-token", 342 ExpiresOn: fmt.Sprint(time.Now().Add(time.Hour).Unix()), 343 Type: "Bearer", 344 }) 345 tokenRefreshSender.PathPattern = ".*/oauth2/token" 346 return tokenRefreshSender 347 } 348 349 func (s *environSuite) initResourceGroupSenders() azuretesting.Senders { 350 resourceGroupName := "juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d" 351 return azuretesting.Senders{ 352 s.makeSender(".*/resourcegroups/"+resourceGroupName, s.group), 353 s.makeSender(".*/virtualnetworks/juju-internal-network", s.vnet), 354 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", s.nsg), 355 s.makeSender(".*/virtualnetworks/juju-internal-network/subnets/juju-internal-subnet", s.subnet), 356 s.makeSender(".*/checkNameAvailability", s.storageNameAvailabilityResult), 357 s.makeSender(".*/storageAccounts/.*", s.storageAccount), 358 s.makeSender(".*/storageAccounts/.*/listKeys", s.storageAccountKeys), 359 } 360 } 361 362 func (s *environSuite) startInstanceSenders(controller bool) azuretesting.Senders { 363 senders := azuretesting.Senders{ 364 s.vmSizesSender(), 365 s.makeSender(".*/subnets/juju-internal-subnet", s.subnet), 366 s.makeSender(".*/Canonical/.*/UbuntuServer/skus", s.ubuntuServerSKUs), 367 s.makeSender(".*/publicIPAddresses/machine-0-public-ip", s.publicIPAddress), 368 s.makeSender(".*/networkInterfaces", s.oldNetworkInterfaces), 369 s.makeSender(".*/networkInterfaces/machine-0-primary", s.newNetworkInterface), 370 } 371 if controller { 372 senders = append(senders, 373 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", &network.SecurityGroup{ 374 Properties: &network.SecurityGroupPropertiesFormat{}, 375 }), 376 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", &network.SecurityGroup{}), 377 ) 378 } 379 senders = append(senders, 380 s.makeSender(".*/availabilitySets/.*", s.jujuAvailabilitySet), 381 s.makeSender(".*/virtualMachines/machine-0", s.virtualMachine), 382 ) 383 return senders 384 } 385 386 func (s *environSuite) networkInterfacesSender(nics ...network.Interface) *azuretesting.MockSender { 387 return s.makeSender(".*/networkInterfaces", network.InterfaceListResult{Value: &nics}) 388 } 389 390 func (s *environSuite) publicIPAddressesSender(pips ...network.PublicIPAddress) *azuretesting.MockSender { 391 return s.makeSender(".*/publicIPAddresses", network.PublicIPAddressListResult{Value: &pips}) 392 } 393 394 func (s *environSuite) virtualMachinesSender(vms ...compute.VirtualMachine) *azuretesting.MockSender { 395 return s.makeSender(".*/virtualMachines", compute.VirtualMachineListResult{Value: &vms}) 396 } 397 398 func (s *environSuite) vmSizesSender() *azuretesting.MockSender { 399 return s.makeSender(".*/vmSizes", s.vmSizes) 400 } 401 402 func (s *environSuite) makeSender(pattern string, v interface{}) *azuretesting.MockSender { 403 sender := azuretesting.NewSenderWithValue(v) 404 sender.PathPattern = pattern 405 return sender 406 } 407 408 func makeStartInstanceParams(c *gc.C, series string) environs.StartInstanceParams { 409 machineTag := names.NewMachineTag("0") 410 stateInfo := &mongo.MongoInfo{ 411 Info: mongo.Info{ 412 CACert: testing.CACert, 413 Addrs: []string{"localhost:123"}, 414 }, 415 Password: "password", 416 Tag: machineTag, 417 } 418 apiInfo := &api.Info{ 419 Addrs: []string{"localhost:246"}, 420 CACert: testing.CACert, 421 Password: "admin", 422 Tag: machineTag, 423 ModelTag: testing.ModelTag, 424 } 425 426 const secureServerConnections = true 427 icfg, err := instancecfg.NewInstanceConfig( 428 machineTag.Id(), "yanonce", imagemetadata.ReleasedStream, 429 series, "", secureServerConnections, stateInfo, apiInfo, 430 ) 431 c.Assert(err, jc.ErrorIsNil) 432 433 return environs.StartInstanceParams{ 434 Tools: makeToolsList(series), 435 InstanceConfig: icfg, 436 } 437 } 438 439 func makeToolsList(series string) tools.List { 440 var toolsVersion version.Binary 441 toolsVersion.Number = version.MustParse("1.26.0") 442 toolsVersion.Arch = arch.AMD64 443 toolsVersion.Series = series 444 return tools.List{{ 445 Version: toolsVersion, 446 URL: fmt.Sprintf("http://example.com/tools/juju-%s.tgz", toolsVersion), 447 SHA256: "1234567890abcdef", 448 Size: 1024, 449 }} 450 } 451 452 func unmarshalRequestBody(c *gc.C, req *http.Request, out interface{}) { 453 bytes, err := ioutil.ReadAll(req.Body) 454 c.Assert(err, jc.ErrorIsNil) 455 err = json.Unmarshal(bytes, out) 456 c.Assert(err, jc.ErrorIsNil) 457 } 458 459 func assertRequestBody(c *gc.C, req *http.Request, expect interface{}) { 460 unmarshalled := reflect.New(reflect.TypeOf(expect).Elem()).Interface() 461 unmarshalRequestBody(c, req, unmarshalled) 462 c.Assert(unmarshalled, jc.DeepEquals, expect) 463 } 464 465 type mockClock struct { 466 gitjujutesting.Stub 467 *testing.Clock 468 } 469 470 func (c *mockClock) After(d time.Duration) <-chan time.Time { 471 c.MethodCall(c, "After", d) 472 c.PopNoErr() 473 return c.Clock.After(d) 474 } 475 476 func (s *environSuite) TestOpen(c *gc.C) { 477 cfg := makeTestModelConfig(c) 478 env, err := s.provider.Open(cfg) 479 c.Assert(err, jc.ErrorIsNil) 480 c.Assert(env, gc.NotNil) 481 } 482 483 func (s *environSuite) TestCloudEndpointManagementURI(c *gc.C) { 484 env := s.openEnviron(c) 485 486 sender := mocks.NewSender() 487 sender.EmitContent("{}") 488 s.sender = azuretesting.Senders{sender} 489 s.requests = nil 490 env.AllInstances() // trigger a query 491 492 c.Assert(s.requests, gc.HasLen, 1) 493 c.Assert(s.requests[0].URL.Host, gc.Equals, "api.azurestack.local") 494 } 495 496 func (s *environSuite) TestStartInstance(c *gc.C) { 497 env := s.openEnviron(c) 498 s.sender = s.startInstanceSenders(false) 499 s.requests = nil 500 result, err := env.StartInstance(makeStartInstanceParams(c, "quantal")) 501 c.Assert(err, jc.ErrorIsNil) 502 c.Assert(result, gc.NotNil) 503 c.Assert(result.Instance, gc.NotNil) 504 c.Assert(result.NetworkInfo, gc.HasLen, 0) 505 c.Assert(result.Volumes, gc.HasLen, 0) 506 c.Assert(result.VolumeAttachments, gc.HasLen, 0) 507 508 arch := "amd64" 509 mem := uint64(3584) 510 rootDisk := uint64(29495) // ~30 GB 511 cpuCores := uint64(1) 512 c.Assert(result.Hardware, jc.DeepEquals, &instance.HardwareCharacteristics{ 513 Arch: &arch, 514 Mem: &mem, 515 RootDisk: &rootDisk, 516 CpuCores: &cpuCores, 517 }) 518 requests := s.assertStartInstanceRequests(c, s.requests) 519 availabilitySetName := path.Base(requests.availabilitySet.URL.Path) 520 c.Assert(availabilitySetName, gc.Equals, "juju") 521 } 522 523 func (s *environSuite) TestStartInstanceTooManyRequests(c *gc.C) { 524 env := s.openEnviron(c) 525 senders := s.startInstanceSenders(false) 526 s.requests = nil 527 528 // 6 failures to get to 1 minute, and show that we cap it there. 529 const failures = 6 530 531 // Make the VirtualMachines.CreateOrUpdate call respond with 532 // 429 (StatusTooManyRequests) failures, and then with success. 533 rateLimitedSender := mocks.NewSender() 534 rateLimitedSender.EmitStatus("(」゜ロ゜)」", http.StatusTooManyRequests) 535 successSender := senders[len(senders)-1] 536 senders = senders[:len(senders)-1] 537 for i := 0; i < failures; i++ { 538 senders = append(senders, rateLimitedSender) 539 } 540 senders = append(senders, successSender) 541 s.sender = senders 542 543 _, err := env.StartInstance(makeStartInstanceParams(c, "quantal")) 544 c.Assert(err, jc.ErrorIsNil) 545 546 c.Assert(s.requests, gc.HasLen, 8+failures) 547 s.assertStartInstanceRequests(c, s.requests[:8]) 548 549 // The last two requests should match the third-to-last, which 550 // is checked by assertStartInstanceRequests. 551 for i := 8; i < 8+failures; i++ { 552 c.Assert(s.requests[i].Method, gc.Equals, "PUT") 553 assertCreateVirtualMachineRequestBody(c, s.requests[i], s.virtualMachine) 554 } 555 556 s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{ 557 {"After", []interface{}{5 * time.Second}}, 558 {"After", []interface{}{10 * time.Second}}, 559 {"After", []interface{}{20 * time.Second}}, 560 {"After", []interface{}{40 * time.Second}}, 561 {"After", []interface{}{1 * time.Minute}}, 562 {"After", []interface{}{1 * time.Minute}}, 563 }) 564 } 565 566 func (s *environSuite) TestStartInstanceTooManyRequestsTimeout(c *gc.C) { 567 env := s.openEnviron(c) 568 senders := s.startInstanceSenders(false) 569 s.requests = nil 570 571 // 8 failures to get to 5 minutes, which is as long as we'll keep 572 // retrying before giving up. 573 const failures = 8 574 575 // Make the VirtualMachines.CreateOrUpdate call respond with 576 // enough 429 (StatusTooManyRequests) failures to cause the 577 // method to give up retrying. 578 rateLimitedSender := mocks.NewSender() 579 rateLimitedSender.EmitStatus("(」゜ロ゜)」", http.StatusTooManyRequests) 580 senders = senders[:len(senders)-1] 581 for i := 0; i < failures; i++ { 582 senders = append(senders, rateLimitedSender) 583 } 584 s.sender = senders 585 586 _, err := env.StartInstance(makeStartInstanceParams(c, "quantal")) 587 c.Assert(err, gc.ErrorMatches, "creating virtual machine.*: max duration exceeded: .*failed with.*") 588 589 s.retryClock.CheckCalls(c, []gitjujutesting.StubCall{ 590 {"After", []interface{}{5 * time.Second}}, // t0 + 5s 591 {"After", []interface{}{10 * time.Second}}, // t0 + 15s 592 {"After", []interface{}{20 * time.Second}}, // t0 + 35s 593 {"After", []interface{}{40 * time.Second}}, // t0 + 1m15s 594 {"After", []interface{}{1 * time.Minute}}, // t0 + 2m15s 595 {"After", []interface{}{1 * time.Minute}}, // t0 + 3m15s 596 {"After", []interface{}{1 * time.Minute}}, // t0 + 4m15s 597 // There would be another call here, but since the time 598 // exceeds the give minute limit, retrying is aborted. 599 }) 600 } 601 602 func (s *environSuite) TestStartInstanceDistributionGroup(c *gc.C) { 603 c.Skip("TODO: test StartInstance's DistributionGroup behaviour") 604 } 605 606 func (s *environSuite) TestStartInstanceServiceAvailabilitySet(c *gc.C) { 607 env := s.openEnviron(c) 608 s.sender = s.startInstanceSenders(false) 609 s.requests = nil 610 unitsDeployed := "mysql/0 wordpress/0" 611 params := makeStartInstanceParams(c, "quantal") 612 params.InstanceConfig.Tags[tags.JujuUnitsDeployed] = unitsDeployed 613 _, err := env.StartInstance(params) 614 c.Assert(err, jc.ErrorIsNil) 615 s.tags[tags.JujuUnitsDeployed] = &unitsDeployed 616 requests := s.assertStartInstanceRequests(c, s.requests) 617 availabilitySetName := path.Base(requests.availabilitySet.URL.Path) 618 c.Assert(availabilitySetName, gc.Equals, "mysql") 619 } 620 621 func (s *environSuite) assertStartInstanceRequests(c *gc.C, requests []*http.Request) startInstanceRequests { 622 // Clear the fields that don't get sent in the request. 623 s.publicIPAddress.ID = nil 624 s.publicIPAddress.Name = nil 625 s.publicIPAddress.Properties.IPAddress = nil 626 s.newNetworkInterface.ID = nil 627 s.newNetworkInterface.Name = nil 628 (*s.newNetworkInterface.Properties.IPConfigurations)[0].ID = nil 629 s.jujuAvailabilitySet.ID = nil 630 s.jujuAvailabilitySet.Name = nil 631 s.virtualMachine.ID = nil 632 s.virtualMachine.Name = nil 633 s.virtualMachine.Properties.ProvisioningState = nil 634 635 // Validate HTTP request bodies. 636 c.Assert(requests, gc.HasLen, 8) 637 c.Assert(requests[0].Method, gc.Equals, "GET") // vmSizes 638 c.Assert(requests[1].Method, gc.Equals, "GET") // juju-testenv-model-deadbeef-0bad-400d-8000-4b1d0d06f00d 639 c.Assert(requests[2].Method, gc.Equals, "GET") // skus 640 c.Assert(requests[3].Method, gc.Equals, "PUT") 641 assertRequestBody(c, requests[3], s.publicIPAddress) 642 c.Assert(requests[4].Method, gc.Equals, "GET") // NICs 643 c.Assert(requests[5].Method, gc.Equals, "PUT") 644 assertRequestBody(c, requests[5], s.newNetworkInterface) 645 c.Assert(requests[6].Method, gc.Equals, "PUT") 646 assertRequestBody(c, requests[6], s.jujuAvailabilitySet) 647 c.Assert(requests[7].Method, gc.Equals, "PUT") 648 assertCreateVirtualMachineRequestBody(c, requests[7], s.virtualMachine) 649 650 return startInstanceRequests{ 651 vmSizes: requests[0], 652 subnet: requests[1], 653 skus: requests[2], 654 publicIPAddress: requests[3], 655 nics: requests[4], 656 networkInterface: requests[5], 657 availabilitySet: requests[6], 658 virtualMachine: requests[7], 659 } 660 } 661 662 func assertCreateVirtualMachineRequestBody(c *gc.C, req *http.Request, expect *compute.VirtualMachine) { 663 // CustomData is non-deterministic, so don't compare it. 664 // TODO(axw) shouldn't CustomData be deterministic? Look into this. 665 var virtualMachine compute.VirtualMachine 666 unmarshalRequestBody(c, req, &virtualMachine) 667 c.Assert(to.String(virtualMachine.Properties.OsProfile.CustomData), gc.Not(gc.HasLen), 0) 668 virtualMachine.Properties.OsProfile.CustomData = to.StringPtr("<juju-goes-here>") 669 c.Assert(&virtualMachine, jc.DeepEquals, expect) 670 } 671 672 type startInstanceRequests struct { 673 vmSizes *http.Request 674 subnet *http.Request 675 skus *http.Request 676 publicIPAddress *http.Request 677 nics *http.Request 678 networkInterface *http.Request 679 availabilitySet *http.Request 680 virtualMachine *http.Request 681 } 682 683 func (s *environSuite) TestBootstrap(c *gc.C) { 684 defer envtesting.DisableFinishBootstrap()() 685 686 ctx := envtesting.BootstrapContext(c) 687 env := prepareForBootstrap(c, ctx, s.provider, &s.sender) 688 689 s.sender = s.initResourceGroupSenders() 690 s.sender = append(s.sender, s.startInstanceSenders(true)...) 691 s.requests = nil 692 result, err := env.Bootstrap( 693 ctx, environs.BootstrapParams{ 694 AvailableTools: makeToolsList(series.LatestLts()), 695 }, 696 ) 697 c.Assert(err, jc.ErrorIsNil) 698 c.Assert(result.Arch, gc.Equals, "amd64") 699 c.Assert(result.Series, gc.Equals, series.LatestLts()) 700 701 c.Assert(len(s.requests), gc.Equals, 17) 702 703 c.Assert(s.requests[0].Method, gc.Equals, "PUT") // resource group 704 c.Assert(s.requests[1].Method, gc.Equals, "PUT") // vnet 705 c.Assert(s.requests[2].Method, gc.Equals, "PUT") // network security group 706 c.Assert(s.requests[3].Method, gc.Equals, "PUT") // subnet 707 c.Assert(s.requests[4].Method, gc.Equals, "POST") // check storage account name 708 c.Assert(s.requests[5].Method, gc.Equals, "PUT") // create storage account 709 c.Assert(s.requests[6].Method, gc.Equals, "POST") // get storage account keys 710 711 assertRequestBody(c, s.requests[0], &s.group) 712 713 s.vnet.ID = nil 714 s.vnet.Name = nil 715 assertRequestBody(c, s.requests[1], s.vnet) 716 717 securityRules := []network.SecurityRule{{ 718 Name: to.StringPtr("SSHInbound"), 719 Properties: &network.SecurityRulePropertiesFormat{ 720 Description: to.StringPtr("Allow SSH access to all machines"), 721 Protocol: network.TCP, 722 SourceAddressPrefix: to.StringPtr("*"), 723 SourcePortRange: to.StringPtr("*"), 724 DestinationAddressPrefix: to.StringPtr("*"), 725 DestinationPortRange: to.StringPtr("22"), 726 Access: network.Allow, 727 Priority: to.IntPtr(100), 728 Direction: network.Inbound, 729 }, 730 }} 731 assertRequestBody(c, s.requests[2], &network.SecurityGroup{ 732 Location: to.StringPtr("westus"), 733 Tags: s.nsg.Tags, 734 Properties: &network.SecurityGroupPropertiesFormat{ 735 SecurityRules: &securityRules, 736 }, 737 }) 738 739 s.subnet.ID = nil 740 s.subnet.Name = nil 741 assertRequestBody(c, s.requests[3], s.subnet) 742 743 assertRequestBody(c, s.requests[4], &storage.AccountCheckNameAvailabilityParameters{ 744 Name: to.StringPtr(fakeStorageAccount), 745 Type: to.StringPtr("Microsoft.Storage/storageAccounts"), 746 }) 747 748 assertRequestBody(c, s.requests[5], &storage.AccountCreateParameters{ 749 Location: to.StringPtr("westus"), 750 Tags: s.storageAccount.Tags, 751 Properties: &storage.AccountPropertiesCreateParameters{ 752 AccountType: "Standard_LRS", 753 }, 754 }) 755 } 756 757 func (s *environSuite) TestAllInstancesResourceGroupNotFound(c *gc.C) { 758 env := s.openEnviron(c) 759 sender := mocks.NewSender() 760 sender.EmitStatus("resource group not found", http.StatusNotFound) 761 s.sender = azuretesting.Senders{sender} 762 _, err := env.AllInstances() 763 c.Assert(err, jc.ErrorIsNil) 764 } 765 766 func (s *environSuite) TestStopInstancesNotFound(c *gc.C) { 767 env := s.openEnviron(c) 768 sender := mocks.NewSender() 769 sender.EmitStatus("vm not found", http.StatusNotFound) 770 s.sender = azuretesting.Senders{sender, sender, sender} 771 err := env.StopInstances("a", "b") 772 c.Assert(err, jc.ErrorIsNil) 773 } 774 775 func (s *environSuite) TestStopInstances(c *gc.C) { 776 env := s.openEnviron(c) 777 778 // Security group has rules for machine-0 but not machine-1, and 779 // has a rule that doesn't match either. 780 nsg := makeSecurityGroup( 781 makeSecurityRule("machine-0-80", "10.0.0.4", "80"), 782 makeSecurityRule("machine-0-1000-2000", "10.0.0.4", "1000-2000"), 783 makeSecurityRule("machine-42", "10.0.0.5", "*"), 784 ) 785 786 // Create an IP configuration with a public IP reference. This will 787 // cause an update to the NIC to detach public IPs. 788 nic0IPConfiguration := makeIPConfiguration("10.0.0.4") 789 nic0IPConfiguration.Properties.PublicIPAddress = &network.SubResource{} 790 nic0 := makeNetworkInterface("nic-0", "machine-0", nic0IPConfiguration) 791 792 s.sender = azuretesting.Senders{ 793 s.networkInterfacesSender( 794 nic0, 795 makeNetworkInterface("nic-1", "machine-1"), 796 makeNetworkInterface("nic-2", "machine-1"), 797 ), 798 s.virtualMachinesSender(makeVirtualMachine("machine-0")), 799 s.publicIPAddressesSender( 800 makePublicIPAddress("pip-0", "machine-0", "1.2.3.4"), 801 ), 802 s.makeSender(".*/virtualMachines/machine-0", nil), // DELETE 803 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg), // GET 804 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-80", nil), // DELETE 805 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg/securityRules/machine-0-1000-2000", nil), // DELETE 806 s.makeSender(".*/networkInterfaces/nic-0", nic0), // PUT 807 s.makeSender(".*/publicIPAddresses/pip-0", nil), // DELETE 808 s.makeSender(".*/networkInterfaces/nic-0", nil), // DELETE 809 s.makeSender(".*/virtualMachines/machine-1", nil), // DELETE 810 s.makeSender(".*/networkSecurityGroups/juju-internal-nsg", nsg), // GET 811 s.makeSender(".*/networkInterfaces/nic-1", nil), // DELETE 812 s.makeSender(".*/networkInterfaces/nic-2", nil), // DELETE 813 } 814 err := env.StopInstances("machine-0", "machine-1", "machine-2") 815 c.Assert(err, jc.ErrorIsNil) 816 817 s.storageClient.CheckCallNames(c, 818 "NewClient", "DeleteBlobIfExists", "DeleteBlobIfExists", 819 ) 820 s.storageClient.CheckCall(c, 1, "DeleteBlobIfExists", "osvhds", "machine-0") 821 s.storageClient.CheckCall(c, 2, "DeleteBlobIfExists", "osvhds", "machine-1") 822 } 823 824 func (s *environSuite) TestConstraintsValidatorUnsupported(c *gc.C) { 825 validator := s.constraintsValidator(c) 826 unsupported, err := validator.Validate(constraints.MustParse( 827 "arch=amd64 tags=foo cpu-power=100 virt-type=kvm", 828 )) 829 c.Assert(err, jc.ErrorIsNil) 830 c.Assert(unsupported, jc.SameContents, []string{"tags", "cpu-power", "virt-type"}) 831 } 832 833 func (s *environSuite) TestConstraintsValidatorVocabulary(c *gc.C) { 834 validator := s.constraintsValidator(c) 835 _, err := validator.Validate(constraints.MustParse("arch=armhf")) 836 c.Assert(err, gc.ErrorMatches, 837 "invalid constraint value: arch=armhf\nvalid values are: \\[amd64\\]", 838 ) 839 _, err = validator.Validate(constraints.MustParse("instance-type=t1.micro")) 840 c.Assert(err, gc.ErrorMatches, 841 "invalid constraint value: instance-type=t1.micro\nvalid values are: \\[D1 Standard_D1\\]", 842 ) 843 } 844 845 func (s *environSuite) TestConstraintsValidatorMerge(c *gc.C) { 846 validator := s.constraintsValidator(c) 847 cons, err := validator.Merge( 848 constraints.MustParse("mem=3G arch=amd64"), 849 constraints.MustParse("instance-type=D1"), 850 ) 851 c.Assert(err, jc.ErrorIsNil) 852 c.Assert(cons.String(), gc.Equals, "instance-type=D1") 853 } 854 855 func (s *environSuite) constraintsValidator(c *gc.C) constraints.Validator { 856 env := s.openEnviron(c) 857 s.sender = azuretesting.Senders{s.vmSizesSender()} 858 validator, err := env.ConstraintsValidator() 859 c.Assert(err, jc.ErrorIsNil) 860 return validator 861 } 862 863 func (s *environSuite) TestAgentMirror(c *gc.C) { 864 env := s.openEnviron(c) 865 c.Assert(env, gc.Implements, new(envtools.HasAgentMirror)) 866 cloudSpec, err := env.(envtools.HasAgentMirror).AgentMirror() 867 c.Assert(err, jc.ErrorIsNil) 868 c.Assert(cloudSpec, gc.Equals, simplestreams.CloudSpec{ 869 Region: "westus", 870 Endpoint: "https://storage.azurestack.local/", 871 }) 872 }